February 18, 2015

Hibernate Best Practise, JPA 2.0 and Second Level Cache with Infinispan

Introduction

The technique of using ORM has been becoming the defacto standard, when developing new application, but after passed the level of school books example, the usage of using ORM is not so simple. In this blog I will talk about minimizing code for simple CRUD actions, but also arguing about getting back to basic for complex side of ORM.

Before started, one must recognised that the number one implementation of ORM is hibernate and that is the implementation framework used here for the specification JPA 2.0 contained in EE 6.

ORM Best Practise

DRY AbstractDomain

All domain object contain some common characteristic, e.g. primary key, toString method, maybe created and last modified date time, etc. Put all those things in a abstract domain class

package se.magnuskkarlsson.example.jpa;

import java.lang.reflect.Field;

@javax.persistence.Cacheable(true)
@javax.persistence.MappedSuperclass
public abstract class AbstractDomain implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    @javax.persistence.Id
    @javax.persistence.GeneratedValue(strategy = javax.persistence.GenerationType.AUTO)
    private Long id;

    // ----------------------- Logic Methods -----------------------

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(this.getClass().getSimpleName());
        sb.append("@").append(hashCode());
        sb.append("[");
        toFieldString(sb, this.getClass().getSuperclass().getDeclaredFields());
        toFieldString(sb, this.getClass().getDeclaredFields());
        sb.append("]");
        return sb.toString();
    }

    // ----------------------- Helper Methods -----------------------

    protected void toFieldString(StringBuilder sb, Field[] fields) {
        for (Field field : fields) {
            if (field.isAnnotationPresent(javax.persistence.Id.class)
                    || field.isAnnotationPresent(javax.persistence.Column.class)) {

                try {
                    String name = field.getName();
                    field.setAccessible(true);
                    Object value = field.get(this);
                    sb.append(name).append("='").append(value).append("', ");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // ----------------------- Get and Set Methods -----------------------

    public Long getId() {
        return id;
    }
}

Note about abstract domain class.

  • Every domain class will be serializable.
  • We add a toString method, which is realized with reflection. Reflection is not the fastest technique, which is OK for a toString method, since it should not be called often.
  • We add javax.persistence.Cacheable(true), for preparing for entity second level cache.

Next is to create a concrete domain class.

package se.magnuskkarlsson.example.jpa;

import java.util.ArrayList;
import java.util.List;

@javax.persistence.Cacheable(true)
@javax.persistence.Entity
@javax.persistence.AttributeOverride(name = "id", column = @javax.persistence.Column(name = "companyId"))
public class Company extends AbstractDomain {

    private static final long serialVersionUID = 1L;

    @javax.persistence.Column(unique = false, nullable = false, length = 255)
    private String name;

    @javax.persistence.Transient
    private List<Employee> employees;

    // ----------------------- Logic Methods -----------------------

    // ----------------------- Helper Methods -----------------------

    // ----------------------- Get and Set Methods -----------------------

    public String getName() {
        return name;
    }

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

    public List<Employee> getEmployees() {
        if (employees == null) {
            employees = new ArrayList<Employee>();
        }
        return employees;
    }

    public void setEmployees(List<Employee> employees) {
        this.employees = employees;
    }
}

Note about concrete domain class.

  • We make use of javax.persistence.AttributeOverride to specialize a primary key for concrete domain class.
  • WE DO NOT MAP RELATIONSHIP WITH ORM. Se below.

KISS. Back to basic for relationship.

Not using ORM for our relationship, might sound crazy at first, but the fact is that managing relationship with ORM is hard and comes with several not obvious side effects.

So what went wrong? Lets start from the beginning. First we mapped all our relationship with ORM. Next we was challenged with how do we load all relationship. You might then started with loading all relationship eagerly. But after testing your implementation with more production like data volume, you realize that you are loading big chunk of your database in memory. This does not hold. OK, you comes to your senses and add lazy loading to all your relationship.

So what happened next? You started to optimize all your queries so you did not need to load each sub children separately. You probably now came across exception like org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags. And problem like loading loading N number of extra data. Or why do you need to load all children for inserting a new child to a parent?

When reaching this point you start to doubt the entire usage and justification of ORM. And that in fact is where I somewhere also landed. So lets get rid of all complexity it means of handling relationship with ORM and get back to basic - KISS.

DRY DomainDAO

We extract common CRUD task to a DAO base class.

package se.magnuskkarlsson.example.jpa;

import java.util.List;

import javax.persistence.TypedQuery;

public class DomainDAO<D extends AbstractDomain> {

    @javax.persistence.PersistenceContext
    protected javax.persistence.EntityManager em;

    // ----------------------- Logic Methods -----------------------

    public D create(D domain) {
        em.persist(domain);
        return domain;
    }

    public D get(Class<D> clazz, long id) {
        return em.find(clazz, id);
    }

    public List<D> getAll(Class<D> clazz) {
        String sql = "FROM " + clazz.getSimpleName();
        TypedQuery<D> query = em.createQuery(sql, clazz);
        // JPA 2.0 Cache Query hints
        // Use the object/data if already in the cache
        query.setHint("javax.persistence.cache.retrieveMode",
                javax.persistence.CacheRetrieveMode.USE);
        // Cache the objects/data returned from the query.
        query.setHint("javax.persistence.cache.storeMode",
                javax.persistence.CacheStoreMode.USE);
        return query.getResultList();
    }

    public D update(D domain) {
        return em.merge(domain);
    }

    public D delete(Class<D> clazz, long id) {
        D domain = em.find(clazz, id);
        if (domain != null) {
            em.remove(domain);
        }
        return domain;
    }

    public boolean contains(Class<D> clazz, long id) {
        return em.getEntityManagerFactory().getCache().contains(clazz, id);
    }

    public void evict(Class<D> clazz, long id) {
        em.getEntityManagerFactory().getCache().evict(clazz, id);
    }

    // ----------------------- Helper Methods -----------------------

    // ----------------------- Get and Set Methods -----------------------

}

Deployment Descriptor

src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">

    <persistence-unit name="example-jpa" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <jta-data-source>java:jboss/datasources/ExampleJPADS</jta-data-source>
        <class>se.magnuskkarlsson.example.jpa.Company</class>
        <class>se.magnuskkarlsson.example.jpa.Employee</class>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <validation-mode>CALLBACK</validation-mode>
        <properties>
            <!-- <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" /> -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />

            <property name="hibernate.cache.use_second_level_cache" value="true" />
            <property name="hibernate.cache.use_query_cache" value="true" />
            <property name="hibernate.cache.region.factory_class" value="org.jboss.as.jpa.hibernate4.infinispan.InfinispanRegionFactory" />
            <property name="hibernate.cache.infinispan.cachemanager" value="java:jboss/infinispan/container/hibernate" />
            <property name="hibernate.transaction.manager_lookup_class" value="org.hibernate.transaction.JBossTransactionManagerLookup" />

            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="false" />
            <property name="hibernate.generate_statistics" value="true" />
            <property name="hibernate.cache.infinispan.statistics" value="true" />
        </properties>
    </persistence-unit>
</persistence>

Reference

Red Hat JBoss EAP Migration Guide

Infinispan User Guide

Wikibook Java Persistence

Test

src/test/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">

    <persistence-unit name="example-jpa-test" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>se.magnuskkarlsson.example.jpa.Company</class>
        <class>se.magnuskkarlsson.example.jpa.Employee</class>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <validation-mode>CALLBACK</validation-mode>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.connection.driver_class" value="org.h2.Driver" />
            <property name="hibernate.connection.url" value="jdbc:h2:mem:" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />

            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="false" />
            <property name="hibernate.generate_statistics" value="true" />
        </properties>
    </persistence-unit>
</persistence>

src/test/resources/log4j.properties

# configure the root logger
log4j.rootLogger=INFO, STDOUT

# configure the console appender
log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.Target=System.out
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] %c:%L - %m%n

### https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch03.html#configuration-logging
# Log all hibernate SQL DML statements as they are executed
log4j.logger.org.hibernate.SQL=TRACE
# Log all hibernate JDBC parameters
log4j.logger.org.hibernate.type=TRACE
# Log all hibernate second-level cache activity
log4j.logger.org.hibernate.cache=TRACE

src/test/java/se/magnuskkarlsson/example/jpa/DomainDAOTest.java

package se.magnuskkarlsson.example.jpa;

import javax.persistence.Persistence;

import junit.framework.Assert;

import org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

public class DomainDAOTest {

    private static final DomainDAO<Company> dao = new DomainDAO<Company>();

    private static final JBossStandaloneJTAManagerLookup lookup = new JBossStandaloneJTAManagerLookup();

    @BeforeClass
    public static void oneTimeSetUp() throws Exception {
        dao.em = Persistence.createEntityManagerFactory("example-jpa-test")
                .createEntityManager();
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void testCRUD() throws Exception {
        dao.em.getTransaction().begin();

        Company company = new Company();
        company.setName("Foo");
        Company create = dao.create(company);
        Assert.assertNotNull(create);
        Assert.assertNotNull(create.getId());
        System.out.println("### " + create.toString());

        Company get1 = dao.get(Company.class, create.getId());
        Assert.assertNotNull(get1);
        Assert.assertEquals(create.getId(), get1.getId());
        Assert.assertEquals("Foo", get1.getName());

        get1.setName("Bar");
        Company update = dao.update(get1);
        Assert.assertNotNull(update);
        Assert.assertEquals(create.getId(), update.getId());
        Assert.assertEquals("Bar", update.getName());

        Company get2 = dao.get(Company.class, create.getId());
        Assert.assertNotNull(get2);
        Assert.assertEquals(create.getId(), get2.getId());
        Assert.assertEquals("Bar", get2.getName());

        Company delete = dao.delete(Company.class, create.getId());
        Assert.assertNotNull(delete);
        System.out.println("### " + delete);

        Company get3 = dao.get(Company.class, create.getId());
        Assert.assertNull(get3);

        dao.em.getTransaction().commit();
    }

    @Test
    public void testCache() throws Exception {
        dao.em.getTransaction().begin();

        Company company = new Company();
        company.setName("Cache");
        Company create = dao.create(company);
        Assert.assertNotNull(create);
        Assert.assertNotNull(create.getId());
        System.out.println("*** create " + create.toString());

        dao.em.getTransaction().commit();

        dao.em.getTransaction().begin();

        Company get1 = dao.get(Company.class, create.getId());
        Assert.assertNotNull(get1);
        Assert.assertEquals(create.getId(), get1.getId());
        Assert.assertEquals("Cache", get1.getName());
        System.out.println("*** get1 " + get1.toString());

        dao.em.getTransaction().commit();

        dao.em.getTransaction().begin();

        Company get2 = dao.get(Company.class, create.getId());
        Assert.assertNotNull(get2);
        Assert.assertEquals(create.getId(), get2.getId());
        Assert.assertEquals("Cache", get2.getName());
        System.out.println("*** get2 " + get2.toString());

        dao.em.getTransaction().commit();

        System.out.println("!! " + dao.contains(Company.class, create.getId()));
    }
}

Reference

Oracle Adam Bien Integration Testing for Java EE

OpenEJB Example

H2 Database Engine Cheat Sheet

Web Application Test

The testing of Second Level Cache with Infispan is not easily done outside the container. So lets write a simple REST service which we can simply test with e.g. REST client - Google Code

package se.magnuskkarlsson.example.jpa;

import java.util.List;

import org.apache.log4j.Logger;

@javax.ejb.Stateless
@javax.ws.rs.Path("/company")
public class CompanyBean {

    private static final Logger log = Logger.getLogger(CompanyBean.class);

    @javax.inject.Inject
    private DomainDAO<Company> dao;

    // String body: {"name":"Company FOO"}
    @javax.ws.rs.POST
    @javax.ws.rs.Consumes({ "application/json; charset=UTF-8" })
    @javax.ws.rs.Produces({ "application/json; charset=UTF-8" })
    public Company create(Company company) {
        log.info("Enter create(Company) " + company);
        Company entity = dao.create(company);
        log.info("Exit create(Company) " + entity);
        return entity;
    }

    @javax.ws.rs.GET
    @javax.ws.rs.Path("/{id}")
    @javax.ws.rs.Produces({ "application/json; charset=UTF-8" })
    public Company get(@javax.ws.rs.PathParam("id") long id) {
        log.info("Enter get(long) " + id);
        Company entity = dao.get(Company.class, id);
        log.info("Exit get(long) " + entity);
        return entity;
    }

    @javax.ws.rs.GET
    @javax.ws.rs.Produces({ "application/json; charset=UTF-8" })
    public List<Company> getAll() {
        log.info("Enter getAll() ");
        List<Company> entities = dao.getAll(Company.class);
        log.info("Exit get(long) " + entities);
        return entities;
    }

    @javax.ws.rs.GET
    @javax.ws.rs.Path("/contains/{id}")
    @javax.ws.rs.Produces({ "application/json; charset=UTF-8" })
    public boolean contains(@javax.ws.rs.PathParam("id") long id) {
        log.info("Enter contains(long) " + id);
        boolean rtn = dao.contains(Company.class, id);
        log.info("Exit contains(long) " + rtn);
        return rtn;
    }

    @javax.ws.rs.PUT
    @javax.ws.rs.Consumes({ "application/json; charset=UTF-8" })
    @javax.ws.rs.Produces({ "application/json; charset=UTF-8" })
    public Company update(Company company) {
        log.info("Enter update(Company) " + company);
        Company entity = dao.update(company);
        log.info("Exit update(Company) " + entity);
        return entity;
    }

    @javax.ws.rs.DELETE
    @javax.ws.rs.Path("/{id}")
    @javax.ws.rs.Produces({ "application/json; charset=UTF-8" })
    public Company delete(@javax.ws.rs.PathParam("id") long id) {
        log.info("Enter delete(Company) " + id);
        Company entity = dao.delete(Company.class, id);
        log.info("Exit delete(Company) " + entity);
        return entity;
    }
}

And the web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <display-name>Example JPA</display-name>

    <!-- Auto scan REST service -->
    <context-param>
        <param-name>resteasy.scan</param-name>
        <param-value>true</param-value>
    </context-param>

    <!-- this need same with resteasy servlet url-pattern -->
    <context-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/rest</param-value>
    </context-param>

    <listener>
        <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
    </listener>

    <servlet>
        <servlet-name>resteasy-servlet</servlet-name>
        <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>resteasy-servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

To make this work on JBoss 7 and later we need to add src/main/webapp/WEB-INF/jboss-deployment-structure.xml.

<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
    <deployment>
        <dependencies>
            <module name="org.apache.log4j" />
            <module name="org.hibernate" />
            <module name="org.infinispan" />
        </dependencies>
    </deployment>
</jboss-deployment-structure>

And to make CDI work we add src/main/webapp/WEB-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">

</beans>

Finally deploy it on JBoss and play around and watch log with below extra logging in JBoss.

            <!-- Log all hibernate SQL DML statements as they are executed -->
            <logger category="org.hibernate.SQL">
                <level name="TRACE"/>
            </logger>
            <!-- Log all hibernate JDBC parameters -->
            <logger category="org.hibernate.type.descriptor.sql">
                <level name="TRACE"/>
            </logger>
            <!-- Log all hibernate second-level cache activity -->
            <logger category="org.hibernate.cache">
                <level name="TRACE"/>
            </logger>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>se.magnuskkarlsson.examples</groupId>
    <artifactId>example-jpa</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>Example JPA</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.build.outputEncoding>UTF-8</project.build.outputEncoding>
        <hibernate-version>4.2.14.Final</hibernate-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <scope>provided</scope>
        </dependency>

        <!-- Test Support -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.185</version>
            <scope>test</scope>
        </dependency>

        <!-- <dependency> <groupId>org.apache.derby</groupId> <artifactId>derbyclient</artifactId> 
            <version>10.7.1.1</version> <scope>test</scope> </dependency> -->

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>${hibernate-version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-infinispan</artifactId>
            <version>${hibernate-version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>4.3.1.Final</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                    <debug>true</debug>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-ejb-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <ejbVersion>3.1</ejbVersion>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

No comments: