Hibernate’s Read-Only Query Hint For Faster Read Operations
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.
By default, Hibernate loads all entity objects in read-write mode. It performs dirty checks to detect changes it needs to persist in your database for each of them. That makes entities very easy to use, but it also creates an overhead if you don’t want to change anything. You can avoid this by setting Hibernate’s read-only hint on your query.
By declaring a query as read-only, you enable Hibernate to perform a few internal optimizations. If Hibernate knows that you don’t change the fetched entity objects, it doesn’t have to perform any dirty checks on them. And if it doesn’t do that, it also doesn’t need to keep dehydrated copies of the fetched objects to detect these changes. This reduces the memory footprint of your session and the effort of all flush operations.
Setting the Read-Only Query Hint
As I explained in my previous article about query hints, you can set them in similar ways on all types of queries. The easiest option is to call the setHint method on the Query and TypedQuery interface. This method expects the name of the hint as a String and its value as an Object.
ChessPlayer chessPlayer = em.createQuery("SELECT p FROM ChessPlayer p WHERE p.firstName = :firstName", ChessPlayer.class) .setParameter("firstName", "Paul") .setHint(QueryHints.READ_ONLY, true) // .setHint("org.hibernate.readOnly", true) .getSingleResult();
The name of the read-only hint is org.hibernate.readyOnly. As you can see in the code snippet, you can provide it as a String or use the READ_ONLY constant of Hibernate’s QueryHints class. The supported values of this hint are true and false (default value).
If you’re using a named query, you can also set the hint as part of the query definition. Hibernate will then automatically apply it to your query when you instantiate it.
@NamedQuery(name = "findByFirstName", query = "SELECT p FROM ChessPlayer p WHERE p.firstName = :firstName", hints = @QueryHint(name = QueryHints.READ_ONLY, value = "true")) public class ChessPlayer { ... }
And if you’re using the find method on the EntityManager interface, you can provide a Map<String, Object> with your hints as the 3rd parameter.
Map<String, Object> hints = new HashMap<>(); hints.put(QueryHints.READ_ONLY, true); ChessPlayer chessPlayer = em.find(ChessPlayer.class, 1L, hints);
In all 3 cases, the result is the same. The query hint doesn’t affect the creation and execution of the query. You also don’t see any differences in your application’s log or the database. Hibernate only excludes the fetched entity objects from all dirty checks and doesn’t store any internal copies of them.
Don’t Change Read-Only Objects
You’re probably not surprised if I tell you that you should never change an entity object that you fetched in read-only mode.
As explained earlier, Hibernate excludes these objects from all dirty checks to reduce the memory footprint and speed up flush operations. Due to this optimization, Hibernate will not detect that you changed one of the read-only objects. It will not trigger any lifecycle state change or SQL UPDATE statement for it.
Unfortunately, Hibernate also doesn’t prevent you from changing a read-only object. So, it’s up to you to ensure that the result of a read-only query is never used in a context that tries to change the returned objects.
You can see all of that when we execute the following test case. I first execute a query with a read-only hint to get a ChessPlayer object. In the next step, I change the firstname of the player and commit the transaction.
EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); ChessPlayer chessPlayer = em.createQuery("select p from ChessPlayer p " + "where p.firstName = :firstName ", ChessPlayer.class) .setParameter("firstName", "Paul") .setHint(QueryHints.READ_ONLY, true) // .setHint("org.hibernate.readOnly", true) .getSingleResult(); chessPlayer.setFirstName("changed first name"); em.getTransaction().commit(); em.close();
Without the read-only hint, Hibernate would detect the change during a flush operation before committing the transaction. But because the read-only hint excluded the ChessPlayer object from all dirty checks, Hibernate doesn’t detect the change and doesn’t perform an SQL UPDATE statement.
16:54:52,932 DEBUG SQL:144 - select chessplaye0_.id as id1_1_, chessplaye0_.birthDate as birthdat2_1_, chessplaye0_.firstName as firstnam3_1_, chessplaye0_.lastName as lastname4_1_, chessplaye0_.version as version5_1_ from ChessPlayer chessplaye0_ where chessplaye0_.firstName=? 16:54:52,950 DEBUG StatisticsImpl:729 - HHH000117: HQL: select p from ChessPlayer p where p.firstName = :firstName , time: 19ms, rows: 1 16:54:53,000 INFO StatisticalLoggingSessionEventListener:258 - Session Metrics { 23800 nanoseconds spent acquiring 1 JDBC connections; 19500 nanoseconds spent releasing 1 JDBC connections; 78200 nanoseconds spent preparing 1 JDBC statements; 2558700 nanoseconds spent executing 1 JDBC statements; 0 nanoseconds spent executing 0 JDBC batches; 0 nanoseconds spent performing 0 L2C puts; 0 nanoseconds spent performing 0 L2C hits; 0 nanoseconds spent performing 0 L2C misses; 9649700 nanoseconds spent executing 1 flushes (flushing a total of 1 entities and 2 collections); 23400 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections) }
Limitations Before Hibernate 5.4.11
If you’re using a Hibernate version older than 5.4.11, you should be aware of bug HHH-11958. In these older versions, the read-only query hint didn’t have any effect if you set it for the EntityManager’s find method. Hibernate then still included the entity object in dirty checks and kept a dehydrated copy of the fetched object.
Since Hibernate 5.4.11, this bug is fixed, and the read-only optimization also works when you’re using the EntityManager‘s find method.
Conclusion
The read-only query hint is a small and straightforward performance tuning feature. It enables you to tell Hibernate which entity objects will not get changed by your business code. Hibernate can then exclude them from its dirty checks to improve the performance of flush operations and reduce the memory footprint of your current session.