Friday, May 22, 2009

Evicting the Hibernate Domain Cache With Spring

Goal


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.


  psql -U postgres -c "CREATE DATABASE hibernate_cache" -d template1
  psql -U postgres -c "CREATE USER hibernate_cache WITH PASSWORD 'hibernate_cache'" -d hibernate_cache
  psql -U postgres -c "grant all privileges on database hibernate_cache to hibernate_cache" -d 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.

<project....>
    ....
    <build>
        ....
        <plugins>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>maven-jetty-plugin</artifactId>
                <configuration>
                    <scanIntervalSeconds>5</scanIntervalSeconds>
                    <reload>automatic</reload>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>



We can start Jetty from a command prompt.


  mvn jetty:run


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.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>timezra.blog.hibernate.cache</groupId>
    <artifactId>timezra.blog.hibernate.cache</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>
    <name>timezra.blog.hibernate.cache Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.context</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.context.support</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web.servlet</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.transaction</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.orm</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.5.2</version>
        </dependency>
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.4.GA</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>1.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.opensymphony.quartz</groupId>
            <artifactId>quartz</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.3-603.jdbc4</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <scope>test</scope>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.5</version>
        </dependency>
        <dependency>
            <scope>test</scope>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>timezra.blog.hibernate.cache</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>maven-jetty-plugin</artifactId>
                <configuration>
                    <scanIntervalSeconds>5</scanIntervalSeconds>
                    <reload>automatic</reload>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>SpringSource Enterprise Bundle Repository - External Bundle Milestones</id>
            <url>http://repository.springsource.com/maven/bundles/milestone</url>
        </repository>
        <repository>
            <id>SpringSource Enterprise Bundle Repository - SpringSource Bundle Releases</id>
            <url>http://repository.springsource.com/maven/bundles/release</url>
        </repository>
        <repository>
            <id>SpringSource Enterprise Bundle Repository - External Bundle Releases</id>
            <url>http://repository.springsource.com/maven/bundles/external</url>
        </repository>
    </repositories>
</project>



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.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
         http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
>
    <display-name>Archetype Created Web Application</display-name>
    <servlet>
        <servlet-name>timezra.blog.hibernate.cache</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>timezra.blog.hibernate.cache</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
</web-app>



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.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd"
>
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property>
        <property name="prefix" value="/WEB-INF/jsp/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
</beans>



Suppose we have the following user story:

As a customer,
I want to browse through a list of titles,
So that I can find out who wrote a particular book.


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.

package timezra.blog.hibernate.cache.dao;

import java.util.Collection;
import timezra.blog.hibernate.cache.domain.Book;

public interface IBookDAO {
    Collection<Book> findAll();
    Book findByIsbn13(final long isbn13);
}



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.

package timezra.blog.hibernate.cache.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

@Entity
@Table(name = "BOOK", schema = "")
@NamedQueries( { @NamedQuery(name = "findAllBooks", query = "from Book"),
        @NamedQuery(name = "findByIsbn13", query = "from Book book where book.isbn13 = :vIsbn13") })
public class Book implements java.io.Serializable {

    private static final long serialVersionUID = 7081749995516354985L;
    private long isbn13;
    private String title;
    private String subtitle;
    private String author;

    @Id
    @Column(name = "ISBN_13", nullable = false)
    public long getIsbn13() {
        return isbn13;
    }

    public void setIsbn13(final long isbn13) {
        this.isbn13 = isbn13;
    }

    @Column(name = "TITLE", nullable = false, length = 50)
    public String getTitle() {
        return title;
    }

    public void setTitle(final String title) {
        this.title = title;
    }

    @Column(name = "SUBTITLE", nullable = true, length = 150)
    public String getSubtitle() {
        return subtitle;
    }

    public void setSubtitle(final String subtitle) {
        this.subtitle = subtitle;
    }

    @Column(name = "AUTHOR", nullable = false, length = 50)
    public String getAuthor() {
        return author;
    }

    public void setAuthor(final String author) {
        this.author = author;
    }
}



In the src/main/resources/hibernate.cfg.xml, we will register the Book.

<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <mapping class="timezra.blog.hibernate.cache.domain.Book" />
    </session-factory>
</hibernate-configuration>




