Hibernate-specific extensions to the Criteria API
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
Most developers know that the JPA specification defines the string-based JPQL query language and that Hibernate extends it to support things like database-specific functions, window functions, and set-based operations. But most developers don’t know that since version 6, Hibernate has done the same for JPA’s Criteria API.
Extending an API is, of course, a little more complex than doing the same for a string-based query language. To extend the features of JPQL, the Hibernate team only needs to add more features to the parser of the query string and doesn’t need to change any of the official APIs. The extension of the Criteria API requires additional interfaces and new methods that return those interfaces.
Hibernate 6 handles this by providing the HibernateCriteriaBuilder interface, which extends JPA’s CriteriaBuilder interface, and by adding a method to its proprietary Session interface to get a HibernateCriteriaBuilder instance.
The HibernateCriteriaBuilder interface
Before we talk about the HibernateCriteriaBuilder interface, we need to take one step back and take a look at the creation of a standard CriteriaQuery. And after that, I’ll show you how to get a HibernateCriteriaBuilder and the features it adds to JPA’s standard Criteria API.
Working with JPA’s CriteriaBuilder interface
The first step to using the Criteria API is always a call of the getCriteriaBuilder method on the EntityManager interface. That method returns an instance of JPA’s CriteriaBuilder, which you can use to create different parts of your query. In the following code snippet, I use it to create a very basic query that returns all ChessGame entities that a player played with the white pieces whose name ends on “anssen”.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
CriteriaBuilder cBuilder = em.getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.like(game.get("playerWhite"), "%anssen"));
em.getTransaction().commit();
em.close();
As you can see, I use JPA’s CriteriaBuilder in 2 places:
- To create a CriteriaQuery object that represents a query that returns ChessGame objects.
- To create a like predicate for the WHERE clause of the query that checks if the playerWhite attribute of the ChessGame is like “%anssen”
JPA’s CriteriaBuilder interface provides lots of other methods that you can use to instantiate different kinds of CriteriaQuery objects, build more complex WHERE clauses, and call database functions. I explain all of that in more detail in the Advanced Hibernate course included in the Persistence Hub, and you can find a full list of all methods in the official Javadoc.
How to get a HibernateCriteriaBuilder instance
Hibernate’s HibernateCriteriaBuilder interface extends JPA’s CriteriaBuilder interface. Due to that, an implementation of the HibernateCriteriaBuilder supports the same methods, and you can use it in the same way I showed you in the previous section. In addition to that, the interface defines a few proprietary methods to support things like set operations and additional database functions.
The main difference you will recognize in your code is how you instantiate a HibernateCriteriaBuilder. The best way to instantiate it is by calling the getCriteriaBuilder method on Hibernate’s Session interface.
HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.like(game.get("playerWhite"), "%anssen"));
You can also cast a CriteriaBuilder interface to HibernateCriteriaBuilder. The cast is, of course, not type-safe, and it relies on the implementation detail that Hibernate uses the same class to implement both interfaces. I, therefore, recommend you get a Session and call the getCriteriaBuilder method.
HibernateCriteriaBuilder cBuilder = (HibernateCriteriaBuilder) em.getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.like(game.get("playerWhite"), "%anssen"));
Features added by HibernateCriteriaBuilder
As you can see in the official Javadoc of the HibernateCriteriaBuilder interface, the interface defines many methods to build different parts of your query. Some of them are defined by JPA’s CriteriaBuilder; others are Hibernate-specific features. Here are some of the most interesting additions defined by the HibernateCriteriaBuilder interface.
Insert Into Select statements
INSERT INTO SELECT statements are a well-known SQL feature that enables you to insert data selected by a query as new records into a database table. Since version 6, Hibernate supports this for HQL statements, and Hibernate 6.1 will provide this feature as an extension to the Criteria API.
Additional Expressions
The HibernateCriteriaBuilder defines several methods to create Expressions that you can use to perform calculations, transform or extract information and get the current date or time. Here are a few examples:
- JpaExpression<Integer> sign(Expression<? extends Number> x)
Returns 1 if the provided argument is positive, -1 if it’s negative, and 0 if it’s exactly 0. - JpaExpression ceiling(Expression x)
Returns the smallest integer greater or equal to the provided argument. - JpaExpression floor(Expression x)
Returns the smallest largest integer smaller or equal to the provided argument. - JpaExpression round(Expression x, Integer n)
Returns the 1st argument rounded to the number of decimal digits provided as the 2nd argument. - JpaExpression exp(Expression x) and JpaExpression power(Expression x, Expression y)
Returns Euler’s number e raised to the power of the provided argument or returns the 1st argument raised to the power of the 2nd argument. - JpaExpression<Double> ln(Expression<? extends Number> x)
Returns the natural logarithm of the provided argument. - JpaExpression<java.time.LocalDate> localDate(), JpaExpression localDateTime() and JpaExpression localTime()
Returns the current date, date and time, or time of your database server.
Similar to the methods defined by JPA’s CriteriaBuilder interface that define expressions, you can use those methods to define your query’s projection or WHERE clause.
HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessGame> q = cBuilder.createQuery(ChessGame.class);
Root<ChessGame> game = q.from(ChessGame.class);
q.select(game);
q.where(cBuilder.equal(game.get("playedOn"), cBuilder.localDate()));
List<ChessGame> games = em.createQuery(q).getResultList();
Hibernate then includes these expressions in the generated SQL statement. Your database processes them and returns the result. This is important if you’re processing the returned values and rely on timezones or other localizations. In those situations, you need to ensure that your Java application and database use the same settings.
11:58:59,183 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.playedOn,c1_0.playerBlack_id,c1_0.playerWhite_id,c1_0.version from ChessGame c1_0 where c1_0.playedOn=current_date
Additional Predicates
Hibernate also provides a few additional predicates, which you can use to define your WHERE clause. The most interesting ones are the different versions of ilike and notilike methods, which provide an easy way to define a case-insensitive LIKE or NOT LIKE expression.
HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessPlayer> q = cBuilder.createQuery(ChessPlayer.class);
Root<ChessPlayer> player = q.from(ChessPlayer.class);
q.select(player);
q.where(cBuilder.ilike(player.get("firstName"), "%ikar%"));
List<ChessPlayer> games = em.createQuery(q).getResultList();
games.forEach(p -> log.info(p));
16:32:13,147 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 where c1_0.firstName ilike ? escape ''
16:32:13,148 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [VARCHAR] - [%ikar%]
16:32:13,168 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Hikaru, lastName=Nakamura]
And if you modeled an association as a java.util.Map, you can use the methods isMapEmpty, isMapNotEmpty, and mapSize to check if or how many elements that Map contains.
Ordering
JPA’s CriteriaBuilder enables you to fetch the result set in the ascending or descending order of one or more entity attributes. In addition, the HibernateCriteriaBuilder also enables you to define the handling of null values and order by the result of an Expression, e.g., the result of a database function.
In addition to the asc and desc methods defined by JPA’s CriteriaBuilder, the HibernateCriteriaBuilder defines a 2nd version of each method that accepts a boolean as the 2nd method parameter. This boolean defines if null values shall be returned first.
HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessPlayer> q = cBuilder.createQuery(ChessPlayer.class);
Root<ChessPlayer> player = q.from(ChessPlayer.class);
q.select(player);
q.orderBy(cBuilder.asc(player.get("firstName"), true));
List<ChessPlayer> games = em.createQuery(q).getResultList();
games.forEach(p -> log.info(p));
17:24:56,003 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 order by c1_0.firstName asc nulls first
17:24:56,017 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=2, firstName=Fabiano, lastName=Caruana]
17:24:56,017 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Hikaru, lastName=Nakamura]
17:24:56,017 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=1, firstName=Magnus, lastName=Carlsen]
17:24:56,017 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=3, firstName=Richard, lastName=Rapport]
If you want to define a more complex ORDER BY clause based on an Expression, you need to call one of the sort methods. They enable you to provide the Expression by which you want to sort the result, if you want to get the result in ascending or descending order and how you want to handle null values.
I use that in the following code snippet to get the query result in the ascending order of the length of the player’s first names. In this example, it doesn’t make any sense to define the handling of null values. But if you’re ordering your query result by a different Expression, you could provide a 3rd method parameter to define the handling of null values.
HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<ChessPlayer> q = cBuilder.createQuery(ChessPlayer.class);
Root<ChessPlayer> player = q.from(ChessPlayer.class);
q.select(player);
q.orderBy(cBuilder.sort(cBuilder.length(player.get("firstName")), SortOrder.ASCENDING));
List<ChessPlayer> games = em.createQuery(q).getResultList();
games.forEach(p -> log.info(p));
08:15:10,477 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 order by character_length(c1_0.firstName) asc
08:15:10,493 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=1, firstName=Magnus, lastName=Carlsen]
08:15:10,493 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Hikaru, lastName=Nakamura]
08:15:10,493 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=2, firstName=Fabiano, lastName=Caruana]
08:15:10,493 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=3, firstName=Richard, lastName=Rapport]
Set operations
Hibernate 6 also introduced support for set operations for HQL and Criteria queries. Using the HibernateCriteriaBuilder, you can now combine the result sets of 2 query statements using the methods union, unionAll, intersect, intersectAll, except, and exceptAll.
Here you can see an example that selects the firstName and lastName of all ChessPlayer in the 1st and the firstName and lastName of all ChessStreamer in the 2nd query and creates a union of both result sets. When using set operations, please keep in mind that all result sets need to follow the same structure.
HibernateCriteriaBuilder cBuilder = em.unwrap(Session.class).getCriteriaBuilder();
CriteriaQuery<Tuple> qPlayer = cBuilder.createTupleQuery();
Root<ChessPlayer> player = qPlayer.from(ChessPlayer.class);
qPlayer.multiselect(player.get("firstName").alias("firstName"), player.get("lastName").alias("lastName"));
CriteriaQuery<Tuple> qStreamer = cBuilder.createTupleQuery();
Root<ChessStreamer> streamer = qStreamer.from(ChessStreamer.class);
qStreamer.multiselect(streamer.get("firstName").alias("firstName"), streamer.get("lastName").alias("lastName"));
CriteriaQuery<Tuple> qPlayerAndStreamer = cBuilder.union(qPlayer, qStreamer);
List<Tuple> persons = em.createQuery(qPlayerAndStreamer).getResultList();
persons.forEach(t -> log.info(t.get("firstName") + ", " + t.get("lastName")));
As you can see in the log output, Hibernate generated an SQL statement that tells the database to apply the set operation union on the 2 result sets that contain the first and last names of all ChessPlayer and ChessStreamer.
17:43:05,857 DEBUG [org.hibernate.SQL] - select c1_0.firstName,c1_0.lastName from ChessPlayer c1_0 union select c2_0.firstName,c2_0.lastName from ChessStreamer c2_0
17:43:05,865 INFO [com.thorben.janssen.sample.TestSample] - Hikaru, Nakamura
17:43:05,865 INFO [com.thorben.janssen.sample.TestSample] - Fabiano, Caruana
17:43:05,865 INFO [com.thorben.janssen.sample.TestSample] - Magnus, Carlsen
17:43:05,865 INFO [com.thorben.janssen.sample.TestSample] - Richard, Rapport
17:43:05,865 INFO [com.thorben.janssen.sample.TestSample] - Levy, Rozman
17:43:05,865 INFO [com.thorben.janssen.sample.TestSample] - Ben, Finegold
Conclusion
As you saw in this article, Hibernate’s HibernateCriteriaBuilder interface extends JPA’s CriteriaBuilder interface and adds methods for Hibernate’s proprietary query features. These are:
- Additional Expressions, like round and exp, that you can use to perform calculations, transform or extract information and get the current date or time.
- Additional Predicates, like the ilike Predicate, that you can use to define your WHERE clauses.
- Methods to define more complex ORDER BY clauses, e.g. based on the result of an SQL function.
- Set operations to combine the result of multiple queries.
By adding all these proprietary features, the HibernateCriteriaBuilder interface provides you with the same query features as Hibernate’s HQL query language, which extends JPA’s JPQL language. That enables you to easily switch between the 2 approaches and use the query definition you feel most comfortable with.