This tutorial presents an exploration of the Hibernate second-level cache. By the end, we will have the tools to integrate Ehcache into a web application and to expire its entries at fixed, regular intervals using the Quartz scheduler to run a Spring batch process.
Setup a Database
For this example, we can setup a PostgreSQL database, as in the previous example. Here, our database, user and password can all be set to hibernate_cache.
A Maven2 Web Application
We can create our timezra.blog.hibernate.cache project with the m2eclipse plugin. Instead of setting up a simple project as in a previous post, we will use the maven-archetype-webapp from the Nexus Indexer catalog. We will also add src/main/java, src/test/resources and src/test/java explicitly to our build path since we naturally want to test our project as we add features incrementally. For web applications, Jetty provides a Maven plugin that scans our target path and automatically re-deploys any changes. The Jetty plugin can be configured in the <build> section of our pom.xml.
We can start Jetty from a command prompt.
The page http://localhost:8080/timezra.blog.hibernate.cache/ will be available through a browser and we should see a "Hello World!" message.
A Core Sample from the Database to the UI
We are now ready to add Hibernate, Ehcache, Spring, Quartz and test-related dependencies. Our fully-configured pom.xml should look familiar to anyone who has gone through both the batch processing and Hibernate tutorials, with a few additions specifically for web development and for emitting Java6-compliant code from our compiler during the build.
Since we will be using Spring MVC for our web framework, we must configure the src/main/webapp/WEB-INF/web.xml to route all page requests with an htm extension through the Spring Front Controller.
We must not necessarily declare the application contextConfigLocation in the web.xml since the Spring MVC convention of locating a file with the servlet name and a -servlet.xml suffix will suffice. Our src/main/webapp/WEB-INF/timezra.blog.hibernate.cache-servlet.xml can simply indicate where the view pages will reside.
Suppose we have the following user story:
We can interact with our domain through a data access component, an IBookDAO that has two functions: one for retrieving all the books, and another for retrieving a single book based on its key.
Our domain needs just one object, a Book, and it will have attributes for the title, author and a unique value for the primary key, which here can be an isbn. We can also specify the HQL queries for accessing this object.
In the src/main/resources/hibernate.cfg.xml, we will register the Book.
Our BookDAO can simply use the @NamedQueries declared on Book to implement its functions.
NB: We have annotated the DAO as a @Repository. Instead of declaring this Spring bean explicitly, we merely need to configure Spring to scan a set of packages to discover the components to use as beans.
Because the web application context is outside the classpath, we can delegate Spring registration of our DAOs to a classpath resource. We will import the application-context-daos.xml in the timezra.blog.hibernate.cache-servlet.xml.
The src/main/resources/application-context-daos.xml file contains the database connection configuration.
NB: We have not declared our DAO explicitly because we have configured Spring to scan for components in the package that contains our @Resource.
Now that we have a domain and a way to access the domain, we can write some tests in the BookDAOTest suite to ensure that our functions work.
NB: Because the application-context-daos.xml is on the classpath, Spring will locate the context for the tests and inject the dependencies.
Finally, we can create a view into the database for each of the functions of the user story. One controller called Books can handle both requests.
Note how Books is registered as a @Controller, just as BookDAO is a @Resource. We will simply register the controller's package in our timezra.blog.hibernate.cache-servlet.xml and Spring will locate the component.
The first request, which takes no parameters, provides a view of all the titles in the database. We can call this view src/main/webapp/WEB-INF/jsp/books.jsp.
The second request provides a view of the details of a single book, which is located by its isbn-13. This view is src/main/webapp/WEB-INF/jsp/book.jsp.
The page http://localhost:8080/timezra.blog.hibernate.cache/books.htm will be available through a browser and should display an empty table of information. If we were to begin adding books to our database, we would see this table fill and we could navigate to individual book detail pages.
Cache For Books
Suppose an external process updates our database of books once nightly, but we expect that customers will browse for books frequently during the day. To reduce database overhead, we can cache the book information on the application server. We will expire the cache only after the database has been updated.
The Hibernate primary cache is used automatically for transaction-level optimization. Therefore, a domain object should only be retrieved once for each full database transaction, even if it is referenced more than once.
Hibernate second-level persistence, by contrast, is SessionFactory-wide, so the information about the object is available across multiple transactions. This second-level store is not automatic and must be explicitly configured. We can register the Book as a candidate for such a cache with a class-level annotation.
For this example, we will use Ehcache, which requires an external configuration, here called src/main/resources/ehcache.xml. We will declare default settings as well as properties for individual domain objects. We can store the Books forever because we will expire the cache periodically through a batch process.
Finally, we can update the Hibernate properties in the application-context-daos.xml.
Now, we will view our book through the browser. None of the information will have changed, as expected.
Suppose we update the book through psql.
Now that the second-level cache is available and our domain object has been registered, I would expect that the information on the page will not have changed. When we refresh the browser, however, the author's name is updated. This indicates that, even though the @NamedQuery uses HQL and the query does a lookup by the Book's primary key, the result is not taken from the second-level store, and a database transaction has occurred. The cache has provided no benefit.
Suppose we change BookDAO#findByIsbn13(...) to query by criteria.
Again, we will view the book through the browser, and again we will modify the data.
When we refresh the browser, the data will have changed to reflect the current state in the database. This behavior indicates that, despite very simple criteria, i.e., primary key equality, Hibernate is bypassing the store and directly querying the database.
Suppose we change BookDAO#findByIsbn13(...) to get the Book directly from the current session.
Finally, when we view the data in the browser, update the author through psql, and refresh the browser again, we will see the expected caching behavior.
NB: We also could have used SessionFactory#load(...), but this method returns a proxy to the actual Book instance. We would need to resolve this within a transactional context, e.g., by calling an accessor method for one of the non-primary-key fields in the DAO method. Also, even if the Book does not exist in the cache, #load(...) will return a proxy, and we will see an error when Hibernate tries to resolve it. For our needs, SessionFactory#get(...) is sufficient.
Evict the Cache
Now that our caching behavior works as anticipated, we will use a Spring batch process with a Quartz timer to evict the cache at a specific time. In this type of scenario, the job might run during a period of low traffic, perhaps in the middle of the night. For our example, we will expire the cache every minute for fast feedback.
For anyone who has worked through the previous tutorial on Spring batch processing, there should be no surprises in this code.
We will first create a job implementation, EvictTheSecondLevelCache, that removes all the Books from the SessionFactory's store.
We can register and schedule the job with a new Spring context file, src/main/resources/application-context-batch.xml.
NB: To inject the sessionFactory declared in application-context-daos.xml, we must import that resource, as well.
Finally, we will register the new configuration with the general web application context, timezra.blog.hibernate.cache-servlet.xml, so that the job begins automatically when the web application starts.
Now, we can load our book details in the browser, update the author through psql, reload the page, and the data should not have changed. If we wait until the cache is invalidated (here, one or two minutes) and refresh the page, the data will reflect the current state of the table.
With a few kilobytes of custom code, we have a project infrastructure for unit tests, a domain, a data access layer, views into our data, a batch process and a cache that will make expanding the application with more tests, a larger domain, more sophisticated queries, caching and page flows straightforward, at least from a technical perspective. Gathering user stories and prioritizing them by potential ROI is another topic entirely and one that must be handled as a conversation between you and your product owner.
Coming Soon: The Hibernate query cache. . . .