Our BookDAO can simply use the @NamedQueries declared on Book to implement its functions.

package timezra.blog.hibernate.cache.dao;

import java.util.Collection;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import timezra.blog.hibernate.cache.domain.Book;

@Repository
public class BookDAO implements IBookDAO {

    private final SessionFactory sessionFactory;

    @Autowired
    public BookDAO(final SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @SuppressWarnings("unchecked")
    @Transactional
    public Collection<Book> findAll() {
        return sessionFactory.getCurrentSession() //
                .getNamedQuery("findAllBooks") //
                .list();
    }

    @Transactional
    public Book findByIsbn13(final long isbn13) {
        return (Book) sessionFactory.getCurrentSession() //
                .getNamedQuery("findByIsbn13") //
                .setParameter("vIsbn13", isbn13) //
                .uniqueResult();
    }
}


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.

....
<beans....>
    <import resource="classpath:application-context-daos.xml"/>
    ....
</beans>



The src/main/resources/application-context-daos.xml file contains the database connection configuration.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd"
>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName">
            <value>org.postgresql.Driver</value>
        </property>
        <property name="username">
            <value>hibernate_cache</value>
        </property>
        <property name="password">
            <value>hibernate_cache</value>
        </property>
        <property name="url">
            <value>jdbc:postgresql://localhost:5432/hibernate_cache</value>
        </property>
    </bean>
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:hibernate.cfg.xml" />
        <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.generate_statistics">true</prop>
                <prop key="hibernate.use_sql_comments">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>
                <prop key="hibernate.query.factory_class">org.hibernate.hql.ast.ASTQueryTranslatorFactory</prop>
            </props>
        </property>
    </bean>
    <context:component-scan base-package="timezra.blog.hibernate.cache.dao"/>
    <bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>
    <tx:annotation-driven transaction-manager="txManager" />
</beans>


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.

package timezra.blog.hibernate.cache.dao;

import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.util.Collection;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import timezra.blog.hibernate.cache.domain.Book;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/application-context-daos.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public class BookDAOTest {

    @Autowired private SessionFactory sessionFactory;
    @Autowired private IBookDAO bookDAO;
    private Book hibernateMadeEasy;
    private Book harnessingHibernate;
    private Book javaPersistenceWithHibernate;

    @Before
    public void setUp() {
        final Session session = sessionFactory.getCurrentSession();
        session.save(getOrCreateHibernateMadeEasy());
        session.save(getOrCreateJavaPersistenceWithHibernate());
        session.save(getOrCreateHarnessingHibernate());
        session.flush();
    }

    @Test
    public void findAllReturnsAllTheBooks() throws Exception {
        final Collection<Book> books = bookDAO.findAll();

        assertTrue(books.contains(getOrCreateHibernateMadeEasy()));
        assertTrue(books.contains(getOrCreateHarnessingHibernate()));
        assertTrue(books.contains(getOrCreateJavaPersistenceWithHibernate()));
    }

    @Test
    public void findByIsbn13ReturnsTheBook() throws Exception {
        final Book actual = bookDAO.findByIsbn13(9780596517724L);
        assertSame(getOrCreateHarnessingHibernate(), actual);
    }

    private Book getOrCreateHibernateMadeEasy() {
        if (hibernateMadeEasy != null) {
            return hibernateMadeEasy;
        }
        hibernateMadeEasy = new Book();
        hibernateMadeEasy.setAuthor("Cameron McKenzie");
        hibernateMadeEasy.setIsbn13(9780615201955L);
        hibernateMadeEasy.setTitle("Hibernate Made Easy");
        hibernateMadeEasy
                .setSubtitle("Simplified Data Persistence with Hibernate and JPA (Java Persistence API) Annotations");
        return hibernateMadeEasy;
    }

    private Book getOrCreateHarnessingHibernate() {
        if (harnessingHibernate != null) {
            return harnessingHibernate;
        }
        harnessingHibernate = new Book();
        harnessingHibernate.setAuthor("James Elliott");
        harnessingHibernate.setIsbn13(9780596517724L);
        harnessingHibernate.setTitle("Harnessing Hibernate");
        return harnessingHibernate;
    }

    private Book getOrCreateJavaPersistenceWithHibernate() {
        if (javaPersistenceWithHibernate != null) {
            return javaPersistenceWithHibernate;
        }
        javaPersistenceWithHibernate = new Book();
        javaPersistenceWithHibernate.setAuthor("Christian Bauer");
        javaPersistenceWithHibernate.setIsbn13(9781932394887L);
        javaPersistenceWithHibernate.setTitle("Java Persistence with Hibernate");
        return javaPersistenceWithHibernate;
    }
}


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.

package timezra.blog.hibernate.cache.controller;

import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import timezra.blog.hibernate.cache.dao.IBookDAO;

@Controller
public class Books {

