One of the smaller changes in Hibernate 6 that can easily get overlooked, which Steve Ebersole presented in a recent Expert Session in the Persistence Hub, is the introduction of the MutationQuery and SelectionQuery interfaces. It allows the separation between queries that change data and the ones that select it from the database.

Older Hibernate versions and the JPA specification use the Query interface to handle both types of queries. It extends the SelectionQuery and MutationQuery interfaces. And you can, of course, still use that interface with Hibernate 6. But the 2 new interfaces are much cleaner because each only defines the methods you can use with the corresponding type of query. And as I will show you in this article, it also enables a stricter validation of the provided statement.

Unwrapping a Hibernate Session

Before we take a closer look at the 2 new interfaces, I want to quickly show you how to get a Hibernate Session if you’re using Hibernate as your JPA implementation. In that case, you often inject an EntityManager instance and not Hibernate’s proprietary Session. You can get the underlying Session by calling the unwrap method.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Session s = em.unwrap(Session.class);

SelectionQuery<R> – Querying data from the database

Getting Hibernate’s SelectionQuery interface is quite simple. After you get a Session instance, you can call the createSelectionQuery method with a JPQL query String or a CriteriaQuery object.

SelectionQuery<Book> q = s.createSelectionQuery("SELECT b FROM Book b WHERE b.title = :title", Book.class);
q.setParameter("title", "Hibernate Tips - More than 70 solutions to common Hibernate problems");
List<Book> books = q.getResultList();

You can also call the createNamedSelectionQuery method with the name of a @NamedQuery.

SelectionQuery<Book> q = s.createNamedSelectionQuery("Book.selectByTitle", Book.class);
q.setParameter("title", "Hibernate Tips - More than 70 solutions to common Hibernate problems");
List<Book> books = q.getResultList();

Improved statement validation

One of the advantages of the SelectionQuery interface is the improved validation. When Hibernate instantiates the SelectionQuery, it immediately validates the query statement.

Session s = em.unwrap(Session.class);
SelectionQuery<Book> q = s.createSelectionQuery("UPDATE Book SET title = upper(title)", Book.class);
List<Book> books = q.getResultList();

If you provided a modifying instead of a select statement, Hibernate throws an IllegalSelectQueryException.

10:27:35,288 INFO  [com.thorben.janssen.model.TestQueryInterfaces] - Hibernate threw the expected IllegalSelectQueryException.
10:27:35,289 INFO  [com.thorben.janssen.model.TestQueryInterfaces] - org.hibernate.query.IllegalSelectQueryException: Expecting a selection query, but found `UPDATE Book SET title = upper(title)` [UPDATE Book SET title = upper(title)]

Cleaner API

Another advantage of the SelectionQuery interface is the much cleaner interface. It only provides the methods to configure and execute a query that selects data. But no methods specific to update operations, like the executeUpdate method. You can find all method definitions in the Javadoc. A few examples are:

  • the getResultList and getResultStream methods to get a query result containing multiple records,
  • the getSingleResult method to get a query result that has precisely 1 record,
  • different versions of the setParameter method to set the value of a bind parameter used in your query,
  • the setFirstResult and setMaxResult methods to define pagination,
  • the setHint method to provide a query hint and
  • various methods to configure the cache handling.

MutationQuery – Implementing modifying queries

There are much fewer things you can define for modifying queries, and that’s why the MutationQuery interface benefits the most from the separation. The MutationQuery interface is much cleaner and easier to use than the Query interface by excluding all selection-specific methods. It only defines:

  • the executeUpdate method to execute the modifying query,
  • multiple versions of the setParameter method to provide bind parameter values,
  • 2 methods to define the JPA and the Hibernate-specific FlushMode and
  • a method to set a query timeout, a query comment, and a query hint.

Defining a MutationQuery

You can instantiate a MutationQuery in a similar way as the SelectionQuery. If you want to use a JPQL or Criteria statement, you have to call the createMutationQuery method on Hibernate’s Session interface and provide a String with your JPQL statement, or a CriteriaUpdate or CriteriaDelete object.

Session s = em.unwrap(Session.class);
MutationQuery q = s.createMutationQuery("UPDATE Book SET title = upper(title)");
q.executeUpdate();

If you define a @NamedQuery for your statement, you can instantiate it by calling the createNamedMutationQuery method with the name of your @NamedQuery.

Session s = em.unwrap(Session.class);
MutationQuery q = s.createNamedMutationQuery("Book.updateTitle");
q.executeUpdate();

By calling the createNativeMutationQuery, you can also instantiate a MutationQuery interface using a native SQL statement.

Session s = em.unwrap(Session.class);
MutationQuery q = s.createNativeMutationQuery("UPDATE Book SET title = upper(title)");
q.executeUpdate();

In all 3 cases, Hibernate returns an instance of the same MutationQuery interface, which you can then use to configure and execute your modifying statement.

Improved validation

Like the SelectionQuery interface, Hibernate validates the provided statement when you instantiate a MutationQuery. If your provided statement selects data instead of modifying it, Hibernate throws an IllegalMutationQueryException.

Session s = em.unwrap(Session.class);
try {
	MutationQuery q = s.createMutationQuery("SELECT b FROM Book b");
	fail("Expected an IllegalMutationQueryException");
} catch (IllegalMutationQueryException e) {
	log.info("Hibernate threw the expected IllegalMutationQueryException.");
	log.info(e);
}

As you can see in the log output, the exception message clearly describes the problem.

10:42:47,778 INFO  [com.thorben.janssen.model.TestQueryInterfaces] - Hibernate threw the expected IllegalMutationQueryException.
10:42:47,779 INFO  [com.thorben.janssen.model.TestQueryInterfaces] - org.hibernate.query.IllegalMutationQueryException: Expecting a mutation query, but found `SELECT b FROM Book b`

Conclusion

Hibernate 6 brings a bunch of changes. The SelectionQuery and MutationQuery interfaces are some of the smaller ones. They provide a much cleaner API than the often used Query interface because they focus on a specific type of operation:

  • The SelectionQuery interface defines the methods required to create, configure and execute a query that selects data from the database.
  • The MutationQuery interface does the same for UPDATE and DELETE statements.

Designing an interface for a specific type of operation enabled removing all the unnecessary methods and the design of much cleaner interfaces.