2013年1月21日 星期一

Hibernate Generic Paging DAO

在頁面上處理lazy loading分頁資料不外乎要計算:
  1. 總筆數
  2. 依每頁顯示筆數計算總頁數
  3. 計算要顯示頁數第一筆資料的offset
如果我們仔細分析,會發現事實上取得資料總筆數和撈取某一頁所需的資料,這兩者的SQL statement在where condition應是一模一樣的,根據這一點我們可以design出paging專用的DAO。

以下會包含兩大部分,一是generic paging DAO的設計,二是test case。

Generic Paging DAO:
  1. GenericPagingDao<Entity extends BaseDto, Condition>
  2. AbstractGenericPagingDaoImpl<Entity extends BaseDto, Condition>
  3. HibernateGenericPagingDao<Entity extends BaseDto, Condition>
  4. AbstractHibernateGenericPagingDaoImpl<Entity extends BaseDto, Condition>
  5. BaseDto
GenericPagingDao<Entity extends BaseDto, Condition>
package com.gss.gmo.cao.data;

import java.util.List;

import com.gss.gmo.cao.base.BaseDto;

/**
 * @author linus_chien
 *
 * @param <Entity>
 * @param <Condition>
 */
public interface GenericPagingDao<Entity extends BaseDto, Condition> {

    Class<Entity> getEntityClass();

    List<Entity> findPagingResults(Condition condition, int firstResult, int maxResults);

    <ResultClass> List<ResultClass> findPagingTransformResults(Condition condition, int firstResult, int maxResults, Class<ResultClass> resultClass);

    long getRowCount(Condition condition);

}
AbstractGenericPagingDaoImpl<Entity extends BaseDto, Condition>
package com.gss.gmo.cao.data.impl;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.data.GenericPagingDao;

/**
 * @author linus_chien
 *
 * @param <Entity>
 * @param <Condition>
 */
public abstract class AbstractGenericPagingDaoImpl<Entity extends BaseDto, Condition> implements GenericPagingDao<Entity, Condition> {

    /**
     * Class of entity.
     */
    private final Class<Entity> entityClass;

    /**
     * Init entityClass by generic type.
     */
    @SuppressWarnings("unchecked")
    public AbstractGenericPagingDaoImpl() {
        Type t = getClass().getGenericSuperclass();
        ParameterizedType pt = (ParameterizedType) t;
        entityClass = (Class<Entity>) pt.getActualTypeArguments()[0];
    }

    @Override
    public final Class<Entity> getEntityClass() {
        return entityClass;
    }

}
HibernateGenericPagingDao<Entity extends BaseDto, Condition>
package com.gss.gmo.cao.hibernate;

import org.hibernate.SessionFactory;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.data.GenericPagingDao;

/**
 * @author linus_chien
 *
 * @param <Entity>
 * @param <Condition>
 */
public interface HibernateGenericPagingDao<Entity extends BaseDto, Condition> extends GenericPagingDao<Entity, Condition> {

    /**
     * @param sessionFactory
     */
    void setSessionFactory(SessionFactory sessionFactory);

}
AbstractHibernateGenericPagingDaoImpl<Entity extends BaseDto, Condition>
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Projections;
import org.hibernate.transform.AliasToBeanResultTransformer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.data.impl.AbstractGenericPagingDaoImpl;
import com.gss.gmo.cao.hibernate.HibernateGenericPagingDao;

/**
 * @author linus_chien
 *
 * @param <Entity>
 * @param <Condition>
 */
public abstract class AbstractHibernateGenericPagingDaoImpl<Entity extends BaseDto, Condition> extends AbstractGenericPagingDaoImpl<Entity, Condition>
        implements HibernateGenericPagingDao<Entity, Condition> {

    /**
     * SessionFactory instance.
     */
    private SessionFactory sessionFactory;

    @Autowired
    @Required
    @Override
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    /**
     * @return Current session.
     */
    protected Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @Override
    public List<Entity> findPagingResults(final Condition condition, final int firstResult, final int maxResults) {
        return findPagingTransformResults(condition, firstResult, maxResults, getEntityClass());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <ResultClass> List<ResultClass> findPagingTransformResults(Condition condition, int firstResult, int maxResults, Class<ResultClass> resultClass) {
        Criteria criteria = getCurrentSession().createCriteria(getEntityClass(), getEntityClass().getSimpleName().toLowerCase());
        initCriteria(condition, criteria);
        exclusiveForPagingResults(condition, criteria);
        criteria.setFirstResult(firstResult);
        criteria.setMaxResults(maxResults);
        if (getEntityClass() != resultClass) {
            criteria.setResultTransformer(new AliasToBeanResultTransformer(resultClass));
        }
        return criteria.list();
    }

    @Override
    public long getRowCount(final Condition condition) {
        Criteria criteria = getCurrentSession().createCriteria(getEntityClass());
        initCriteria(condition, criteria);
        criteria.setProjection(Projections.rowCount());
        return (Long) criteria.uniqueResult();
    }

    /**
     * Setup criteria by generic Condition type.
     *
     * @param condition
     * @param criteria
     */
    protected abstract void initCriteria(Condition condition, Criteria criteria);

    /**
     * Execute this method only for findPagingResults. Do nothing by default.
     *
     * @param condition
     * @param criteria
     */
    protected void exclusiveForPagingResults(Condition condition, Criteria criteria) {
    }

}
BaseDto
package com.gss.gmo.cao.base;

import java.io.Serializable;

/**
 * Force DTO to implement equals(), hashCode(), toString().
 *
 * @author linus_chien
 *
 */
public abstract class BaseDto implements Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public abstract int hashCode();

    @Override
    public abstract boolean equals(Object obj);

    @Override
    public abstract String toString();

}
Test Case:
  1. Person
  2. PersonPagingDao
  3. PersonPagingDaoImpl
  4. AbstractHibernateGenericPagingDaoImplTest
  5. AbstractHibernateGenericPagingDaoImplTest-context.xml