    private final IBookDAO bookDAO;

    @Autowired
    public Books(final IBookDAO bookDAO) {
        this.bookDAO = bookDAO;
    }

    @RequestMapping("/books.htm")
    public ModelAndView showAllBooks() throws Exception {
        return new ModelAndView("books", "books", bookDAO.findAll());
    }

    @RequestMapping("/book.htm")
    public ModelAndView showABook(final HttpServletRequest request) throws Exception {
        return new ModelAndView("book", "book", bookDAO.findByIsbn13(Long.valueOf(request.getParameter("isbn13"))));
    }
}



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.

....
<beans....>
    ....
    <context:component-scan base-package="timezra.blog.hibernate.cache.controller"/>
</beans>



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.

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>All The Books</title>
</head>
<body>
  <h1>All The Books</h1>
  <table>
    <tr><th>ISBN 13</th><th>Title</th></tr>
    <c:forEach items="${books}" var="book">
      <tr>
        <td><a href="<c:url value='/book.htm'/>?isbn13=<c:out value='${book.isbn13}' />"><c:out value="${book.isbn13}" /></a></td>
        <td><c:out value="${book.title}" /></td>
      </tr>
    </c:forEach>
  </table>
</body>
</html>



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.

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>A Book</title>
</head>
<body>
  <h1>${book.title}<c:if test="${not empty book.subtitle}">:&nbsp;<c:out value="${book.subtitle}" /></c:if></h1><br/>
  Author: <c:out value="${book.author}" /><br/>
  ISBN 13: <c:out value="${book.isbn13}" /><br/>
</body>
</html>



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.


  psql -U hibernate_cache -d hibernate_cache
  Insert Into book (author, subtitle, title, isbn_13)
        values ('Gary Mak', 'A Problem-Solution Approach (Books for Professionals by Professionals)', 'Spring Recipes', 9781590599792);



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.

....
@org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_ONLY)
....



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.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="ehcache.xsd">
    <diskStore path="java.io.tmpdir" />
    <defaultCache 
        maxElementsInMemory="10000" 
        eternal="false"
        timeToIdleSeconds="120" 
        timeToLiveSeconds="120" 
        overflowToDisk="true"
        diskPersistent="false" 
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU" />
    <cache 
        name="timezra.blog.hibernate.cache.domain.Book"
        maxElementsInMemory="100" 
        eternal="true" 
        overflowToDisk="false" />
</ehcache>



Finally, we can update the Hibernate properties in the application-context-daos.xml.

....
<property name="hibernateProperties">
    <props>
        ....
        <prop key="hibernate.cache.provider_class">net.sf.ehcache.hibernate.SingletonEhCacheProvider</prop>
        <prop key="hibernate.cache.use_second_level_cache">true</prop>
        <prop key="hibernate.cache.provider_configuration">classpath:ehcache.xml</prop>
    </props>
</property>
....



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.


  Update book Set author = 'Mak, Gary' Where isbn_13 = 9781590599792;



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.

    ....
    @Transactional
    public Book findByIsbn13(final long isbn13) {
        return (Book) sessionFactory.getCurrentSession() //
                .createCriteria(Book.class) //
                .add(org.hibernate.criterion.Restrictions.eq("isbn13", isbn13)) //
                .uniqueResult();
    }
    ....



Again, we will view the book through the browser, and again we will modify the data.


  Update book Set author = 'Gary Mak' Where isbn_13 = 9781590599792;



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.

    @Transactional
    public Book findByIsbn13(final long isbn13) {
        return (Book) sessionFactory.getCurrentSession().get(Book.class, isbn13);
    }



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.

