John Ferguson Smart is currently Project Director in a French IT firm specialised in
J2EE solutions. He holds a PhD in Computer Science from the University of
Aix-Marseille, France. His specialities are J2EE architecture and developpment
and IT project management, including offshore project management. He works on
large-scale J2EE projects for government and business with teams spread across
France, Egpyt and Algeria, and writes the occasional technical article in the J2EE field.
In this article, we examine the Hibernate Criteria API, a powerful and elegent alternative to HQL well adapted for dynamic search functionalities where complex Hibernate queries have to be generated 'on-the-fly'. Be sure you catch the first article in the series, Hibernate Querying 101: Tips and Tricks, before you start this tutorial.
Complex multi-criteria search functionalities are frequently found in web-enabled business applications. Multi-criteria search screens typically have a large number of search criteria, many of which are optional. Well-known general public multi-criteria search functionalities can be found in the 'Advanced Search' screens on sites such as Yahoo, AltaVista, and Lycos.
In many public web sites (such as Amazon), complex multi-criteria search functionnalites tend to be less frequent these days, for the very good reason that users generally don't use them (or at least, not efficiently). EBay for example has a powerful multi-criteria search functionality which for which the business case is probably fairly limited. A good key-word and full-text search is usually preferable for sites aimed at the general public.
There are exceptions, though. Travel sites, such as air and train reservation sites, are cases where multi-criteria searches are both necessary and intuitive.
As we have said, business web sites are also another story.
The traditional approach to implementing a multi-criteria search query with Hibernate involves building an HQL query on-the-fly, based on the search criteria entered by the user.
Here is an example of one such approach :
Map parameters = new HashMap();
StringBuffer queryBuf = new StringBuffer("from Sale s ");
boolean firstClause = true;
if (startDate != null) {
queryBuf.append(firstClause ? " where " : " and ");
queryBuf.append("s.date >= :startDate");
parameters.put("startDate",startDate);
firstClause = false;
}
if (endDate != null) {
queryBuf.append(firstClause ? " where " : " and ");
queryBuf.append("s.date <= :endDate");
parameters.put("endDate",endDate);
firstClause = false;
}
// And so on for all the query criteria...
String hqlQuery = queryBuf.toString();
Query query = session.createQuery(hqlQuery);
//
// Set query parameter values
//
Iterator iter = parameters.keySet().iterator();
while (iter.hasNext()) {
String name = (String) iter.next();
Object value = map.get(name);
query.setParameter(name,value);
}
//
// Execute the query
//
List results = query.list();
This approach is cumbersome and error-prone. It is also risky in a team-development context, as inexperienced developpers will often take dangerous short-cuts using this approach. During code review, I?ve often come across multi-criteria search functions using error-prone String concatenation, direct use of query parameters in the query string, and so on :
...
if (startDate != null) {
if (firstClause) {
query = query + " where ";
} else {
query = query + " and ";
query += " s.date >= '" + startDate + "'";
}
// And so on...
As we will see, the Hibernate Criteria API provides a safer and cleaner solution to this type of problem.
The Hibernate Criteria API provides an elegant way of building on-the-fly dynamic queries on Hibernate-persisted databases. Using this technique, the previous 24-line example can be coded more consicely and more clearly using a mere 8 lines of code :
Criteria criteria = session.createCriteria(Sale.class);
if (startDate != null) {
criteria.add(Expression.ge("date",startDate);
}
if (endDate != null) {
criteria.add(Expression.le("date",endDate);
}
List results = criteria.list();
Let's have a look at the use of the Hibernate Criteria API in more detail.
A Criteria object is created using the createCriteria()
method in the Hibernate session object :
Criteria criteria = session.createCriteria(Sale.class);
Once created, you add Criterion objects (generally obtained
from static methods of the Expression class) to build
the query. Methods such as setFirstResult(),
setMaxResults(), and setCacheable() may be
used to customize the query behaviour in the same way as in the
Query interface. Finally, to execute the query, the
list() (or, if appropriate uniqueResult())
method is invoqued :
List sales = session.createCriteria(Sale.class)
.add(Expression.ge("date",startDate);
.add(Expression.le("date",endDate);
.addOrder( Order.asc("date") )
.setFirstResult(0)
.setMaxResults(10)
.list();
The Hibernate Criteria API supports a rich set of comparaison operators.
The standard SQL operators (=, <, ?, >, ? ) are supported respectively by the following methods in the Expression class : eq(), lt(), le(), gt(), ge() :
session.createCriteria(Sale.class)
.add(Expression.lt("date",salesDate))
.list();
session.createCriteria(Sale.class)
.add(Expression.eq("product",someProduct))
.list();
Note that, as in HQL and unlike in a JDBC-based query, you are free to use business objects as query parameters, without having to use primary and foriegn key references.
The API also provides additional comparaison operators : like,
between, in, isNull, isNotNull...
session.createCriteria(Sale.class)
.add(Expression.between("date", startDate, endDate))
.list();
session.createCriteria(Product.class)
.add(Expression.like("A%"))
.list();
session.createCriteria(Product.class)
.add(Expression.in("color",selectedColors))
.list();
In addition, the API provides convenient operators which :
eqProperty, ltProperty, etc?
session.createCriteria(Sale.class)
.eqProperty("saleDate","releaseDate")
.list();
In HQL (and SQL), the order by clause allows you to order
your query results. Using the Query API, this is done using the
addOrder() method and the Order class :
session.createCriteria(Sale.class)
.add(Expression.between("date", startDate, endDate))
.addOrder( Order.desc("date") )
.addOrder( Order.asc("product.number") )
.list();
When writing HQL queries, join clauses are often necessary to optimise the query using a "left join fetch" clause, as in the following example (I discuss this type of optimisation in another article.)
from Sale sale where sale.date > :startDate left join fetch sale.product
When using the criteria API, you can do the same thing using the setFetchMode()
function :
session.createCriteria(Sale.class)
.setFetchMode("product",FetchMode.EAGER)
.list();
Imagine the case of an online shop which sells shirts. Each shirt model comes in a certain number of available sizes. You want a query to find all the shirt models with sizes over 40. In HQL, the query might be the following :
from Shirt shirt join shirt.availableSizes size where size.number > 40
Using the Criteria API, you use the createCriteria() to create
an inner join between the two tables, as in the following example :
session.createCriteria(Shirt.class)
.createCriteria("availableSizes")
.add(Expression.gt("number", new Integer(40)))
.list();
Another way of doing this is to use the createAlias() method,
which does not involve creating a new instance of the Criteria class.
session.createCriteria(Shirt.class)
.createAlias("availableSizes","size")
.add(Expression.gt("size.number", new Integer(40)))
.list();
Note that in both these cases, the availableSizes collection
in each Shirt object will not be initialised : it is simply
used as part of the search criteria.
As we can see, the Hibernate Criteria API is without doubt tailor-made for dynamic query generation. It is worth noting however that there are many places where its use is not appropriate, and indeed will create code which is more complex and harder to maintain than using a standard HQL query. If the query to be executed does not involve dynamic construction (that is, the HQL query can be externalised as a named query in the Hibernate mapping files), than the use of the Hibernate Criteria API is probably not appropriate. When possible, externalising static queries presents a number of advantages :
The Hibernate Criteria API is a powerful and elegent library which is well adapted for implementing multi-criteria search functionnalities where Hibernate queries must be built 'on-the-fly'. Using it in appropriate circumstances will result in cleaner, clearer, more reliable and more maintainable code.