Person
package com.gss.gmo.cao.hibernate.impl;

import java.util.List;

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

import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Type;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;

/**
 * @author linus_chien
 *
 */
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
@Filter(name = "personFilter", condition = "name like :name and address is null")
public class Person extends BaseDto {

    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private String address;

    @Embedded
    private AuditInfo auditInfo;

    @Type(type = "listType")
    private List<String> emails;

}
PersonPagingDao
package com.gss.gmo.cao.hibernate;

import com.gss.gmo.cao.hibernate.impl.Person;

public interface PersonPagingDao extends HibernateGenericPagingDao<Person, String> {

}
PersonPagingDaoImpl
package com.gss.gmo.cao.hibernate.impl;

import org.hibernate.Criteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.stereotype.Component;

import com.gss.gmo.cao.hibernate.PersonPagingDao;

@Component
public class PersonPagingDaoImpl extends AbstractHibernateGenericPagingDaoImpl<Person, String> implements PersonPagingDao {

    @Override
    protected void initCriteria(String condition, Criteria criteria) {
        criteria.add(Restrictions.eq("name", condition));
    }

    @Override
    protected void exclusiveForPagingResults(String condition, Criteria criteria) {
        criteria.addOrder(Order.desc("name"));
    }

}
AbstractHibernateGenericPagingDaoImplTest
package com.gss.gmo.cao.hibernate.impl;

import static org.junit.Assert.assertEquals;

import java.util.List;

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 com.gss.gmo.cao.hibernate.PersonDao;
import com.gss.gmo.cao.hibernate.PersonPagingDao;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TransactionConfiguration(defaultRollback = false)
@Transactional
public class AbstractHibernateGenericPagingDaoImplTest {

    @Autowired
    private PersonDao personDao;

    @Autowired
    private PersonPagingDao personPagingDao;

    @Test
    public void testFindPagingResults() {
        Person person = new Person();
        person.setId("A");
        person.setName("Linus");
        personDao.save(person);

        person = new Person();
        person.setId("B");
        person.setName("Linus");
        personDao.save(person);

        person = new Person();
        person.setId("C");
        person.setName("Linus");
        personDao.save(person);

        List<Person> persons = personPagingDao.findPagingResults("Linus", 1, 2);
        assertEquals(2, persons.size());

        persons = personPagingDao.findPagingResults("A", 1, 2);
        assertEquals(0, persons.size());
    }

    @Test
    public void testGetRowCount() {
        assertEquals(3, personPagingDao.getRowCount("Linus"));
        assertEquals(0, personPagingDao.getRowCount("A"));
    }

    @Test
    public void testFindPagingTransformResults() {
        List<PersonWrapper> personWrappers = personPagingDao.findPagingTransformResults("Linus", 1, 2, PersonWrapper.class);
        assertEquals(2, personWrappers.size());
        for (PersonWrapper personWrapper : personWrappers) {
            assertEquals("Linus", personWrapper.getPerson().getName());
        }
    }

}
AbstractHibernateGenericPagingDaoImplTest-context.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.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config />
    <context:component-scan base-package="com.gss.gmo.cao.hibernate.impl" />
    <tx:annotation-driven transaction-manager="transactionManager" />

    <!-- Datasource -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver">
        </property>
        <property name="url" value="jdbc:hsqldb:mem:ExampleTest">
        </property>
        <property name="username" value="sa">
        </property>
        <property name="password" value="">
        </property>
    </bean>

    <!-- Hibernate session factory -->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource">
            <ref bean="dataSource" />
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
                <prop key="hibernate.hbm2ddl.auto">create</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
            </props>
        </property>
        <property name="packagesToScan" value="com.gss.gmo.cao.hibernate.impl" />
        <property name="annotatedPackages" value="com.gss.gmo.cao.hibernate.impl" />
    </bean>

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

</beans>
Test case裡面的PersonDao只是一般CRUD的DAO,這裡就不浪費版面多做贅述。