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