Hibernate Tip: How to control cache invalidation for native queries


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.


Hibernate Tips is a series of posts in which I describe a quick and easy solution for common Hibernate questions. If you have a question for a future Hibernate Tip, please post a comment below.

Question:

“I was told that native queries remove all entities from my 2nd level cache. But you’re still recommending them. Don’t they negatively affect the performance?”

Solution:

Yes, some native queries invalidate the 2nd level cache. But no, if you do it correctly, it doesn’t have any negative performance impacts, and it doesn’t change my recommendation to use them.

To answer this question in more detail, we first need to discuss which kinds of native queries invalidate the 2nd level cache before we talk about finetuning this process.

Which native queries invalidate the cache?

Native SQL SELECT statements don’t affect the 2nd level cache, and you don’t need to worry about any negative performance impacts. But Hibernate invalidates the 2nd level cache if you execute an SQL UPDATE or DELETE statement as a native query. This is necessary because the SQL statement changed data in the database, and by that, it might have invalidated entities in the cache. By default, Hibernate doesn’t know which records were affected. Due to this, Hibernate can only invalidate the entire 2nd level cache.

Let’s take a look at an example.

Before I execute the following test, the Author entity with id 1 is already in the 2nd level cache. Then I run an SQL UPDATE statement as a native query in one transaction. In the following transaction, I check if the Author entity is still in the cache.

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

log.info("Before native update");
log.info("Author 1 in Cache? " + em.getEntityManagerFactory().getCache().contains(Author.class, 1L));

Query q = em.createNativeQuery("UPDATE Book SET title = title || ' - changed'");
q.executeUpdate();

em.getTransaction().commit();
em.close();



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

log.info("After native update");
log.info("Author 1 in Cache? " + em.getEntityManagerFactory().getCache().contains(Author.class, 1L));

a = em.find(Author.class, 1L);
log.info(a);

em.getTransaction().commit();
em.close();

If you don’t provide additional information, Hibernate invalidates the 2nd level cache and removes all entities from it. You can see that in the log messages written by the 2nd transaction. The Author entity with id 1 is no longer in the cache, and Hibernate has to use a query to get it from the database.

06:32:02,752 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Before native update
06:32:02,752 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Author 1 in Cache? true
06:32:02,779 DEBUG [org.hibernate.SQL] - UPDATE Book SET title = title || ' - changed'
06:32:02,782 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    14800 nanoseconds spent acquiring 1 JDBC connections;
    22300 nanoseconds spent releasing 1 JDBC connections;
    201400 nanoseconds spent preparing 1 JDBC statements;
    1356000 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;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    17500 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}
06:32:02,782 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - After native update
06:32:02,782 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Author 1 in Cache? false
06:32:02,783 DEBUG [org.hibernate.SQL] - select author0_.id as id1_0_0_, author0_.firstName as firstNam2_0_0_, author0_.lastName as lastName3_0_0_, author0_.version as version4_0_0_ from Author author0_ where author0_.id=?
06:32:02,784 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Author firstName: Joshua, lastName: Bloch
06:32:02,785 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    11900 nanoseconds spent acquiring 1 JDBC connections;
    15300 nanoseconds spent releasing 1 JDBC connections;
    18500 nanoseconds spent preparing 1 JDBC statements;
    936400 nanoseconds spent executing 1 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    256700 nanoseconds spent performing 1 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    114600 nanoseconds spent performing 1 L2C misses;
    107100 nanoseconds spent executing 1 flushes (flushing a total of 1 entities and 0 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

Only invalidate affected regions

But that doesn’t have to be the case. You can tell Hibernate, which entity classes are affected by the query. You just need to unwrap the Query object to get a Hibernate-specific SqlQuery, and call the addSynchronizedEntityClass method with a class reference.

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

log.info("Before native update");
log.info("Author 1 in Cache? " + em.getEntityManagerFactory().getCache().contains(Author.class, 1L));

Query q = em.createNativeQuery("UPDATE Book SET title = title || ' - changed'");
q.unwrap(NativeQuery.class).addSynchronizedEntityClass(Book.class);
q.executeUpdate();

em.getTransaction().commit();
em.close();



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

log.info("After native update");
log.info("Author 1 in Cache? " + em.getEntityManagerFactory().getCache().contains(Author.class, 1L));

a = em.find(Author.class, 1L);
log.info(a);

em.getTransaction().commit();
em.close();

My SQL UPDATE statement changes records in the Book table which gets mapped by the Book entity. After providing this information to Hibernate, it online invalidates the region of the Book entity and the Author entities stay in the 2nd level cache.

06:30:51,985 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Before native update
06:30:51,985 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Author 1 in Cache? true
06:30:52,011 DEBUG [org.hibernate.SQL] - UPDATE Book SET title = title || ' - changed'
06:30:52,014 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    18400 nanoseconds spent acquiring 1 JDBC connections;
    19900 nanoseconds spent releasing 1 JDBC connections;
    86000 nanoseconds spent preparing 1 JDBC statements;
    1825400 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;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    19400 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}
06:30:52,015 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - After native update
06:30:52,015 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Author 1 in Cache? true
06:30:52,015 INFO  [org.thoughts.on.java.model.Test2ndLevelCache] - Author firstName: Joshua, lastName: Bloch
06:30:52,016 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    10000 nanoseconds spent acquiring 1 JDBC connections;
    25700 nanoseconds spent releasing 1 JDBC connections;
    0 nanoseconds spent preparing 0 JDBC statements;
    0 nanoseconds spent executing 0 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    86900 nanoseconds spent performing 1 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    104700 nanoseconds spent executing 1 flushes (flushing a total of 1 entities and 0 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

Learn more:

If you want to learn more about native queries or Hibernate’s caches, you will find the following articles interesting:

Hibernate Tips Book

Get more recipes like this one in my new book Hibernate Tips: More than 70 solutions to common Hibernate problems.

It gives you more than 70 ready-to-use recipes for topics like basic and advanced mappings, logging, Java 8 support, caching, and statically and dynamically defined queries.

Get it now!

One Comment

  1. Such a shame this can’t be done with EclipseLink. **sad face**

Comments are closed.