package timezra.blog.hibernate.cache.batch;

import org.hibernate.SessionFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.StatefulJob;
import org.springframework.scheduling.quartz.QuartzJobBean;
import timezra.blog.hibernate.cache.domain.Book;

public class EvictTheSecondLevelCache extends QuartzJobBean implements StatefulJob {

    private SessionFactory sessionFactory;

    @Override
    protected void executeInternal(final JobExecutionContext context) throws JobExecutionException {
        sessionFactory.evict(Book.class);
    }

    public void setSessionFactory(final SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
}



We can register and schedule the job with a new Spring context file, src/main/resources/application-context-batch.xml.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd"
>
    <import resource="classpath:application-context-daos.xml"/>
    <bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass" value="timezra.blog.hibernate.cache.batch.EvictTheSecondLevelCache" />
        <property name="jobDataAsMap">
            <map>
                <entry key="sessionFactory">
                    <ref bean="sessionFactory" />
                </entry>
            </map>
        </property>
    </bean>
    <bean id="jobDetailTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
        <property name="jobDetail" ref="jobDetail" />
        <property name="cronExpression" value="0 * * * * ?" />
    </bean>
    <bean id="schedulerFactoryBean"
        class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="jobDetailTrigger" />
            </list>
        </property>
    </bean>
</beans>


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.

....
<beans....>
    <import resource="classpath:application-context-batch.xml"/>
    ....
</beans>



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.

Conclusion


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.

The Maven2 standard webapp project structure.

Coming Soon: The Hibernate query cache. . . .

Wednesday, May 13, 2009

Mapping Hibernate Entities to Views

Goal


By the end of this tutorial, we will have the tools to map a Hibernate Entity that does not correspond directly to a database table onto structures such as views or query results.

Setup a PostgreSQL Database


This tutorial breaks from the pattern of previous posts in this series by using PostgreSQL instead of Oracle. The PostgreSQL default one-click installation is sufficient for the example. On Windows, I found it necessary to remove whitespace from the default installation path, and adding the </path/to/postgresql>/bin to the system Path variable has been helpful.

For this example, we can create a new database called examples:

  psql -U postgres -c "CREATE DATABASE examples" -d template1


We can also create a new database user and assign full privileges:

  psql -U postgres -c "CREATE USER hibernate_query WITH PASSWORD 'hibernate_query'" -d examples
  psql -U postgres -c "grant all privileges on database examples to hibernate_query" -d examples


Create an M2Eclipse Project


Creating a simple M2Eclipse project without an archetype should be familiar if you have followed previous posts in this series. Note that, for our example, we will configure our pom.xml for Spring 3.0.0.M3 and for the postgresql 8.3-603.jdbc4 driver. Unlike in prior examples, no extra Maven2 setup of the Oracle JDBC driver is necessary since the PostgreSQL driver is distributed directly from the Maven2 repository.
Our pom.xml will look similar to this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>examples.hibernate.spring.query</groupId>
    <artifactId>examples.hibernate.spring.query</artifactId>
    <name>examples.hibernate.spring.query</name>
    <version>0.0.1-SNAPSHOT</version>
    <description>Hibernate Query Example</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.transaction</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.orm</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.5.2</version>
        </dependency>
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.4.GA</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.3-603.jdbc4</version>
        </dependency>
        <dependency>
            <scope>test</scope>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.5</version>
        </dependency>
        <dependency>
            <scope>test</scope>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>3.0.0.M3</version>
        </dependency>
    </dependencies>
    <repositories>
        <repository>
            <id>SpringSource Enterprise Bundle Repository - External Bundle Milestones</id>
            <url>http://repository.springsource.com/maven/bundles/milestone</url>
        </repository>
        <repository>
            <id>SpringSource Enterprise Bundle Repository - SpringSource Bundle Releases</id>
            <url>http://repository.springsource.com/maven/bundles/release</url>
        </repository>
        <repository>
            <id>SpringSource Enterprise Bundle Repository - External Bundle Releases</id>
            <url>http://repository.springsource.com/maven/bundles/external</url>
        </repository>
    </repositories>
</project>



Similarly, our Spring src/main/resources/applicationContext.xml must also be configured for a PostgreSQL, rather than Oracle, connection:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd"
>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName">
            <value>org.postgresql.Driver</value>
        </property>
        <property name="username">
            <value>hibernate_query</value>
        </property>
        <property name="password">
            <value>hibernate_query</value>
        </property>
        <property name="url">
            <value>jdbc:postgresql://localhost:5432/examples</value>
        </property>
    </bean>
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:/hibernate.cfg.xml" />
        <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.generate_statistics">true</prop>
                <prop key="hibernate.use_sql_comments">true</prop>
                <prop key="hibernate.hbm2ddl.auto">create</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>
                <prop key="hibernate.query.factory_class">org.hibernate.hql.ast.ASTQueryTranslatorFactory</prop>
            </props>
        </property>
    </bean>
    <bean id="txManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>
    <tx:annotation-driven transaction-manager="txManager" />
</beans>



We can also create a very simple domain of authors. Every Author will have a first name, a last name and a generated id:

package examples.hibernate.spring.query.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity
@Table(name = "AUTHOR", schema = "")
public class Author implements java.io.Serializable {

