Capturing Property Changes with Spring, JPA and Hibernate

It's breathtaking!

Sometimes you need to capture changes to entities just before they get saved to the database. In some systems, a change to a person’s bank account or tax liability may trigger a lot of processing.
While you could be tracking these via DTOs – the process can be quite repetitive and error-prone. You could also use database triggers for that, but you shouldn’t really. Even if the entire extra-processing is inside your database, your application will have no visibility of that.

Hibernate in its grandness, provides us with a nice way to attach an implementation of org.hibernate.Interceptor to our Hibernate Session or SessionFactory. Through this interceptor, your application is able to hook into specific points in your entity’s lifecycle and do what it needs to do.

Implementation

The code below creates a change interceptor, and lays the foundation for creating specific entity listeners – to be fired when the interceptor catches changes for the entities you are interested in (e.g. you may be interested in changes to properties of your Employee entities, but not care about the Company or Pet changes).

//PropertyChangeInterceptor.java
package com.duckranger.changes.interceptor;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import com.duckranger.domain.Employee;
import com.duckranger.domain.listener.EmployeeChangeListener;
import com.duckranger.domain.listener.PropertyChangeListener;

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;

/**
 * Intercept changes on entities
 * 
 * @author Nimo Naamani
 *
 */
public class PropertyChangeInterceptor extends EmptyInterceptor {

  private static final long serialVersionUID = 1L;
  private Map<Class, PropertyChangeListener<?>> listeners;

  @SuppressWarnings("unchecked")
  @Override
  public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames,
    Type[] types) {

    PropertyChangeListener listener = listeners.get(entity.getClass());
    
    //Only check for changes if an entity-specific listener was registered
    if (listener != null) {
      boolean report = false;
      for (int i = 0; i < currentState.length; i++) {
        if (currentState[i] == null) {
          if (previousState[i] != null) {
            report = true;
          }
        } else if (!currentState[i].equals(previousState[i])) {
          report = true;
        }

        if (report) {
          listener.onChange(previousState[i], currentState[i], propertyNames[i]);
          report = false;
        }
      }
    }
    return false;
  }

  public PropertyChangeInterceptor() {
    listeners = new HashMap<Class, PropertyChangeListener<?>>();
    listeners.put(Employee.class, new EmployeeChangeListener());
  }
}
  1. The only callback method we care about here is the onFlushDirty() which gets called when an entity is found to be dirty at flush time. This means that there were changes made to the entity – the changes we might be interested in.
  2. Lines 29-44: Loop through the entity's properties to find out whether a change was made. Note that Hibernate provides us with both previous and current state of the entity, and this way we are able to find out all changed properties.
  3. Lines 39-42: If a change was indeed encountered – pass the change details to the listener. Note that in this implementation, the listener's method is going to be called for every single property change. If this is not what you’re after – you might want to change the code a bit.
  4. Lines 48-51: Register listeners in the constructor. In our case – since I am only interested in changes to Employee – I only register an EmployeeChangeListener

The code for the PropertyChangeListener is quite simple too:

//PropertyChangeListener.java
package com.duckranger.domain.listener;

/**
 * Entity property change listener
 *
 * @author Nimo Naamani
 *
 * @param <T> - the class this listener listens on
 */
public interface PropertyChangeListener<T> {

  /**
   * Used to do something on change. Note that the type of values is inferred by <E>
   * 
   * @param propertyName - the property name on the entity
   * @param entity - the entity object
   * @param oldVal - the old value
   * @param newVal - the new value
   */
  <E> void onChange(E oldVal, E newVal, String propertyName);
}

This is the entire interface. We use because we don't know what object we are going to get (On the Interceptor – we receive arrays of Object).

When the listener's interface is sorted, the implementation is quite trivial. Here, for example, is the EmployeeChangeListener implementation:

package com.duckranger.domain.listener;

import com.duckranger.domain.Employee;
/**
 * Listen for changes on the Employee entity 
 * 
 * @author Nimo Naamani
 *
 */
public class EmployeeChangeListener implements PropertyChangeListener<Employee> {

  @Override
  public <E> void onChange(E oldVal, E newVal, String propertyName) {
    System.out.println("property:" + propertyName);
    System.out.println("old  val:" + oldVal);
    System.out.println("new  val:" + newVal);
  }
}

Obviously, this listener doesn't do much, as this will be implementation-specific. Regardless – this works.

How to integrate with Spring

If you use Spring and Hibernate normally – there shouldn’t be an issue – that is, providing you use the right mix of Spring/Hibernate versions (See issue SPR-8940).
However – what happens when you are using Hibernate as a JPA provider? In this case, there is nowhere to register the Interceptor – as this is not a JPA construct.

A workaround for this would be to override HibernatePersistence itself, as described right here

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>