Complex Searching / Querying With NHibernate

Let us take the following form, the like of which has been known to cause grown man to cry. The client want something "very simple": I just want to be able to search on my policies, and this is the stuff that I think that I need now, and I'll need more in the future.

(Image from clipboard).png

Just a hint on the data model as relevant to this search form, it goes something like this:

  • Polic:
    • M:M - Clients:
      • 1:1 Primary Address
    • 1:M - Claims:
        • 1:M Payments
  • Account:
    • 1:M - Policies
Note that I make no promises that this is a viable model for insurance, I just needed a complex domain and I can't use my usual Employees > Salaries example, since it would hit too close to the real issue that I did.

Anyway, the problem with this kind of fomrs is that they are complex beasts. I have seen search forms that were two pages long, and were accompanied with a manual (just for the search form) that was bigger than the entire system specification. The real kicker here is that there isn't a single path that the user is going through, the system should be able to handle any combination of search terms, and ignore any that isn't relevant.

There is additional complexity added by the fact that this data is not sitting in the same table, actually, just from the rough data modle above, it looks like it is sitting in no less than 6 tables.

This is also the place where the Stored Procedure approach hurts the most, in my experiance.

Let us start by looking at the UI code for this, shall we?

protected void Search_Click(object sender, EventArgs e)

{

       PoliciesFinder finder = new PoliciesFinder();

       if (txtClientId.Text != "")

              finder.ClientId = int.Parse(txtClientId.Text);

       if (txtClientName.Text != "")

              finder.ClientNameLike = txtClientName.Text;

       if (ddlPolicyType.SelectedIndex != 0) //everything

              finder.PolicyType = (PolicyType )Enum.Parse(typeof (PolicyType), ddlPolicyStatus.Text);

       finder.PolicyStatus = (PolicyStatus )Enum.Parse(typeof (PolicyStatus), ddlPolicyStatus.Text);

       finder.City = (WellKnownCities )Enum.Parse(typeof (WellKnownCities), ddlCity.Text);

       if (txtClaimNumber.Text != "")

              finder.ClaimNumber = int.Parse(txtClaimNumber.Text);

       if (ClaimStatus_Paid.Checked)

              finder.ClaimStatus = ClaimStatus.Paid;

       if (ClaimStatus_Processing.Checked)

              finder.ClaimStatus = ClaimStatus.Processing;

       if (ClaimStatus_Rejected.Checked)

              finder.ClaimStatus = ClaimStatus.Rejected;

       if (txtPaymentNum.Text != "")

              finder.PaymentNumber = int.Parse(txtPaymentNum.Text);

       if (chkAccountSize_Small.Checked)

              finder.AccountSize |= AccountSize.Small;

       if(chkAccountSize_Medium.Checked)

              finder.AccountSize |= AccountSize.Medium;

       if (chkAccountSize_Big.Checked)

              finder.AccountSize |= AccountSize.Big;

       if (chkAccountSize_MoneyCow.Checked)

              finder.AccountSize |= AccountSize.MoneyCow;

 

       IList<Policy> policies = finder.Find();

       gridPolicies.DataSource = policies;

       gridPolicies.DataBind();

}

[Despite its size, this is still just first attempt, with no checks for user input, etc]

What we have here is a Finder object, which the UI uses. It is not the job of the UI to make searches, especially not searches this complex. If this is more than a one liner that can be expressed with NHQG, I tend to create a finder object for this, which will contain all the logic for the search. All the properties on finder are nullables, since they are all optionals...

public class PoliciesFinder

{

       int? clientId;

       string clientNameLike;

       PolicyType? policyType;

       PolicyStatus? policyStatus;

       WellKnownCities? city;

       int? claimNumber;

       ClaimStatus? claimStatus;

       int? paymentNumber;

       AccountSize accountSize;

       ...
       ...
       ...
}

Now, let us turn to the implementation of the Find() method.

public IList<Policy> Find()

{

       DetachedCriteria query = DetachedCriteria.For(typeof(Policy), "policy");

 

       AddClientQuery(query);

 

       AddPolicyQuery(query);

 

       AddClaimsQuery(query);

 

       AddAccountQuery(query);

 

       AddDateRangeQuery(query);

 

       return query.GetExecutableCriteria(Context.Session).List<Policy>();

}

We create a detached criteria, and start to pass it to each of the helper methods...

private void AddClientQuery(DetachedCriteria query)

{

       DetachedCriteria clientCriteria = null;

      

       if(clientId.HasValue)

       {

              clientCriteria = DetachedCriteria.For(typeof (Client));

              clientCriteria.Add(Expression.Eq("Id", clientId.Value));     

       }

      

       if(clientNameLike!=null)

       {

              clientCriteria = clientCriteria ?? DetachedCriteria.For(typeof (Client));

              clientCriteria.Add(Expression.Like("Name", clientNameLike, MatchMode.Anywhere));

       }

      

       if(city.HasValue)

       {

              clientCriteria = clientCriteria ?? DetachedCriteria.For(typeof(Client));

              clientCriteria.Add(Expression.Eq("Area",city.Value));

       }

 

       if (clientCriteria != null)

       {

              clientCriteria.SetProjection(Projections.Property("Id"));

              clientCriteria.Add(Property.ForName("Id").EqProperty("client.Id"));

              query.CreateCriteria("Clients","client")

                     .Add(Subqueries.Exists(clientCriteria));

       }

}

The interesting part here is in the end, where we use a subquery to check for the existance of a client that match the descriptions.