    private static final long serialVersionUID = -6270202393794713117L;

    private int id;

    private String firstName;

    private String lastName;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
    @SequenceGenerator(name = "author_id_seq", sequenceName = "author_id_seq")
    @Column(name = "ID", nullable = false)
    public int getId() {
        return id;
    }

    public void setId(final int id) {
        this.id = id;
    }

    @Column(name = "FIRST_NAME", nullable = false, length = 50)
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(final String firstName) {
        this.firstName = firstName;
    }

    @Column(name = "LAST_NAME", nullable = false, length = 50)
    public String getLastName() {
        return lastName;
    }

    public void setLastName(final String lastName) {
        this.lastName = lastName;
    }
}



We can register our domain with Hibernate in the src/main/resources/hibernate.cfg.xml:

<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <mapping class="examples.hibernate.spring.query.domain.Author" />
    </session-factory>
</hibernate-configuration>




The Custom Query


Suppose we want to run a custom query from Hibernate, but the resulting rows do not correspond directly to a Table or to the columns in a table. For example, suppose we want to find the number of unique first names for all authors.
Mapping the results of a custom query to the fields of a Hibernate POJO is very similar to mapping table columns.
For our example, we can call an individual result an AuthorAggregate. It will contain fields for a first name and a count of the number of occurrences for that first name. The aggregate must also have an id for uniquely identifying each result:

package examples.hibernate.spring.query.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.annotations.GenericGenerator;

@Entity
public class AuthorAggregate {

    private long id;
    private String firstName;
    private Long nameCOunt;

    public void setId(final long id) {
        this.id = id;
    }

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    public long getId() {
        return id;
    }

    public void setFirstName(final String firstName) {
        this.firstName = firstName;
    }

    @Column(name = "FIRST_NAME", length = 50)
    public String getFirstName() {
        return firstName;
    }

    public void setNameCount(final Long nameCOunt) {
        this.nameCOunt = nameCOunt;
    }

    @Column(name = "NAME_COUNT")
    public Long getNameCount() {
        return nameCOunt;
    }
}



We will register this object in our src/main/resources/hibernate.cfg.xml as part of our domain so that Hibernate can map the results of the query to the object's fields:

....
        <mapping class="examples.hibernate.spring.query.domain.AuthorAggregate" />
....



We can test the execution of our query and the mapping of the results in JUnit with an AuthorTest:

package examples.hibernate.spring.query.domain;

import static org.junit.Assert.assertEquals;

import java.util.Collection;
import org.hibernate.SessionFactory;
import org.hibernate.classic.Session;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = true)
@Transactional
public class AuthorTest {

    private static final String AUTHORS_BY_FIRST_NAME = //
    "select first_name, count(author.id) as name_count, random() * 100000000000000000 as id " //
            + "from author " //
            + "group by author.first_name";

    @Autowired
    private SessionFactory sessionFactory;

    @Before
    public void setUp() {
        final Session session = sessionFactory.getCurrentSession();
        session.save(createCameronMcKenzie());
        session.save(createChristianBauer());
        session.save(createCameronJudd());
        session.flush();
    }

