Featured Image with Sidebar

Hibernate Query Spaces – Optimizing Flush and Cache Operations

By Thorben Janssen


Hibernate needs to ensure that all tables referenced in a query are up to date before executing the query. That means it has to flush all pending changes for these tables before it runs the query. Otherwise, the database would process the query on an outdated data set and return wrong results.

Hibernate not only handles this step automatically but also tries to do that as efficiently as possible. Based on the query statement, Hibernate tries to detect which tables the query references. This is called the “query space”. Based on the query space, Hibernate decides if it needs to flush any changes to the database.

The query space also helps Hibernate identifying the parts of the 2nd level cache it needs to invalidate when using a modifying query to implement a bulk operation. Hibernate doesn’t know which database records and entity objects are affected by it when you execute such a query. It has to invalidate all objects of an entity class referenced by the query space, e.g., all cached ChessPlayer entities. If the query space is empty, Hibernate needs to invalidate the entire 2nd level cache.

Detecting Query Spaces Automatically

Hibernate generates the executed SQL statement for JPQL and Criteria statements and if you define the query via one of Hibernate’s or JPA’s APIs, e.g., by calling the EntityManager.find method. In these situations, Hibernate detects the query space automatically and determines if it has to flush any changes.

In the following example, we find and update a ChessPlayer entity. After that, we execute a JPQL query that only references the ChessTournament entity.

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

log.info("JPQL Query");
em.createQuery("SELECT t FROM ChessTournament t WHERE t.name LIKE '%Zurich%'", ChessTournament.class)
		.getResultList();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();

The query space of my JPQL query only includes the ChessTournament but not the ChessPlayer table. Due to that, Hibernate doesn’t flush the ChessPlayer entity to the database until you commit the transaction.

15:21:38,973 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
15:21:39,014  INFO TestNativeUpdate:49 - JPQL Query
15:21:39,110 DEBUG SQL:144 - select chesstourn0_.id as id1_2_, chesstourn0_.endDate as enddate2_2_, chesstourn0_.name as name3_2_, chesstourn0_.startDate as startdat4_2_, chesstourn0_.version as version5_2_ from ChessTournament chesstourn0_ where chesstourn0_.name like '%Zurich%'
15:21:39,115  INFO TestNativeUpdate:53 - Flush and Commit
15:21:39,123 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?

That changes if you also persist a new ChessTournament entity. 

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

ChessTournament tournament = new ChessTournament();
tournament.setName("My Tournament");
em.persist(tournament);

log.info("JPQL Query");
em.createQuery("SELECT t FROM ChessTournament t WHERE t.name LIKE '%Zurich%'", ChessTournament.class)
		.getResultList();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();

Hibernate now finds a pending change and decides to flush the persistence context.

15:22:55,945 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
15:22:55,978 DEBUG SQL:144 - select nextval ('tournament_seq')
15:22:55,982 DEBUG SQL:144 - select nextval ('tournament_seq')
15:22:55,988  INFO TestNativeUpdate:49 - JPQL Query
15:22:56,073 DEBUG SQL:144 - insert into ChessTournament (endDate, name, startDate, version, id) values (?, ?, ?, ?, ?)
15:22:56,082 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?
15:22:56,086 DEBUG SQL:144 - select chesstourn0_.id as id1_2_, chesstourn0_.endDate as enddate2_2_, chesstourn0_.name as name3_2_, chesstourn0_.startDate as startdat4_2_, chesstourn0_.version as version5_2_ from ChessTournament chesstourn0_ where chesstourn0_.name like '%Zurich%'
15:22:56,092  INFO TestNativeUpdate:53 - Flush and Commit

Query Spaces in Native Queries

One of the main advantages of a native query is that Hibernate doesn’t parse it. That enables you to use all features supported by your database and implement very complex queries.

But as so often, this also provides some drawbacks. One of them is that Hibernate doesn’t know which tables the query references. The query space is empty, and Hibernate can’t check if it needs to flush a dirty entity. Due to that, it always has to flush all dirty entities.

Let’s replace the JPQL query from the first example with a native SQL query that selects the information.

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

