Hibernate Automatic Dirty Check for Detached Objects

by Yagish Sharma on August 26, 2010

Per the Hibernate documentation, Hibernate can perform dirty checks only when the objects are loaded and changed in the scope of a single Session. This means we cannot use detached objects, but we must keep our session open for one conversation (multiple http requests) using Managed Sessions, just to get hibernate’s dirty check. This introduces new challenges of storing the Hibernate Session object in HttpSession, which fails if we deploy in a clustered environment, since most of the fields in Hibernate org.hibernate.impl.SessionImpl class are transient. So, how do we get the benefit of using Detached Objects and also get Hibernate’s automatic dirty checking?

The following is a brief background of how Hibernate does its dirty checking:

Hibernate Session contains a PersistenceContext object, which maintains a cache of all the objects read from the database as a Map. In the same Session, when I read an object from the db and make changes to it, Hibernate compares the objects and triggers the updates when the session is flushed. Every object in the PersistenceContext is called Persistent object. Once the session closes, the PersistenceContext is lost and so is the cached copy. A detached object, when saved, opens a new session containing an empty PersistenceContext, so there is nothing to compare against for the dirty check.

If we have an original cached copy of the Detached Object (a clone saved in HttpSession) and we are able to place that copy in the PersistenceContext somehow, we can get the dirty check to work. Here is how to do it.

Couple of points here, before we start reading the code -

1)  All your domain models have a generic way to expose their primary key. Here I have defined PrimaryKeyAwareDomainModelObject interface, which exposes a method to access the primary key property.

public interface PrimaryKeyAwareDomainModelObject {
public Integer getPrimaryKey();
}

2) You already have the original cached copy of the Detached Object, and have made changes to service layers to pass it to the DAO layer.

3) Also, your Detached Objects are POJO’s.

4) You are using Spring HibnerateDaoSupport, else you can get the SessionFactory to access the Hibernate Session.



/*** This class handles adding the cached Detached Object to PersistenceContext of a new Session.

** 1) Get a new session, or the session associated with this transaction from the
* sessionFactory.

*2) Inside the PersistentContext of the session, add the old
* Model object, so hibernate thinks that it was loaded as part of read
* operation in the same session

*3) Copy the changed values from the new Model
* object to the old Model object, since hibernate does identity check on the
* object in the persistenceContext, passing a new Model directly to hibernate
* will not trigger dirty checking, but create issues.

*4) Call usual saveOrUpdate, which will check the object if its dirty before update.
***/