private void AddPolicyQuery(DetachedCriteria query)

{

       if(policyType.HasValue)

       {

              query.Add(Expression.Eq("Type", policyType.Value));

       }

      

       if(policyStatus.HasValue)

       {

              query.Add(Expression.Eq("Status", policyStatus.Value));

       }

}

Nothing interesting above, but the next one is interesting:

private void AddClaimsQuery(DetachedCriteria query)

{

       DetachedCriteria claimCriteria = null;

      

       if(claimNumber.HasValue)

       {

              claimCriteria = query.CreateCriteria("Claims");

              claimCriteria.Add(Expression.Eq("Id", claimNumber.Value));

       }

      

       if(claimStatus.HasValue)

       {

              claimCriteria = claimCriteria ?? query.CreateCriteria("Claims");

              claimCriteria.Add(Expression.Eq("Status", claimStatus.Value));

       }

      

       if(paymentNumber.HasValue)

       {

              claimCriteria = claimCriteria ?? query.CreateCriteria("Claims");

              claimCriteria

                     .CreateCriteria("Payments", "payment")

                     .Add(Expression.Eq("Id", paymentNumber.Value));

       }

}

Notice the use of CreateCriteria to join to other tables with ease, so we can check them as well.

private void AddAccountQuery(DetachedCriteria query)

{

       if(accountSize!=AccountSize.None)

       {

              ICriterion accountSizeExpr=null;

              foreach (AccountSize value in Enum.GetValues(typeof(AccountSize)))

              {

                     if((accountSize & value ) == value)

                     {

                           if(accountSizeExpr==null)

                           {

                                  accountSizeExpr = Expression.Eq("Size", value);

                           }

                           else

                            {

                                  accountSizeExpr = Expression.Or(

                                         accountSizeExpr,

                                         Expression.Eq("Size", value));

                           }

                     }

              }

              query.CreateCriteria("Account")

                     .Add(accountSizeExpr);

       }

}

And this example shows the fun you can have with a search API :-) Now all we have to do is...

private void AddDateRangeQuery(DetachedCriteria query)

{

       if(startDate.HasValue)

       {

              query.Add(Expression.Le("Range.Start", startDate.Value));

       }

      

       if(endDate.HasValue)

       {

              query.Add(Expression.Ge("Range.End", endDate.Value));

       }

}

And we are basically done. We still need to test this finder, and to see that it give us the correct results, of course (which I hadn't done in this case, by the way). The advantages of this approach is that the finder is easily extensible in the future, if the customer would like me to give him additional fields, I can simply add it to the UI and then add additional querying to the finder. The nice thing about it is that I am not concating strings, so I can just play with it just about everywhere I want, and let NHibernate figure out the final query.

One thing to be aware of is that the final SQL query is probably going to be fairly involved if all the filterring are going to be used, but that is not an expected usage for this kind of screen.

Print | posted on Thursday, December 07, 2006 8:09 AM

Feedback


Gravatar

#  12/7/2006 9:07 AM Ovidiu

Thanks for the details!
As a side question, isn't it recommended to use txtClaimNumber.Text.Length == 0 instead of txtClaimNumber.Text != &quot;&quot; ?
And we stopped using the txt / chk a while ago, is old-school the new balck and noone knows yet?


Gravatar

#  12/7/2006 10:02 AM Paulo Quicoli

First of all, God bless Jerusalem !

Well,

Very nice you example, but could you please explain me what DetachedCriteria class is ? In nHibernate examples i always saw &quot;Cirteria&quot; class....

thanks, from a brazilian friend.


Gravatar

#  12/7/2006 1:51 PM Liang

Very useful example. Thanks!


Gravatar

#  12/7/2006 4:53 PM C-J Berg

Ovidiu: The best way for checking if a string is null or empty is to use String.IsNullOrEmpty() in .NET Framework 2.0. It will use the fastest possible implementation available in the runtime environment your application is running.

It is true that str.Length == 0 is slightly faster than str != &quot;&quot; on .NET Framework 1.x running on x86, and I would think that it holds true for 2.0 as well. You need of course make sure the string cannot be null before checking its length, but TextBox controls always return String.Empty instead of null.

This kind of optimization is hardly ever noticeable though, unless you’re in a tight loop. It’s way more important to run a profiler frequently and let it guide you to the real bottlenecks of your application.

Oren: Great example!


Gravatar

#  12/7/2006 9:20 PM Ayende Rahien

txtClaimNumber.Text.Length == 0 will throw if the text property is null.
I find != &quot;&quot; much clearer, anyway.

The txt, chk prefixes are the only prefixes that I tend to use, since they are a great aid when using the code-behind.


Gravatar

#  12/7/2006 9:33 PM Ayende Rahien

DetachedCriteria is exactly like a Criteria, but it can be craeted without a session, useful if your session is hidden deep in the code. (Think Active Record or Rhino Commons Repository).


Gravatar

#  12/8/2006 1:47 PM C-J Berg

Well, &quot;txtClaimNumber.Text.Length == 0&quot; will not throw if it's an instance of a TextBox control, because it always return String.Empty if the value is null (even if you explicitly set it to null). Granted, making such assumptions is just begging for bugs. And as you point out it might be less readable to some, and readability should always be a top priority.


Gravatar

#  12/10/2006 9:05 PM Michael

Thanks for the post Oren, but if I'm using Active Record how can I get an instance of the DetachedCriteria property?