log.info("Native Query");
em.createNativeQuery("SELECT * FROM ChessTournament t WHERE t.name LIKE '%Zurich%'", ChessTournament.class)
		.getResultList();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();
15:23:58,129 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
15:23:58,157  INFO TestNativeUpdate:74 - Native Query
15:23:58,190 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?
15:23:58,206 DEBUG SQL:144 - SELECT * FROM ChessTournament t WHERE t.name LIKE '%Zurich%'
15:23:58,212  INFO TestNativeUpdate:78 - Flush and Commit

Hibernate now flushed the dirty ChessPlayer entity before it executed the native query even though it wasn’t necessary. To get the same optimized behavior as provided for a JPQL statement, you need to tell Hibernate which query spaces your native query references. You can do that via Hibernate’s SynchronizeableQuery interface or since Hibernate 5.3.20 and 5.4.24 by setting the query hint org.hibernate.query.native.spaces.

Setting the Query Space via API

The management of query spaces is a proprietary Hibernate feature, and JPA’s Query interface doesn’t support it. You need to unwrap it to get Hibernate’s SynchronizeableQuery interface. It offers multiple methods to specify the entities or database tables referenced in the query.

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

log.info("Native Query");
Query q = em.createNativeQuery("SELECT * FROM ChessTournament t WHERE t.name LIKE '%Zurich%'",
		ChessTournament.class);
SynchronizeableQuery hq = q.unwrap(SynchronizeableQuery.class);
hq.addSynchronizedEntityClass(ChessTournament.class);
q.getResultList();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();

Based on the provided query spaces, Hibernate can then check if it needs to flush any changes to the database. In this example, the query and the changed entity are independent of each other. Hibernate decides to delay the SQL UPDATE statement until the end of the transaction.

15:42:00,553 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
15:42:00,583  INFO TestNativeUpdate:99 - Native Query
15:42:00,617 DEBUG SQL:144 - SELECT * FROM ChessTournament t WHERE t.name LIKE '%Zurich%'
15:42:00,623  INFO TestNativeUpdate:106 - Flush and Commit
15:42:00,630 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?

Setting the Query Space as a Hint

Since Hibernate 5.3.20 and 5.4.24, you can also provide the query space as a query hint. The main benefit of this approach is that you can use it with JPA’s Query interface. You no longer need to cast it to one of Hibernate’s proprietary interfaces.

You can set a query hint by calling the setHint method on the Query interface with the name of the hint and its value. Most names are long and hard to remember. I recommend using the constants in Hibernate’s QueryHints class to make your code easier to read and write.

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

log.info("Native Query");
Query q = em.createNativeQuery("SELECT * FROM ChessTournament t WHERE t.name LIKE '%Zurich%'",
		ChessTournament.class);
q.setHint(QueryHints.NATIVE_SPACES, ChessTournament.class.getName());
q.getResultList();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();

Like in the previous example, Hibernate now knows the query space and decides to postpone executing the SQL UPDATE statement.

16:02:23,193 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
16:02:23,223  INFO TestNativeUpdate:123 - Native Query
16:02:23,275 DEBUG SQL:144 - SELECT * FROM ChessTournament t WHERE t.name LIKE '%Zurich%'
16:02:23,282  INFO TestNativeUpdate:129 - Flush and Commit
16:02:23,294 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?

Query Spaces for Native Modifying Queries

You can also use the 2 previously discussed options to set the query space for a native, modifying query. Hibernate will then use the query space to determine if it needs to flush any entities and limit the invalidation of the 2nd level cache to the affected areas.

If you don’t provide any additional information to Hibernate, the query space is empty. Hibernate then flushes all dirty entities and invalidates the entire 2nd level cache before executing the SQL UPDATE statement.

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

log.info("Native Query");
Query q = em.createNativeQuery("UPDATE ChessTournament SET name = 'changed' WHERE id = 1",
		ChessTournament.class);