    @Test
    public void findAggregationOfNames() throws Exception {
        final Collection<AuthorAggregate> authorsByName = findByFirstName();
        assertEquals(2, authorsByName.size());
        for (final AuthorAggregate authorAggregate : authorsByName) {
            if ("Cameron".equals(authorAggregate.getFirstName())) {
                assertEquals(Long.valueOf(2), authorAggregate.getNameCount());
            } else {
                assertEquals("Christian", authorAggregate.getFirstName());
                assertEquals(Long.valueOf(1), authorAggregate.getNameCount());
            }
        }
    }

    @SuppressWarnings("unchecked")
    private Collection<AuthorAggregate> findByFirstName() {
        return sessionFactory.getCurrentSession() //
                .createSQLQuery(AUTHORS_BY_FIRST_NAME) // 
                .addEntity(AuthorAggregate.class) //
                .list();
    }

    private Author createCameronMcKenzie() {
        final Author cameronMcKenzie = new Author();
        cameronMcKenzie.setFirstName("Cameron");
        cameronMcKenzie.setLastName("McKenzie");
        return cameronMcKenzie;
    }

    private Author createCameronJudd() {
        final Author cameronMcKenzie = new Author();
        cameronMcKenzie.setFirstName("Cameron");
        cameronMcKenzie.setLastName("Judd");
        return cameronMcKenzie;
    }

    private Author createChristianBauer() {
        final Author christianBauer = new Author();
        christianBauer.setFirstName("Christian");
        christianBauer.setLastName("Bauer");
        return christianBauer;
    }
}


NB: To generate a unique id for each result, we are using the PostgreSQL random() function. In Oracle, we could generate a string of random characters with a function call such as dbms_random.string('P', 20). Also note that before we execute the native query, we must flush the Hibernate session to ensure that the Objects created and saved through HQL are persisted all the way to the database.

The View


Suppose that we want a list of all the last names of all the authors whose first name is Cameron.
Now that the author table exists in our database, we can create a custom view for this requirement through psql. We can connect to the examples database through psql

  psql -U hibernate_query -d examples


We will create the list of surnames directly through SQL:

CREATE OR REPLACE VIEW cameron AS
  SELECT last_name AS surname
  FROM author
  WHERE first_name = 'Cameron';


Setting up the Hibernate domain object for this view is similar to mapping the results of a custom query to an annotated POJO as described above. For this example, instead of creating the native query inline and registering the output Entity directly with the query, we will utilize a javax.persistence annotation to register a NamedNativeQuery directly with our domain object, Cameron:

package examples.hibernate.spring.query.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedNativeQuery;

@Entity
@NamedNativeQuery(name = "findUniqueCameronsInOrder", query = "select * from cameron order by surname", resultClass = Cameron.class)
public class Cameron implements java.io.Serializable {

    private static final long serialVersionUID = 8765016103450361311L;

    private String surname;

    @Id
    @Column(name = "SURNAME", nullable = false, length = 50)
    public String getSurname() {
        return surname;
    }

    public void setSurname(final String surname) {
        this.surname = surname;
    }
}


NB: Our named query is doing slightly more than just selecting everything from the view. It selects only unique surnames, thus ensuring that the surname can be used as the @Id, and it also orders the returned surnames alphabetically.

We will again register this domain object with Hibernate in the src/main/resources/hibernate.cfg.xml:

....
        <mapping class="examples.hibernate.spring.query.domain.Cameron" />
....



We can run the query with a few additions to the JUnit test:


....
    @Test
    public void findTheCameronsInTheView() throws Exception {
        final List<Cameron> camerons = findUniqueCameronsInOrder();
        assertEquals(2, camerons.size());
        final Cameron judd = camerons.get(0);
        final Cameron mcKenzie = camerons.get(1);
        assertEquals("Judd", judd.getSurname());
        assertEquals("McKenzie", mcKenzie.getSurname());
    }

    @SuppressWarnings("unchecked")
    private List<Cameron> findUniqueCameronsInOrder() {
        return sessionFactory.getCurrentSession() //
                .getNamedQuery("findUniqueCameronsInOrder") //
                .list();
    }
....



Conclusion


In this 12K project, we now have two examples for registering Hibernate-annotated POJOs with database structures that do not correspond directly to tables.
The query example project structure.