One of the biggest strengths of O/R mappers is that many have object-oriented "query by criteria" API. I will show what I mean by that in a bit. But to explain why I think a criteria API is such a great thing I will show the alternatives first.

Lets take a common search form scenario where a user can specify a number of customer filtering options like date added, name, the customer should have an address, the customer should be linked to a specific salesman, etc. If you were forced to use a stored procedure how would you implement this query?

I have seen a couple of solutions to problems like this, here is one that is really bad:

IF @name = ''
BEGIN 
     --- the whole query for this case 
END    
ELSE IF @name <> '' AND @shouldHaveAddress IS NOT NULL 
BEGIN 
    --- the whole query for this case 
END 
ELSE IF @name <> '' AND @shouldHaveAddress IS NOT NULL AND @salesmanNr <> '' 
    --- the whole query for this case 
END

Here the whole query is duplicated for each possible parameter mutation. I have seen a stored procedure like this recently that duplicated a very long query about 10 times (with very small variations in the where and join clauses).

Another solution is to complicate the query with embedded IFs, CASE and additional OR statements. But this solution does not only make the query unintelligible but also I think has some limitations. The solution that I see many arrive at is to build the query using string concatenation.

SET @sqlSelect = 'SELECT distinct ' + @NEWLINE + ' ' + @returnColumns + @NEWLINE
SET @sqlFrom = ' FROM Customers cust' + @NEWLINE
IF @companies is NOT NULL
BEGIN
  SET @sqlFROM = @sqlFROM + ' JOIN Companies comp on comp.Id=cust.CompanyId' + @NEWLINE
  SET @sqlFROM = @sqlFROM + ' LEFT JOIN Addresses addr on addr.Id=cust.AddressId ' + @NEWLINE  
END
IF @salesmanId IS NOT NULL
BEGIN
  SET @sqlFROM = @sqlFROM + ' JOIN Salesmen sal on sal.Id=' + @salesmanId 
END
...
..
.

I actually think that building up the query like this is not all that bad, however I would do it in C# and skip the stored procedure. The reason I prefer this approach is that it there is less duplication and because it is somewhat easier to maintain. But it is far from what we want!

So how do can we handle this via the nhibernate query api?

ICriteria query = session.CreateCriteria(typeof(Employee));

if (searchOptions.FirstName != null)
{
  query.Add(Expression.Eq("FirstName", searchOptions.FirstName));
}

if (!searchOptions.LastName != null)
{
  query.Add(Expression.Eq("LastName", searchOptions.LastName));
}

if (searchOptions.PhoneNumber != null)
{
  query.CreateCriteria("PhoneNumbers")
    .Add(Expression.Like("Number", searchOptions.PhoneNumber + "%"));
}
return query.List<Employee>();

The nhibernate criteria api might be verbose for simple scenarios compared to a SQL query in a string, but it is scenarios like the above that it really shines :) But wait it can be better. If you use Ayende's great NHibernate Query Generator (NHQ) you can do this:

QueryBuilder<Employee> query = new QueryBuilder<Employee>();

if (options.FirstName != null)
{
  query &= Where.Employee.FirstName == options.FirstName;
}

if (options.LastName != null)
{
  query &= Where.Employee.LastName == options.LastName;
}

if (options.PhoneNumber != null)
{
  query &= Where.Employee.PhoneNumbers.Number.Like(options.PhoneNumber, MatchMode.Start);
}            

return Repository<Employee>.FindAll(query, OrderBy.Employee.LastName.Desc);

NHQ is a very clever code-gen util that you setup as a post-build step. It generates the Where/QueryBuilder classes from the nhibernate mapping files. The result is that you do not need any strings in your queries! More commonly you use it like this:

return Repository<User>.FindAll(Where.User.Name == name);

I think it comes very close to using LINQ. So to end this post I guess I need to show how the above query would look with LINQ, because LINQ to SQL handles the above scenario pretty well.

IQueryable<Employee> query = linqContext.Employees;

if (options.FirstName != null)
{
  query = query.Where(emp => emp.FirstName == options.FirstName);
}

if (options.LastName != null)
{
  query = employees.Where(emp => emp.LastName == options.LastName);
}

if (options.PhoneNumber != null)
{
  query = from emp in query
	  from phoneNr in emp.PhoneNumbers
	  where phoneNr.Number.StartsWith(options.PhoneNumber)
	  select emp;
}            

return employees.ToList();

The above code is nice, but I think querying across relations is handled nicer in the nhibernate critiera api especially when using NHQ. Well that is all, now you know why I think a criteria API is are one of the mayor reasons to use an O/R mapper.

5 comments:

pdu said...

Cqn you post a complete example (with the signature of the function), it is very interesting...

P

Torkel Ödegaard said...

Here is a complete example using NHQ:

public IList<Employee> Find(EmployeeSearchOptions options)
{
QueryBuilder<Employee> query = Where.Employee.CompanyName == options.CompanyName;

if (options.FirstName != null)
{
query &= Where.Employee.FirstName == options.FirstName;
}

if (options.LastName != null)
{
query &= Where.Employee.LastName == options.LastName;
}

if (options.PhoneNumber != null)
{
query &= Where.Employee.PhoneNumbers.Number.Like(options.PhoneNumber, MatchMode.Start);
}

return Repository<Employee>.FindAll(query, OrderBy.Employee.LastName.Desc);
}

Anonymous said...

I think your like statement didn't need to concatenate the % symbol, in the Pre-NHQ version. Why not use MatchMode?

query.CreateCriteria("PhoneNumbers").Add(Expression.Like("Number", searchOptions.PhoneNumber, MatchMode.Start))

Torkel Ödegaard said...

Yes, you are correct, you can also use MatchMode. I am not sure why I didn't use it here, I usually do.

Steve Smith said...

Thanks for your valuable information on nhibernate criteria api

Online Linq Training
Linq Training in Chennai