Other XP Bloggers

Tuesday, May 26, 2009

Evicting the Hibernate Query Cache With Spring

Goal

This tutorial builds on the example presented in a previous post. Here we will explore the difference between the Hibernate second-level cache and the standard query cache and how the two can be used in conjunction to reduce the number of database transactions in an application.

The Hibernate Second-level Cache

We will begin with a quick review of the behavior of the Hibernate second-level cache. The results displayed on the http://localhost:8080/timezra.blog.hibernate.cache/books.htm page should look familiar if you have coded along with the hibernate cache eviction example.

After we follow the link for Spring Recipes, we will see that the subtitle of the book has not been updated. As demonstrated in the previous post, the query is persisting the individual Books in the second-level cache, so this result should not surprise us.

If we reopen or refresh books.htm and follow the link for Spring Recipes again, the subtitle of the book reflects the current state of the database. This result should raise an eyebrow. It appears that the transaction to find all the books has been re-run in the database and Hibernate has updated the individual items from the query's result in the second-level cache. Even though Hibernate stores individual Books, it does not store the result of the query itself, so all the Books are reloaded and persisted again.

Cache the Query Results

Caching individual domain objects has eliminated some unneccessary database traffic, but in this situation, we can optimize even more. Our database is updated exactly once at night, so we should not need a new transaction every time a user views the list of books.In the Hibernate properties of application-context-daos.xml, we will enable the query cache.

We must also configure Book's @NamedQuery "findAllBooks" to store its results in the standard query cache.

....@NamedQueries({@NamedQuery(name="findAllBooks",query="from Book",hints={@javax.persistence.QueryHint(name="org.hibernate.cacheable",value="true")}),@NamedQuery(name="findByIsbn13",query="from Book book where book.isbn13 = :vIsbn13")})....

NB: Rather than naming our queries with the javax.persistence annotations, we could just as well have used org.hibernate.annotations, in which case our declaration would not require @QueryHints.

....@org.hibernate.annotations.NamedQueries({@org.hibernate.annotations.NamedQuery(name="findAllBooks",query="from Book",cacheable=true),@org.hibernate.annotations.NamedQuery(name="findByIsbn13",query="from Book book where book.isbn13 = :vIsbn13")})....

NB: Rather than configuring the @NamedQuery with @QueryHints, we could also enable caching in the BookDAO invocation of the query itself.

With any of these configurations, we can again run our test in the browser.

view all the books

select a single book

update the book in the database

view all the books again

select the same individual book

The book now does not reflect the most current state of the database and clearly comes from the cache. This is the behavior that we are seeking.

Flush the Query Cache

From the previous tutorial, our Hibernate second-level cache is cleared every minute on the zeroth second. If we run the tests described above and wait long enough, we will eventually see an update to the book's subtitle. Suppose, however, we delete a book from the database.

Delete From book Where isbn_13 = 9781590599792;

If we allow the second-level cache to expire and refresh the list of all books, we will see an error.

org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [timezra.blog.hibernate.cache.domain.Book#9781590599792] at org.hibernate.impl.SessionFactoryImpl$2.handleEntityNotFound(SessionFactoryImpl.java:409) at org.hibernate.event.def.DefaultLoadEventListener.load(DefaultLoadEventListener.java:171) at org.hibernate.event.def.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:223) at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:126) at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:905) at org.hibernate.impl.SessionImpl.internalLoad(SessionImpl.java:873) at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:590) at org.hibernate.type.ManyToOneType.assemble(ManyToOneType.java:219) at org.hibernate.cache.StandardQueryCache.get(StandardQueryCache.java:155) at org.hibernate.loader.Loader.getResultFromQueryCache(Loader.java:2184) at org.hibernate.loader.Loader.listUsingQueryCache(Loader.java:2147) at org.hibernate.loader.Loader.list(Loader.java:2117) at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:401) at org.hibernate.hql.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:361) at org.hibernate.engine.query.HQLQueryPlan.performList(HQLQueryPlan.java:196) at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1148) at org.hibernate.impl.QueryImpl.list(QueryImpl.java:102) at timezra.blog.hibernate.cache.dao.BookDAO.findAll(BookDAO.java:25) at sun.reflect.GeneratedMethodAccessor23.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202) at $Proxy31.findAll(Unknown Source) at timezra.blog.hibernate.cache.controller.Books.showAllBooks(Books.java:24)....(more)

The persisted query results now reference the primary key for an invalid row that cannot be refreshed. Of course, since the query cache expires every 120 seconds per the default configuration in ehcache.xml, if we wait another minute, the error will no longer appear and we will simply see an empty table of books.

In order to avoid these types of stale or non-existent results, we can modify EvictTheSecondLevelCache.java to flush the standard query cache.

NB: We have chosen to evict the query cache before the second-level cache. If the two lines were reversed, we would still run the risk, however small, of encountering the org.hibernate.ObjectNotFoundException described above.

Here we are evicting all queries in the default org.hibernate.cache.StandardQueryCache. If we want to be selective about the cached queries that should be flushed, SessionFactory#sessionFactory.evictQueries(...) also takes the name of a cache region, which we can declare as a @QueryHint in Book.java just as we have configured cacheability (or we could set another attribute on the @org.hibernate.annotations.NamedQuery, or we could set the cache region when the query is called directly in the BookDAO).

....@NamedQueries({@NamedQuery(name="findAllBooks",query="from Book",hints={@javax.persistence.QueryHint(name="org.hibernate.cacheable",value="true"),@javax.persistence.QueryHint(name="org.hibernate.cacheRegion",value="findAllBooks")}),@NamedQuery(name="findByIsbn13",query="from Book book where book.isbn13 = :vIsbn13")})....

Cache the Query But Not the Domain

We now have three configurations for the two caches:

neither cache is enabled

only the second-level cache is enabled

both caches are enabled

What is the result if we enable the standard query cache without storing domain objects?We can explore this scenario by simply removing the query hint from the @NamedQuery to disable the domain store. We will then insert a record into the database.

Update book Set title = 'Sprig in Acton' Where isbn_13 = 9781933988139;

The new title appears on the refreshed books.htm page. Even though Hibernate has stored the query results, because it has not saved the domain objects themselves, they have been refreshed by their primary keys from the database. Our particular set of books is rather small, but suppose we have a larger data set. The first time the query to find all books runs, there is exactly one transaction. For subsequent requests, until the query cache is cleared, Hibernate refreshes each Book individually by its ISBN. Clearly there is no performance benefit for us to cache only query results without domain objects in this scenario. In fact, this misconfiguration could cause a significant performance loss far worse than having no cache at all.

Conclusion

By expanding the infrastructure introduced in the previous example, we have further optimized our application's database transactions by caching not only individual objects, but also the result sets of queries. We can now also clear this store of query results at a fixed regular time coordinated with the eviction of the second-level cache.

My example hack is pasted below after a cursory glance at the code. I only use Reflection as an absolute last resort. Perhaps you might want to post this question on a hibernate forum, suggest an addition to the SessionFactory API and propose this example (sans Reflection) as a reference implementation for the SessionFactoryImpl.I appreciate the question and hope this helps,---Tim---