Saturday, October 24, 2009

Reusable DAOs: CRUD operations with domain objects

(belongs to this series of articles)

Trying to create reusable DAO objects, following the article "Don't repeat the DAO" for the reusable domain objects proposed earlier in this series.

It is proposed to follow the recipe from the IBM site referred to above:

package com.example.crud.dao;

import java.io.Serializable;
import java.util.Collection;

import com.example.domain.DomainObject;

/**
 * Generic DAO interface - usable for CRUD and more.
 * 
 * Throws unchecked exceptions for any errors. The exception type depends on the
 * implementation.
 * 
 * @author maxim
 * 
 * @param <T>
 *            domain object type
 * @param <PK>
 *            primary key type
 */
public interface GenericDao<T extends DomainObject<PK>, PK extends Serializable> {

 /**
  * Persist the newInstance object into database
  **/
 void create(T newInstance);

 /**
  * Retrieve an object that was previously persisted to the database using
  * the indicated id as primary key
  */
 T findById(PK id);

 /**
  * 
  * @return all instances of T as a collection
  */
 Collection<T> findAll();

 /**
  * Main paging method
  * 
  * @param pageSize
  *            number of records (page size)
  * @param firstRecord
  *            start from this record (inclusive)
  * @param order
  *            field name to sort
  * @param asc
  *            sort ascending if true, descending otherwise
  * 
  * @return all instances that belong to the page
  */
 Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc);

 /**
  * Save changes made to a persistent object.
  * 
  * @return TODO
  */
 T update(T transientObject);

 /**
  * Remove an object from persistent storage in the database
  */
 void remove(T object);

 /**
  * Remove an object from persistent storage in the database by ID
  */
 void remove(PK id);

 /**
  * 
  * @return total number of entities in the DB
  */
 Long getTotalEntities();

 /**
  * Overload of "getPage" with a possibility to apply dynamic criteria.
  * Service layer could use this to implement find by related record, for
  * example
  * 
  * @see #getPage(int, int, String, boolean)
  * 
  * @param pageSize
  *            page size
  * @param firstRecord
  *            first record number, starts from 0
  * @param order
  *            sort order defined by field name, e.g. "id"
  * @param asc
  *            true for ascending sort
  * @param queryString
  *            string to add to query, e.g. "where o.id=?", or null
  * @param parameters
  *            objects to use the query with
  * @return up to <code>pageSize</code> of objects or empty collection
  */
 Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc, String queryString, Object... parameters);
}

And the JPA implementation of this:

/**
 * 
 */
package com.example.crud.dao.jpa;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;

import javax.persistence.EntityManager;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.example.crud.dao.GenericDao;
import com.example.domain.DomainObject;

/**
 * Implements {@link GenericDao} using JPA.
 * 
 * Works in managed environment using the injected {@link EntityManager}
 * 
 * @author maxim
 * 
 */
public class GenericDaoJpa<T extends DomainObject<PK>, PK extends Serializable>
  implements GenericDao<T, PK> {


 private static Log logger = LogFactory.getLog(GenericDaoJpa.class);

 
 @PersistenceContext
 public transient EntityManager em;

 public Class<T> type;

 @SuppressWarnings("unchecked")
 public GenericDaoJpa() {
  type = (Class<T>) ((ParameterizedType) getClass()
    .getGenericSuperclass()).getActualTypeArguments()[0];
 }

 /*
  * (non-Javadoc)
  * 
  * @see
  * com.example.prototype.persistence.GenericDao#create(java.lang.Object )
  */

 public void create(T newInstance) {
  em.persist(newInstance);
 }

 /*
  * (non-Javadoc)
  * 
  * @seecom.example.struts2.crud.persistence.GenericDao#findById(java.io.
  * Serializable)
  */

 public T findById(PK id) {
  T entity = em.find(type, id);
  postFindById(entity, em);
  return entity;
 }

 /**
  * Override in subclasses to do something here: this is where we could force
  * the loading of lazy loading collections, for example.
  * 
  * The rationale is that we typically need this for individual records, but
  * not for collections; so getPage() and the like return only eager-loading
  * properties, whereas findById() and the like should return fully loaded
  * objects.
  * 
  * @param entity
  *            the entity retrieved by find -- call methods on for lazy
  *            loading objects and collections to force loading
  * @param em
  */
 protected void postFindById(T entity, EntityManager em) {
 }

 /*
  * (non-Javadoc)
  * 
  * @see
  * com.example.prototype.persistence.GenericDao#remove(java.lang.Object )
  */

 public void remove(PK id) {
  remove(em.getReference(type, id));
 }

 /*
  * @see
  * com.example.prototype.persistence.GenericDao#remove(java.lang.Object
  */

 public void remove(T object) {
  em.remove(object);
 }

 /*
  * (non-Javadoc)
  * 
  * @see
  * com.example.prototype.persistence.GenericDao#update(java.lang.Object )
  */

 public T update(T transientObject) {
  try {
   return em.merge(transientObject);
  } catch (OptimisticLockException e) {
   logger.error("Record is modified by another process", e);
   throw new RuntimeException(
     "Error saving record: modified by another process");
  }
 }

 @Override
 public Collection<T> findAll() {
  Query query = em.createQuery("select o from " + type.getSimpleName()
    + " o");
  return findAll(query);
 }

 @SuppressWarnings("unchecked")
 protected Collection<T> findAll(Query q) {
  return q.getResultList();
 }

 @Override
 public Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc) {
  return getPage(pageSize, firstRecord, order, asc, null, (Object) null);
 }

 @SuppressWarnings("unchecked")
 public Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc, String queryString, Object... parameters) {
  final String whereString = queryString == null ? "" : queryString;
  final String ascending = asc ? "asc" : "desc";
  final String orderString = null == order || order.trim().equals("") ? ""
    : "order by " + order + " " + ascending;
  final Query q = em.createQuery("select o from " + type.getSimpleName()
    + " o " + whereString + " " + orderString);
  if (null != parameters && null != queryString) {
   for (int i = 0; i < parameters.length; i++) {
    q.setParameter(i + 1, parameters[i]);
   }
  }
  if (0 < pageSize) {
   q.setMaxResults(pageSize);
  }
  q.setFirstResult(firstRecord);
  return q.getResultList();
 }

 /**
  * 
  * @return the total number of entities
  */
 public Long getTotalEntities() {
  Query query = em.createQuery("select count(o) as count from "
    + type.getSimpleName() + " o");
  return (Long) query.getSingleResult();
 }

 public EntityManager getEm() {
  return em;
 }

 public void setEm(EntityManager em) {
  this.em = em;
 }

}

The only original twist in the above is the "postFindById()" method. It is a hack for something that would be normally out of scope for this article -- the problem of lazy loading collections in an entity object that is used outside of the transaction scope. This is the reason for the widespread error: "org.hibernate.LazyInitializationException: failed to lazily initialize a collection ... no session or session was closed" that would happen in the following widespread case:

(1) display a list of objects
(2) display the details of one of them, part of those details being lazy-loading collections of dependent objects

If the object is a JPA/Hibernate entity, then the ORM session would indeed be closed between (1) and (2). There are many ways to code around this, but one that I find the most generic is to make DAO methods that retrieve the entity by ID (and by definition work with a single entity, as opposed to a list) force the loading of lazy-loading collections. Which collections need to be loaded can't be determined in the generic DAO, hence the delegating of this to subclasses using an abstract method.

No comments: