How does Hibernate Query Cache work

(Last Updated On: January 29, 2018)

Introduction

Now that I covered both Entity and Collection caching, it’s time to investigate how Query Caching works.

The Query Cache is strictly related to Entities, and it draws an association between a search criterion and the Entities fulfilling that specific query filter. Like other Hibernate features, the Query Cache is not as trivial as one might think.

Entity model

For our test cases, we are going to use the following domain model:

PostAuthorQueryCache

The Post entity has a many-to-one association to an Author and both entities are stored in the second-level cache.

Enabling query cache

The Query Cache is disabled by default, and to activate it, we need to supply the following Hibernate property:

properties.put("hibernate.cache.use_query_cache", 
    Boolean.TRUE.toString());

For Hibernate to cache a given query result, we need to explicitly set the cachable query attribute when creating the Query.

Read-through caching

The Query Cache is read-through and like the NONSTRICT_READ_WRITE concurrency startegy, it can only invalidate stale entries.

In the next example, we are going to cache the following query:

private List<Post> getLatestPosts(Session session) {
    return (List<Post>) session.createQuery(
        "select p " +
        "from Post p " +
        "order by p.createdOn desc")
    .setMaxResults(10)
    .setCacheable(true)
    .list();
}

First, we are going to investigate the Query Cache internal structure using the following test case:

doInTransaction(session -> {
    LOGGER.info(
        "Evict regions and run query");
    session.getSessionFactory()
        .getCache().evictAllRegions();
    assertEquals(1, getLatestPosts(session).size());
});

doInTransaction(session -> {
    LOGGER.info(
        "Check get entity is cached");
    Post post = (Post) session.get(Post.class, 1L);
});

doInTransaction(session -> {
    LOGGER.info(
        "Check query result is cached");
    assertEquals(1, getLatestPosts(session).size());
});

This test generates the following output:

QueryCacheTest - Evict regions and run query

StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache        
EhcacheGeneralDataRegion - Element for key sql: 
    select
       querycache0_.id as id1_1_,
       querycache0_.author_id as author_i4_1_,
       querycache0_.created_on as created_2_1_,
       querycache0_.name as name3_1_ 
    from
       Post querycache0_ 
    order by
       querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2 
is null
StandardQueryCache - Query results were not found in cache

select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_ 
from
   Post querycache0_ 
order by
   querycache0_.created_on desc limit 10

StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872026465492992
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2
value: [5872026465492992, 1]

JdbcTransaction - committed JDBC Connection

------------------------------------------------------------

QueryCacheTest - Check get entity is cached

JdbcTransaction - committed JDBC Connection

------------------------------------------------------------

QueryCacheTest - Check query is cached

StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
StandardQueryCache - Checking query spaces are up-to-date: [Post]

EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872026465406976, result set timestamp: 5872026465492992
StandardQueryCache - Returning cached query results

JdbcTransaction - committed JDBC Connection
  • All cache regions are evicted, to make sure the Cache is empty
  • Upon running the Post query, the Query Cache checks for previously stored results
  • Because there is no Cache entry, the query goes to the database
  • Both the selected entities and the query result are being cached
  • We then verify that the Post entity was stored in the second-level cache
  • A subsequent query request will be resolved from the Cache, without hitting the database

Query parameters

Query parameters are embedded in the cache entry key as we can see in the following examples.

Basic types

First, we are going to use a basic type filtering:

private List<Post> getLatestPostsByAuthorId(Session session) {
    return (List<Post>) session.createQuery(
        "select p " +
        "from Post p " +
        "join p.author a " +
        "where a.id = :authorId " +
        "order by p.createdOn desc")
    .setParameter("authorId", 1L)
    .setMaxResults(10)
    .setCacheable(true)
    .list();
}
doInTransaction(session -> {
    LOGGER.info("Query cache with basic type parameter");
    List<Post> posts = getLatestPostsByAuthorId(session);
    assertEquals(1, posts.size());
});

The Query Cache entry looks like this:

EhcacheGeneralDataRegion - 
key: 
    sql: 
        select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        inner join
           Author querycache1_ 
              on querycache0_.author_id=querycache1_.id 
        where
           querycache1_.id=? 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {authorId=1}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2 
value: [5871781092679680, 1]

The parameter is stored in the cache entry key. The cache entry value first element is always the result set fetching timestamp. The following elements are the entity identifiers that were returned by this query.

Entity types

We can also use Entity types as query parameters:

private List<Post> getLatestPostsByAuthor(Session session) {
        Author author = (Author) session.get(Author.class, 1L);
    return (List<Post>) session.createQuery(
        "select p " +
        "from Post p " +
        "join p.author a " +
        "where a = :author " +
        "order by p.createdOn desc")
    .setParameter("author", author)
    .setMaxResults(10)
    .setCacheable(true)
    .list();
}
doInTransaction(session -> {
    LOGGER.info("Query cache with entity type parameter");
    List<Post> posts = getLatestPostsByAuthor(session);
    assertEquals(1, posts.size());
});

The cache entry is similar to our previous example since Hibernate only stored the entity identifier in the cache entry key. This makes sense, since Hibernate already caches the Author entity.

EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        inner join
           Author querycache1_ 
              on querycache0_.author_id=querycache1_.id 
        where
           querycache1_.id=? 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {author=1}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2 
value: [5871781092777984, 1]

Consistency

HQL/JPQL Query Invalidation

Hibernate second-level cache favors strong-consistency, and the Query Cache is no different. Like with flushing, the Query Cache can invalidate its entries whenever the associated tablespace changes. Every time we persist/remove/update an Entity, all Query Cache entries using that particular table will get invalidated.

doInTransaction(session -> {
    Author author = (Author) 
        session.get(Author.class, 1L);
    assertEquals(1, getLatestPosts(session).size());

    LOGGER.info("Insert a new Post");
    Post newPost = new Post("Hibernate Book", author);
    session.persist(newPost);
    session.flush();

    LOGGER.info("Query cache is invalidated");
    assertEquals(2, getLatestPosts(session).size());
});

doInTransaction(session -> {
    LOGGER.info("Check Query cache");
    assertEquals(2, getLatestPosts(session).size());
});

This test will add a new Post and then rerun the cacheable query. Running this test gives the following output:

QueryCacheTest - Insert a new Post

insert 
into
   Post
   (id, author_id, created_on, name) 
values
   (default, 1, '2015-06-06 17:29:59.909', 'Hibernate Book')

UpdateTimestampsCache - Pre-invalidating space [Post], timestamp: 5872029941395456
EhcacheGeneralDataRegion - key: Post value: 5872029941395456

QueryCacheTest - Query cache is invalidated
StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2

StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872029941395456, result set timestamp: 5872029695619072
StandardQueryCache - Cached query results were not up-to-date

select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_ 
from
   Post querycache0_ 
order by
   querycache0_.created_on desc limit 10

StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872029695668224
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2 
value: [5872029695668224, 2, 1]

JdbcTransaction - committed JDBC Connection

UpdateTimestampsCache - Invalidating space [Post], timestamp: 5872029695680512
EhcacheGeneralDataRegion - key: Post value: 5872029695680512

------------------------------------------------------------

QueryCacheTest - Check Query cache

StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2

StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872029695680512, result set timestamp: 5872029695668224
StandardQueryCache - Cached query results were not up-to-date

select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_ 
from
   Post querycache0_ 
order by
   querycache0_.created_on desc limit 10

StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872029695705088
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2 
value: [5872029695705088, 2, 1]

JdbcTransaction - committed JDBC Connection
  • Once Hibernate detects an Entity state transition, it pre-invalidates the affected query cache regions
  • The Query Cache entry is not removed, but its associated timestamp is updated
  • The Query Cache always inspects an entry key timestamp, and it skips reading its value if the key timestamp is newer than the result set loading timestamp
  • If the current Session reruns this query, the result will be cached once more
  • The current database transaction commits
  • The actual invalidation takes place and the table space timestamp is updated according to the transaction commit timestamp

Native Query Invalidation

As I previously stated, native queries leave Hibernate in the dark, as it cannot know which tables the native query might modify eventually. In the following test, we are going to update the Author table, while checking the impact it has on the current Post Query Cache:

doInTransaction(session -> {
    assertEquals(1, getLatestPosts(session).size());

    LOGGER.info("Execute native query");
    assertEquals(1, session.createSQLQuery(
        "update Author set name = '\"'||name||'\"' "
    ).executeUpdate());

    LOGGER.info("Check query cache is invalidated");
    assertEquals(1, getLatestPosts(session).size());
});

The test generates the following output:

QueryCacheTest - Execute native query

UpdateTimestampsCache - Pre-invalidating space [Author], timestamp: 5872035446091776
EhcacheGeneralDataRegion - key: Author value: 5872035446091776
UpdateTimestampsCache - Pre-invalidating space [Post], timestamp: 5872035446091776
EhcacheGeneralDataRegion - key: Post value: 5872035446091776

update
   Author 
set
   name = '"'||name||'"'

QueryCacheTest - Check query cache is invalidated

StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
            parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2

StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872035446091776, result set timestamp: 5872035200290816
StandardQueryCache - Cached query results were not up-to-date

select
   querycache0_.id as id1_1_,
   querycache0_.author_id as author_i4_1_,
   querycache0_.created_on as created_2_1_,
   querycache0_.name as name3_1_ 
from
   Post querycache0_ 
order by
   querycache0_.created_on desc limit 10

StandardQueryCache - Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=5872035200364544
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2 
value: [5872035200364544, 1]

JdbcTransaction - committed JDBC Connection

UpdateTimestampsCache - Invalidating space [Post], timestamp: 5872035200372736
EhcacheGeneralDataRegion - key: Post value: 5872035200372736
UpdateTimestampsCache - Invalidating space [Author], timestamp: 5872035200372736
EhcacheGeneralDataRegion - key: Author value: 5872035200372736

Both the Author and the Post cache regions were invalidated, even if just the Author table was modified. To fix this, we need to let Hibernate know what tables we are going to alter.

Native Query Cache Region Synchronization

Hibernate allows us to define the query table space through query synchronization hints. When supplying this info, Hibernate can invalidate the requested cache regions:

doInTransaction(session -> {
    assertEquals(1, getLatestPosts(session).size());

    LOGGER.info("Execute native query with synchronization");
    assertEquals(1, session.createSQLQuery(
            "update Author set name = '\"'||name||'\"' "
    ).addSynchronizedEntityClass(Author.class)
    .executeUpdate());

    LOGGER.info("Check query cache is not invalidated");
    assertEquals(1, getLatestPosts(session).size());
});

The following output is being generated:

QueryCacheTest - Execute native query with synchronization

UpdateTimestampsCache - Pre-invalidating space [Author], timestamp: 5872036893995008
EhcacheGeneralDataRegion - key: Author value: 5872036893995008

update
   Author 
set
   name = '"'||name||'"'

QueryCacheTest - Check query cache is not invalidated

StandardQueryCache - Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
EhcacheGeneralDataRegion - 
key: 
    sql: select
           querycache0_.id as id1_1_,
           querycache0_.author_id as author_i4_1_,
           querycache0_.created_on as created_2_1_,
           querycache0_.name as name3_1_ 
        from
           Post querycache0_ 
        order by
           querycache0_.created_on desc;
    parameters: ; 
    named parameters: {}; 
    max rows: 10; 
    transformer: org.hibernate.transform.CacheableResultTransformer@110f2

StandardQueryCache - Checking query spaces are up-to-date: [Post]
EhcacheGeneralDataRegion - key: Post
UpdateTimestampsCache - [Post] last update timestamp: 5872036648169472, result set timestamp: 5872036648226816
StandardQueryCache - Returning cached query results

JdbcTransaction - committed JDBC Connection

UpdateTimestampsCache - Invalidating space [Author], timestamp: 5872036648263680
EhcacheGeneralDataRegion - key: Author value: 5872036648263680

Only the provided table space was invalidated, leaving the Post Query Cache untouched. Mixing native queries and Query Caching is possible, but it requires a little bit of diligence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

The Query Cache can boost the application performance for frequently executed entity queries, but it’s not a free ride. It’s susceptible to consistency issues and without a proper memory management control mechanism, it can easily grow quite large.

Code available on GitHub.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

16 thoughts on “How does Hibernate Query Cache work

  1. Hey,

    I have a multi-tenant application and am using hibernate filters to achieve the multi-tenancy. As findOne and similar methods do not support filters, I am not using them and hence the L1 cache is not used in the application at all.

    As a workaround, I was trying to use the Query cache only for findById queries (internally it will have a where attached by the hibernate filters) but seems like it requires L2 cache setup as well.

    Is there any way I can use query cache with L1 cache? I needed this behaviour because I intend to do only a session level caching.

    1. The query cache cannot work for the L1 cache since L1 always take precedence over anything in L2 or the DB. Just override the findOne Spring method to pass the tenant identifier. It’s as simple as that.

      1. Thanks for the quick response.

        As far as I understand, the L1 cache will get triggered on session.find by primary key or equivalent. So will I need to override session.find implementation?

        Have I understood anything wrong here?

    2. Vlad, I read the chapter and some more details mentioned in the book about caching. What I understood was that Persistence Context will store the entity, but it will do this when entityManager.find(Post.class, postId) is triggered.

      If I override the Spring’s findOne method and start passing tenant identifier in the query, I will have to do it using entityManager.createQuery or equivalent which in turn will not save things in PersistenceContext (I did a POC on this and every call to the findOne method in the same session led to a db query)

      Can you please help me here.

      Thanks

      1. You can customize the way the Session is built to pass the tenant identifier from a ThreadLocal library. It might be that Spring offers some integration for that. Nevertheless, it’s easy to build it via AOP too.

      2. Have done this using AOP. I enable the filter and pass the tenant identifier to it.

        This works well for all queries. But entityManager.find ignores it (the enabled filter) and does not add the additional where clause which a hibernate filter adds.

        All I intend to do here is to use the L1 cache. Because it triggers only on entityManager.find, I am trying to find a way to pass tenant identifier to it.

      3. That sounds like a bug. Create a replicating test case and open a Jira issue for it.

  2. I have ehcache+JGroups clustered L2 cache. Do I understand correctly that I need exactly synchronized clocks on every cluster member for timestamps/query cache to work correctly?

    1. You should ask this question on the Ehcache forum as they know better the answer to this question.

  3. Hi,
    I have the same problem as described in this SO question https://stackoverflow.com/questions/43467484/hibernate-caching-and-lazy-loaded-associations?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

    The difference is I do not fetch a collection but a ToOne relationship. Do you happen to have some advice why the entity is correctly fetched for the first time, but when it is retrieved from cache it throws a LazyInitialization exception? Both the parent and child entities are cacheable and have the cache strategy set to ReadOnly.

      1. Thanks a lot Vlad, for your quick answer and the JIRA ticket. For me it worked by initializing the JOIN FETCH ToOne association with Hibernate.initialize.

      2. Hi Vlad,
        I found that your issue HHH-12430 was closed as duplicate. The original issue HHH-1523 is marked as open since 2006. In the original issue details only Hibernate versions 3.0.5 and 3.1 are pointed to
        be affected. This is misleading and we know that this is not true because it is not working in 5.2.16 and 5.0.12 as we checked.

        Could you please append 5.x versions to the affected versions in HHH-1523 issue? Maybe this could bring more attention to this old issue and will result in a faster fix…

      3. I appended the affected versions. I’m not sure how easily this fix is going to be since the entity query cache will always store just the ids of the entities being fetched but not associated fetches. That’s also probably why it wasn’t fixed yet.

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.