q.executeUpdate();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();
17:01:23,171 DEBUG AbstractReadWriteAccess:66 - Getting cached data from region [`org.thoughts.on.java.model.ChessPlayer` (AccessType[read-write])] by key [org.thoughts.on.java.model.ChessPlayer#1]
17:01:23,171 DEBUG AbstractReadWriteAccess:72 - Cache miss : region = `org.thoughts.on.java.model.ChessPlayer`, key = `org.thoughts.on.java.model.ChessPlayer#1`
17:01:23,171 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
17:01:23,174 DEBUG AbstractReadWriteAccess:98 - Caching data from load [region=`org.thoughts.on.java.model.ChessPlayer` (AccessType[read-write])] : key[org.thoughts.on.java.model.ChessPlayer#1] -> value[CacheEntry(org.thoughts.on.java.model.ChessPlayer)]
17:01:23,175  INFO TestNativeUpdate:155 - Native Query
17:01:23,194 DEBUG AbstractReadWriteAccess:145 - Locking cache item [region=`org.thoughts.on.java.model.ChessPlayer` (AccessType[read-write])] : `org.thoughts.on.java.model.ChessPlayer#1` (timeout=6606029140762624, version=0)
17:01:23,200 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?
17:01:23,212 DEBUG SQL:144 - UPDATE ChessTournament SET name = 'changed' WHERE id = 1
17:01:23,214  INFO TestNativeUpdate:161 - Flush and Commit

As explained before, you can provide the query space as a query hint or by setting it via Hibernate’s API. Hibernate then flushes the ChessPlayer entity and updates the 2nd level cache when the transaction gets committed.

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

ChessPlayer player = em.find(ChessPlayer.class, 1L);
player.setFirstName("Magnus");
player.setLastName("Carlsen");

log.info("Native Query");
Query q = em.createNativeQuery("UPDATE ChessTournament SET name = 'changed' WHERE id = 1",
		ChessTournament.class);
q.setHint(QueryHints.NATIVE_SPACES, ChessTournament.class.getName());
q.executeUpdate();

log.info("Flush and Commit");
em.getTransaction().commit();
em.close();
17:02:57,663 DEBUG AbstractReadWriteAccess:66 - Getting cached data from region [`org.thoughts.on.java.model.ChessPlayer` (AccessType[read-write])] by key [org.thoughts.on.java.model.ChessPlayer#1]
17:02:57,664 DEBUG AbstractReadWriteAccess:72 - Cache miss : region = `org.thoughts.on.java.model.ChessPlayer`, key = `org.thoughts.on.java.model.ChessPlayer#1`
17:02:57,664 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_ from ChessPlayer chessplaye0_ where chessplaye0_.id=?
17:02:57,666 DEBUG AbstractReadWriteAccess:98 - Caching data from load [region=`org.thoughts.on.java.model.ChessPlayer` (AccessType[read-write])] : key[org.thoughts.on.java.model.ChessPlayer#1] -> value[CacheEntry(org.thoughts.on.java.model.ChessPlayer)]
17:02:57,667  INFO TestNativeUpdate:155 - Native Query
17:02:57,693 DEBUG SQL:144 - UPDATE ChessTournament SET name = 'changed' WHERE id = 1
17:02:57,695  INFO TestNativeUpdate:161 - Flush and Commit
17:02:57,696 DEBUG AbstractReadWriteAccess:145 - Locking cache item [region=`org.thoughts.on.java.model.ChessPlayer` (AccessType[read-write])] : `org.thoughts.on.java.model.ChessPlayer#1` (timeout=6606029527842816, version=0)
17:02:57,702 DEBUG SQL:144 - update ChessPlayer set birthDate=?, firstName=?, lastName=?, version=? where id=? and version=?

Conclusion

Flushing the persistence context and invalidating the 2nd level cache can have a huge performance impact. That’s why Hibernate tries only to perform them when it’s absolutely necessary. To do that, it checks if the persistence context contains any pending changes that affect the query space. If that’s the case, Hibernate flushes the pending changes.

Hibernate automatically detects the query space if it generates the SQL statement itself. If you’re executing a native SQL statement, you should set the query space. You can provide it as a query hint or by calling one of the methods on Hibernate’s SynchronizeableQuery interface.

About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Tools

Books and Courses

Coaching and Consulting

Leave a Reply

Your email address will not be published. Required fields are marked

This site uses Akismet to reduce spam. Learn how your comment data is processed.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}