Wednesday, October 5, 2011

JPA Callbacks With Hibernate

Goal:


The purpose of this blog entry is to provide one solution for using JPA Annotations with Hibernate but without using the Hibernate EntityManager. Thanks to Ben Macfarlane for posing this problem and for doing the groundwork for me to come up with a somewhat simple solution.

tl;dr


If you want to look right away at working code that compares the configuration of JPA callbacks with the EntityManager and the Hibernate SessionFactory, a sample project based on this tutorial is available.

Create a Maven Project:


We will start with a boilerplate Maven project that includes dependencies on Spring, Hibernate and JUnit.
pom.xml
<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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>timezra.hibernate</groupId>
  <artifactId>callbacks</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <description>Demo for using JPA callbacks with hibernate</description>
  <properties>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.0.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib-nodep</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>3.6.7.Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-annotations</artifactId>
      <version>3.5.6-Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>3.1.0.GA</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.15.0-GA</version>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.3.160</version>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.4</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.0.6.RELEASE</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>



Create a Domain and Domain Access:


Our domain can be as simple as a single POJO that uses javax.persistence annotations such as @PrePersist, @PreUpdate and @PostLoad.
src/main/java/timezra/hibernate/callbacks/domain/Author.java
package timezra.hibernate.callbacks.domain;

import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import org.hibernate.validator.NotNull;

@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = Author.NAME_ATTRIBUTE))
public class Author {

  
public static final String NAME_ATTRIBUTE = "name";

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  
private Long id;
  @NotNull
  
private String name;
  
private Date dateOfBirth;
  @Transient
  
private Integer age;
  @NotNull
  
private Date dateCreated;
  @NotNull
  
private Date lastUpdated;

  @PrePersist
  
void prePersist() {
    dateCreated = lastUpdated = 
new Date();
  }

  @PreUpdate
  
void preUpdate() {
    lastUpdated = 
new Date();
  }

  @PostLoad
  
void postLoad() {
    
if (dateOfBirth != null) {
      
final Calendar now = Calendar.getInstance(Locale.getDefault());
      
final int thisYear = now.get(Calendar.YEAR);
      
final int thisDay = now.get(Calendar.DAY_OF_YEAR);
      now.setTime(dateOfBirth);
      
final int birthYear = now.get(Calendar.YEAR);
      
final int birthDay = now.get(Calendar.DAY_OF_YEAR);
      age = thisYear - birthYear - (birthDay > thisDay ? 1 : 0);
    }
  }

  
public Long getId() {
    
return id;
  }

  
public Date getDateCreated() {
    
return dateCreated;
  }

  
public Date getLastUpdated() {
    
return lastUpdated;
  }

  
public Integer getAge() {
    
return age;
  }

  
public Date getDateOfBirth() {
    
return dateOfBirth;
  }

  
public void setDateOfBirth(final Date dateOfBirth) {
    
this.dateOfBirth = dateOfBirth;
  }

  
public String getName() {
    
return name;
  }

  
public void setName(final String name) {
    
this.name = name;
  }
}



A DAO and Service can provide access into our domain.
src/main/java/timezra/hibernate/callbacks/dao/AuthorDAO.java
package timezra.hibernate.callbacks.dao;

import javax.annotation.Resource;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.stereotype.Repository;
import timezra.hibernate.callbacks.domain.Author;

@Repository
public class AuthorDAO {

  @Resource
  
private SessionFactory sessionFactory;

  
public void create(final Author a) {
    getSession().save(a);
  }

  
public Author findByName(final String name) {
    
return (Author) getSession().createCriteria(Author.class//
        .add(Restrictions.eq(Author.NAME_ATTRIBUTE, name)) //
        .uniqueResult();
  }

  
public void update(final Author a) {
    getSession().update(a);
  }

  
public void delete(final Author a) {
    getSession().delete(a);
  }

  
private Session getSession() {
    
return sessionFactory.getCurrentSession();
  }
}



src/main/java/timezra/hibernate/callbacks/service/AuthorService.java
package timezra.hibernate.callbacks.service;

import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import timezra.hibernate.callbacks.dao.AuthorDAO;
import timezra.hibernate.callbacks.domain.Author;

@Service
public class AuthorService {

  @Resource
  
private AuthorDAO authorDAO;

  @Transactional
  
public void create(final Author a) {
    authorDAO.create(a);
  }

  @Transactional
  
public void update(final Author a) {
    authorDAO.update(a);
  }

  @Transactional
  
public void delete(final Author a) {
    authorDAO.delete(a);
  }

  @Transactional(propagation = Propagation.SUPPORTS)
  
public Author findByName(final String name) {
    
return authorDAO.findByName(name);
  }
}



Wire It Together With Spring:


We will use Spring to glue all our components together and to manage their lifecycles.
src/main/resources/applicationContext.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" xmlns:context="http://www.springframework.org/schema/context"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
>

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="org.h2.Driver" />
    <property name="url" value="jdbc:h2:test" />
    <property name="username" value="sa" />
    <property name="password" value="" />
  </bean>

  <bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="packagesToScan">
      <list>
        <value>timezra.hibernate.callbacks.domain</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <props>
        <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
        <prop key="hibernate.hbm2ddl.auto">update</prop>
      </props>
    </property>
  </bean>

  <bean id="transactionManager"
    class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
  </bean>

  <context:component-scan base-package="timezra.hibernate.callbacks" />
  <context:annotation-config />
  <tx:annotation-driven />
</beans>



Test The JPA Callbacks:


Finally we can test that the annotated methods are called at the expected times during Hibernate lifecycle events.
src/test/java/timezra/hibernate/callbacks/domain/AuthorTest.java
package timezra.hibernate.callbacks.domain;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.annotation.Resource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import timezra.hibernate.callbacks.service.AuthorService;

@RunWith(SpringJUnit4ClassRunner.
class)
@ContextConfiguration(
"/applicationContext.xml")
public class AuthorTest {

  @Resource
  
private AuthorService authorService;

  
private Author testAuthor;

  @Before
  
public void setup() {
    testAuthor = 
new Author();
    testAuthor.setName(
"timezra");
    authorService.create(testAuthor);
  }

  @After
  
public void tearDown() {
    authorService.delete(testAuthor);
  }

  @Test
  
public void theCreationDateIsSetAutomatically() {
    assertNotNull(testAuthor.getDateCreated());
  }

  @Test
  
public void theUpdatedDateIsSetAutomaticallyOnCreation() {
    assertEquals(testAuthor.getDateCreated(), testAuthor.getLastUpdated());
  }

  @Test
  
public void theUpdatedDateIsSetAutomaticallyOnUpdate() {
    testAuthor.setDateOfBirth(
new Date());

    authorService.update(testAuthor);

    assertTrue(testAuthor.getDateCreated().before(testAuthor.getLastUpdated()));
  }

  @Test
  
public void theAgeIsSetAutomaticallyWhenTheAuthorIsLoaded() {
    
final Integer expectedAge = 42;
    
final Calendar birthDate = Calendar.getInstance(Locale.getDefault());
    birthDate.add(Calendar.YEAR, -expectedAge);
    testAuthor.setDateOfBirth(birthDate.getTime());
    authorService.update(testAuthor);

    assertNull(testAuthor.getAge());
    assertEquals(expectedAge, authorService.findByName(testAuthor.getName()).getAge());
  }
}


We should expect all these tests to fail with a Hibernate validation Exception.
JUnit Test Failures for JPA Annotations

Update Project Dependencies:


Fortunately, the hibernate-entitymanager component provides a set of JPA lifecycle listeners that can be used independent of the Hibernate EntityManager, so we will update our project dependencies.
pom.xml
<project ....>
  ....
  <dependencies>
    ....
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.6.7.Final</version>
    </dependency>
    ....
  </dependencies>
</project>


NB: for this sample code, the version must be 3.6.x+ in order to access the ReflectionManager from the Hibernate configuration.

Register Hibernate Event Listeners:


We must manually tie the JPA lifecycle listeners to Hibernate events, much as the org.hibernate.ejb.EventListenerConfigurator#configure would if the Hibernate EntityManager were used.
src/main/java/timezra/hibernate/callbacks/domain/EntityCallbackHandlerInitializer.java
package timezra.hibernate.callbacks.dao;

import java.util.Iterator;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.cfg.Configuration;
import org.hibernate.ejb.event.EntityCallbackHandler;
import org.hibernate.mapping.PersistentClass;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;
import org.springframework.stereotype.Component;

@Component
public class EntityCallbackHandlerInitializer {