import org.hibernate.EntityMode;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.EntityKey;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.engine.Status;
import org.hibernate.impl.SessionImpl;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class HibBeforeUpdateListener<T extends PrimaryKeyAwareDomainModelObject, Q extends HibernateDaoSupport> {

/***
* Before doing a call on saveOrUpdate on hibernate session - fetch the
* session - get the PersistenceContext, and add the oldModelObj - copy the
* modified fields from new modelObj to oldModelObj.
* You can pass SessionFactory directly instead of HibernateDaoSupport
*/
@SuppressWarnings("unchecked")

public T beforeUpdate(T modelObj, T oldModelObj, Q dataAccessor) {

SessionFactory factory = dataAccessor.getSessionFactory();
Session session = factory.getCurrentSession();
// get the PersistenceContext of this session
PersistenceContext persistenceContext = session instanceof SessionImpl ? ((SessionImpl) session).getPersistenceContext(): null;
if (null != persistenceContext) {
addEntityToPersistenceContext(persistenceContext, oldModelObj,(SessionFactoryImplementor) factory);
}
// copy modified values from newModel to oldModel

// I am using Dozer

return (T) DozerUtility.copy(modelObj, oldModelObj);
}

private EntityKey generateKey(T modelObj, SessionFactoryImplementor sessionFactory) {
// EntityKey is the key used in the map of objects stored in
// PersistenceContext  it requires the primary key, defined by the @Id annotation in the model class
// , persister object which is SingleTableEntityPersister class in my case, and our models type, which is POJO
return new EntityKey(modelObj.getPrimaryKey(), sessionFactory.getEntityPersister(modelObj.getClass().getCanonicalName()),EntityMode.POJO);
}

/***
 * Hibernate stores the entities as an EntityEntry inside a IdentityMap, ie.
 * 2 objects are equal, if they reference the same object, rather than
 * Object.equals and hashcode is equal. EntityEntry contains the original
 * object as loaded from the db, plus also its original state as an Object
 * array, called loadedState. This method mimics the same behavior of
 * hibernate internals, and stores the object and its loadedState.
 *
 * @param context
 * @param oldModelObj
 * @param sessionFactory
 */
 private void addEntityToPersistenceContext(PersistenceContext context, T oldModelObj, SessionFactoryImplementor sessionFactory) {
       Map<String, T> allEntitiesMap = new HashMap<String, T>();
       for(T modelObj : getAllEntityObjects(oldModelObj, allEntitiesMap).values()){
                 context.addEntity(modelObj, Status.MANAGED, sessionFactory.getEntityPersister(modelObj.getClass().getCanonicalName())
                                 .getPropertyValues(modelObj, EntityMode.POJO), generateKey(
                                  modelObj, sessionFactory), null, LockMode.READ, true,
                                  sessionFactory.getEntityPersister(modelObj.getClass().getCanonicalName()), false, true);
     }
 }

 /***
 * Make a list of all sub Entity objects instances inside the object,
 * and add them separately
 * @return
 */
 private Map<String, T> getAllEntityObjects(T object, Map<String, T> allEntityObjects){
   // definitely this object will be added to persistence context
   if(!allEntityObjects.containsKey(object.getClass().getCanonicalName())){
            allEntityObjects.put(object.getClass().getCanonicalName(), (T)object);
    }
   // get all the instance variables and check their annotations
   Field[] fields = object.getClass().getDeclaredFields();
   // if no fields (impossible scenario), then just add the object and send it back
    if(fields.length == 0){
     return allEntityObjects;
   }
   for(Field f : fields){
    Class fieldClass = f.getType();
    if(fieldClass.getAnnotation(javax.persistence.Entity.class) != null){
     try{
         f.setAccessible(true);
         Object fieldValue = f.get(object);
        // only add the domain model objects
         if(fieldValue instanceof PrimaryKeyAwareDomainModelObject && !allEntityObjects.containsKey(fieldClass.getCanonicalName())){
            allEntityObjects.put(fieldClass.getCanonicalName(), (T)fieldValue);
            getAllEntityObjects((T)fieldValue, allEntityObjects);
         }
      }catch(IllegalAccessException ex){
        // do nothing and move to next field
      }
    }
  }
    return allEntityObjects;
 }
}

And here is how to use it in a DAO -


public class MyDataAccessorImpl extends
HibernateDaoSupport{

public void update(T modelObj, T oldModelObj) {
PrimaryKeyAwareDomainModelObject modelObjToUpdate = new HibBeforeUpdateListener().beforeUpdate(modelObj, oldModelObj, this);
getHibernateTemplate().saveOrUpdate(modelObjToUpdate);
}

}

This approach was the most efficient that used Hibernate’s own public APIs. If anyone else has a similar approach to the same problem, I would love to hear…

{ 2 comments… read them below or add one }

tmillhouse August 26, 2010 at 6:23 pm

This is a great overview of implementing dirty checking across session boundaries in hibernate. I know many people are aware of filters such as Spring’s OpenSessionInViewFilter, but that is only good across a single request. This approach outlined above goes above and beyond, and it really has many uses!

Makarska October 1, 2010 at 2:13 pm

Thanks for this great post. The info I have gained from your blog is truly encouraging

Leave a Comment

Previous post:

Next post: