How does Hibernate Query Cache work

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 table space 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 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.

If you liked this article, you might want to subscribe to my newsletter too.

Advertisements

6 thoughts on “How does Hibernate Query Cache work

  1. I am using Hibernate(with JPA) in an application that has a high write-read ratio. For caching I have enabled query-cache and hibernate second level cache(ehcache). The problem I am facing is due to automatic query cache invalidation when an update is done. Is there any way to configure query cache to update its values instead of invalidating them? Also, since I am using queries to fetch entities, avoiding query-cache is also not an option

    1. The query cache is not useful for write-mostly applications, as you probably figured out yourself. There is no write-through query caching option, so you need to question why you are using this feature in the first place. The entity caching is useful when you plan on changing those entities you’re querying. If you only need projections/views for rendering a table or something similar, then you are better of using a native query instead and take advantage of database indexing.

      There are other data access alternatives, like Speedment ORM, which can speed up data retrieval considerably (but not JPA compliant).

  2. Hi, your blogs have been most helpful. I would like to know more about query cache consistency ( using HQL). Say I update an entity and commit to database, is there any time-lag in which another fetch operation can read stale values from query cache before its invalidation or does the query cache invalidate immediately after commit?

  3. Hi, thanks for helpful article. If I update entity at the beginning of transaction after that query cache would not work to the end of current transaction. So it means that I have to commit update transaction and open new if I want to use query cache. Am I right?
    Use case:
    beginTransaction();
    updateEntityXforKeyY();
    loop{
    //this read will ping db every time
    readEntityXforKeyZ();
    }
    commitTransaction();

    1. No. If you update an entity, the all the associated query spaces will be invalidated and the other transactions will simply go to the database instead of loading entries from the cache.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s