Migrating from Hibernate’s to JPA’s 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, 2 monthly Q&A calls, monthly coding challenges, a community of like-minded developers, and regular expert sessions.
As shown in my previous post, Hibernate offers several APIs to query data from the database. If you want to define your query dynamically at runtime, you can use JPA’s Criteria API. In the past, Hibernate also offered its own proprietary Criteria API. It has been deprecated in Hibernate 5, and you should avoid it when implementing new use cases.
Sooner or later, you will also need to replace Hibernate’ Criteria API in your existing use cases. Let’s talk about the differences compared to JPA’s Criteria API and the required migration steps.
Same name – Different APIs
The 2 APIs serve the same purpose. So, it’s no surprise that they are very similar. The names of the packages, interfaces, and methods obviously differ, but there are a few other differences you need to know.
Advantages of Hibernate’s Deprecated Criteria API
Hibernate’s Criteria query is a little easier to define and execute. As you will see in the migration chapter, it uses a straightforward approach to create the different parts of the query and execute it. JPA’s Criteria API, on the other hand, makes heavy use of the CriteriaBuilder interface to create the different parts of your query. It’s verbosity often makes it a little hard to read.
Hibernate’s API enables you to define parts of your query as native SQL snippets. This provides a flexible approach to use the features of your database, even if Hibernate doesn’t offer direct support for them. Unfortunately, there is no corresponding feature in JPA’s Criteria API.
Another feature that a lot of developers miss during the migration is Hibernate’s Example criterion. It allows you to define your WHERE clause based on an entity object and its attribute values.
Book b = new Book(); b.setFormat(Format.PAPERBACK); b.setTopic("Java"); List results = session.createCriteria(Book.class) .add( Example.create(b) ) .list();
Using JPA’s Criteria API, you need to implement the required WHERE clause yourself.
Advantages of JPA’s Criteria API
JPA’s Criteria API doesn’t support all the features you might have used with Hibernate’s API. But the migration will provide you a few other benefits.
If you use JPA’s Criteria API together with its metamodel, you no longer need to reference entity attributes by their name. You can use the attributes of the generated metamodel classes instead. That enables you to define your query in a typesafe way. It not only makes the definition of your query easier; it also makes a refactoring of your entity classes a lot easier.
JPA’s CriteriaBuilder provides a method to define bind parameters, which you can use to create your WHERE clause and to set the corresponding values before you execute the queries. In contrast to inline parameters, bind parameters avoid SQL injection vulnerabilities and enable your database to cache the execution plan of your query.
Most migrations are relatively simple, and I will show you a few examples in the following sections. We can group the required steps as follows:
- Get a CriteriaBuilder before working on your query.
- Use CriteriaBuilder instead of Hibernate’s Session interface to create your CriteriaQuery.
- Call the from method on your CriteriaQuery to start defining your FROM clause.
- Use the join method of the Root interface to define JOIN clauses instead of createCriteria or createAlias.
- Create Expressions using the CriteriaBuilder to define your WHERE clause instead of calling the add method on the Criteria interface. The resulting code is much more verbose and often harder to read than your previous Hibernate-specific Criteria query.
- Call groupBy, having and orderBy on the CriteriaQuery interface to define your GROUP BY, HAVING, and ORDER BY clauses. This approach is very similar to the generated SQL statement. Using Hibernate’s deprecated Criteria query, you defined these clauses as part of your projection.
Migrating a Basic Query
Let’s start with a basic query that selects all Book entities from the database.
Using Hibernate’s Criteria API, you can define this query in 1 line. You only need to call the createCriteria method on your Session interface with the entity class you want to select. In the next step, you can execute the query by calling the list method.
List books = s.createCriteria(Book.class).list();
JPA’s Criteria API is much more verbose. You first need to get the CriteriaBuilder and call the createQuery method on it to instantiate your CriteriaQuery. In the next step, you need to call the from method on the CriteriaQuery to define the FROM clause. After you’ve done that, you can provide the CriteriaQuery to the createQuery method of your EntityManager and execute the returned TypedQuery.
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Book> cq = cb.createQuery(Book.class); cq.from(Book.class); List<Book> books = em.createQuery(cq).getResultList();
JPA’s more verbose Criteria API might be harder to read, but it provides strong typing. That’s especially the case if you use JPA’s Metamodel, as I do in the following examples.
Migrating JOINS With a WHERE Clause
This query clearly shows how much less code Hibernate’s Criteria API required. You first call the createCriteria method to get a Criteria object. Using this object, you can create a sub-Criteria that represents the JOIN clause. You can then add one or more Restrictions to this sub-Criteria.
Criteria q = s.createCriteria(Author.class); q.createCriteria("books").add(Restrictions.like("title", "%Hibernate%")); List authors = q.list();
From an object-oriented perspective, Hibernate’s deprecated API might be easier to read because you define the filter operations on the Criteria that represents the JOIN clause. But it’s very different from the actual SQL statement that Hibernate has to generate.
JPA’s Criteria API suffers from a similar issue. You can see the structure of the generated SQL statement more clearly in your code. But it’s still different. At the same time, the verbosity of the Criteria API reduces the readability of your code.
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Author> cq = cb.createQuery(Author.class); Root<Author> root = cq.from(Author.class); SetJoin<Author, Book> books = root.join(Author_.books); ParameterExpression<String> paramTitle = cb.parameter(String.class); cq.where(cb.like(books.get(Book_.title), paramTitle)); TypedQuery<Author> q = em.createQuery(cq); q.setParameter(paramTitle, "%Hibernate%"); List<Author> authors = q.getResultList();
This code is similar to the one in the previous migration example. This time, you also need to define the JOIN clause from the Author to the Book table. You can do that using the join method on the Root interface that represents the Author table. By using the metamodel class Author_, you can do that in a typesafe way. If you don’t want to use the metamodel, you can also provide a String that references the attribute by its name.
The description of the WHERE clause consists of the creation of a ParameterExpression of type String and the definition of the WHERE clause itself. As in the previous example, you need to use the CriteriaBuilder to define the LIKE expression with references to the title attribute of the Book and the ParameterExpression.
After you’ve done that, you can instantiate a TypedQuery, set the bind parameter value, and execute it.
Migrating Function Calls With a GROUP BY Clause
Similar to the previous examples, a query that selects the firstName and lastName of an Author and counts her books requires only a few lines of code if you use Hibernate’s deprecated Criteria API. But this code is different from the generated SQL statement, and I don’t find it intuitive to read or write.
You need to create a projectionList containing the 3 information the query shall return. But instead of defining a GROUP BY clause using the firstName and lastName, you reference each of them as a groupProperty in the projection.
Criteria q = s.createCriteria(Author.class); q.setProjection(Projections.projectionList() .add(Projections.groupProperty("firstName")) .add(Projections.groupProperty("lastName")) .add(Projections.count("books"))); List authors = q.list();
JPA’s Criteria stays a little closer to the generated SQL statement. You first join the Author with the Book entity. In the next step, you define the selection by providing references to the firstName and lastName attributes of the Author and describing a call of the count function to get the number of books. After that is done, you need to call the groupBy method on the CriteriaQuery interface to create the GROUP BY clause.
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Tuple> cq = cb.createTupleQuery(); Root<Author> root = cq.from(Author.class); SetJoin<Author, Book> books = root.join(Author_.books); cq.multiselect(root.get(Author_.firstName), root.get(Author_.lastName), cb.count(books.get(Book_.id))); cq.groupBy(root.get(Author_.firstName), root.get(Author_.lastName)); TypedQuery<Tuple> q = em.createQuery(cq); List<Tuple> authors = q.getResultList();
As you have seen, JPA’s and Hibernate’s Criteria APIs are different. But a migration between the 2 APIs isn’t incredibly complicated. You should only expect problems if you’re using Hibernate’s query by example or SQL snippet features.
JPA’s Criteria API is much more verbose than Hibernate’s deprecated API. That makes the required changes appear much bigger than they actually are. Unfortunately, the verbosity also often reduces the readability of the code. But as I have seen time and time again, developers who are new to JPA’s Criteria API quickly understand the general concepts. After using it for a few queries, they often use it with confidence to implement complex queries.