  @Resource
  
private AnnotationSessionFactoryBean annotationSessionFactory;

  @Resource
  
private EntityCallbackHandler entityCallbackHandler;

  @PostConstruct
  
public void init() throws ClassNotFoundException {
    
final Configuration configuration = annotationSessionFactory.getConfiguration();
    
final ReflectionManager reflectionManager = configuration.getReflectionManager();
    
final Iterator<PersistentClass> classMappings = configuration.getClassMappings();
    
while (classMappings.hasNext()) {
      entityCallbackHandler.add(
          reflectionManager.classForName(classMappings.next().getClassName(), 
this.getClass()),
          reflectionManager);
    }
  }
}



Finally, we will update the Hibernate configuration in the Spring context to use these event listeners. This configuration is based off a similar solution on the Spring Forum.
src/main/resources/applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans ....>
  ....
  <bean id="sessionFactory" ....>
    ....
    <property name="eventListeners">
      <map>
        <entry key="save" value-ref="saveEventListener" />
        <entry key="flush-entity" value-ref="flushEntityEventListener" />
        <entry key="post-load" value-ref="postLoadEventListener" />
      </map>
    </property>
  </bean>

  <bean id="saveEventListener" parent="callbackHandlerEventListener"
    class="org.hibernate.ejb.event.EJB3SaveEventListener" />
    
  <bean id="flushEntityEventListener" parent="callbackHandlerEventListener"
    class="org.hibernate.ejb.event.EJB3FlushEntityEventListener" />
    
  <bean id="postLoadEventListener" parent="callbackHandlerEventListener"
    class="org.hibernate.ejb.event.EJB3PostLoadEventListener" />

  <bean id="entityCallbackHandler" class="org.hibernate.ejb.event.EntityCallbackHandler" />

  <bean id="callbackHandlerEventListener" abstract="true"
    class="org.hibernate.ejb.event.CallbackHandlerConsumer">
    <property name="callbackHandler" ref="entityCallbackHandler" />
  </bean>
  ....
</beans>


NB: This configuration is for a subset of javax.persistence annotations and should be considered a starting point. It is up to the reader to configure Hibernate to use other other listeners in the org.hibernate.ejb.event package if the enablement of more JPA annotations is desired. For the purpose of this tutorial, the three listeners above are sufficient.

The test cases now succeed.

Conclusion:


The purpose of this tutorial has been to demonstrate that, with minimal code and configuration additional to what is necessary for a standard Maven/Spring/Hibernate project, Hibernate can detect JPA annotations and tie them to its persistence lifecycle events. Along the way, we have also developed a simple methodology for testing that JPA annotated methods are called when expected, and this methodology can be used to extend the sample code to cover the remaining javax.persistence annotations not specified in this tutorial.

Sample code based on this tutorial is also available.

3 comments:

Rajeev Shukla said...

Is it good to use @Transactional inside DAO ?

Meenakshi Shri said...

Hi,
Could you show how to do it with hibernate 5? In hibernate entity manager 5, The class org.hibernate.ejb.event.EntityCallbackHandler is removed.

Valtor said...

Here's how to do it in Hibernate 5: https://n1njahacks.wordpress.com/2016/10/07/jpa-callbacks-with-hibernates-sessionfactory-and-no-entitymanager/