Enterprise Java

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 criteria 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:

1
2
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:

1
2
3
4
5
6
7
8
9
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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:

01
02
03
04
05
06
07
08
09
10
11
12
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();
}
1
2
3
4
5
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
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();
}
1
2
3
4
5
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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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 preinvalidates 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 and changes propagate from session-level isolation to general read consistency
  • The actual invalidation takes place and the cache entry timestamp is updated again

This approach can break the READ COMMITTED consistency guarantees, because Dirty reads are possible, since the current isolated changes are propagated to the Cache prior to committing the database transaction.

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:

01
02
03
04
05
06
07
08
09
10
11
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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:

01
02
03
04
05
06
07
08
09
10
11
12
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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.

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.

Reference: How does Hibernate Query Cache work from our JCG partner Vlad Mihalcea at the Vlad Mihalcea’s Blog blog.

Vlad Mihalcea

Vlad Mihalcea is a software architect passionate about software integration, high scalability and concurrency challenges.
Subscribe
Notify of
guest


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

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button