Search This Blog

Saturday 30 June 2012

Intercepting Events in Hibernate

Much of what Hibernate does, happens in the background and the application is not aware of the same. For example dirty checking of entities and scheduling sql update queries for the modified objects is something that Hibernate manages on its own.This is very good in a way, because it removes the amount of code we need to write.
This reduced awareness of Hibernate actions may be a problem however if we would like to perform certain operations in a very generic manner. For example auditing. In certain cases we need to maintain a log of the modifications that are performed on a certain table. One way to do it would be creating an audit record every time we update the entity. This would require us to mange the code, add insert statements all over the place and be sure to enforce that all developers to do the same.
However Hibernate provides us with an alternative trick. It allows us to plug into Hibernate's  core processing and be aware of its behind the scene activities. For logging, we could plugin into Hibernate's update mechanism and add our audit code there. This ensures that auditing happens behind the scenes and the burden of managing it is removed from individual developers.
The way to plugging in is the org.Hibernate.Interceptor interface. To go through the methods available I decided to create my own Dummy Interceptor implementation
public class CustomInterceptor extends EmptyInterceptor {

    private static final Logger logger = Logger
            .getLogger(CustomInterceptor.class);
As the Hibernate Interceptor interface includes a whole set of methods, most of which we may not be concerned with intercepting, Hibernate provides the dummy class org.hibernate.EmptyInterceptor which basically does nothing. We can extend this class and override the necessary methods only.
public void onDelete(Object entity, Serializable id, Object[] state,
        String[] propertyNames, Type[] types) {
    // Called before an object is deleted. It is not recommended that the
    // interceptor modify the state.
    logger.info("onDelete: Attempting to delete an object " + entity + " with id "
            + id);
    final int length = state.length;
    logger.info("onDelete: Object Details are as below: ");
    for (int i = 0; i < length; i++) {
        logger.info("onDelete: propertyName : " + propertyNames[i] 
                + " ,type :  " + types[i] 
                + " ,state : " + state[i]);
    }
}
Overriding this method allows us to plugin to the delete mechanism. The method receives the Entity being deleted, its identifier and arrays of details regarding the object. Similar intercept methods include:
public boolean onFlushDirty(Object entity, Serializable id,
            Object[] currentState, Object[] previousState,
        String[] propertyNames, Type[] types) {
    logger.info("onFlushDirty: Detected dirty object " + entity + " with id " + id);
    final int length = currentState.length;
    logger.info("onFlushDirty: Object Details are as below: ");
    for (int i = 0; i < length; i++) {
        logger.info("onFlushDirty: propertyName : " + propertyNames[i] 
                + " ,type :  " + types[i] 
                + " , previous state : " + previousState[i]
                + " , current state : " + currentState[i]);
    }
    return false;//as no change made to object here
}

public boolean onLoad(Object entity, Serializable id, Object[] state,
        String[] propertyNames, Type[] types) {
    logger.info("onLoad: Attempting to load an object " + entity + " with id "
            + id);
    final int length = state.length;
    logger.info("onLoad: Object Details are as below: ");
    for (int i = 0; i < length; i++) {
        logger.info("onLoad: propertyName : " + propertyNames[i] 
                + " ,type :  " + types[i] 
                + " ,state : " + state[i]);
    }
    return false;
}
public boolean onSave(Object entity, Serializable id, Object[] state,
        String[] propertyNames, Type[] types) {
    logger.info("onSave: Saving object " + entity + " with id " + id);
    final int length = state.length;
    logger.info("onSave: Object Details are as below: ");
    for (int i = 0; i < length; i++) {
        logger.info("onSave: propertyName : " + propertyNames[i] 
            + " ,type :  " + types[i] 
            + " , state : " + state[i]);
    }
    return false;//as no change made to object here
}
The onFlushDirty method is called when an object is detected to be dirty, during a flush. The interceptor may modify the detected currentState of the entity, which will be then be propagated to both the database and the persistent object. 
Note that not all flushes end in actual synchronization with the database, in which case the new currentState will be propagated to the object, but not necessarily (immediately) to the database. It is strongly recommended that the interceptor not modify the previousState of the entity.
The above warning is from the Hibernate source code and should be followed. I mean who knows what the code does better than the guys who wrote it ;) Similar statements found, have been placed below in italics
The onLoad method is called just before an object is initialized. The interceptor may change the state, which will be propagated to the persistent object. Note that when this method is called, entity will be an empty uninitialized instance of the class.
The onSave method is called before an object is saved. The interceptor may modify the state, which will be used for the SQL INSERT and propagated to the persistent object.
public void postFlush(Iterator entities) {
    logger.info("postFlush: List of objects that have been flushed... ");
    int i =0;
    while (entities.hasNext()) {
        Object element = (Object) entities.next();
        logger.info("postFlush: " + (++i) + " : " + element);
    }
}
public void preFlush( Iterator entities) {
    logger.info("preFlush: List of objects to flush... ");
    int i =0;
    while (entities.hasNext()) {
        Object element = (Object) entities.next();
        logger.info("preFlush: " + (++i) + " : " + element);
    }
}
public Boolean isTransient(Object entity) {
    logger.info("isTransient: Checking object for Transient state... " + entity);
    return null;
}

public Object instantiate(String entityName, EntityMode entityMode,
        Serializable id) {
    logger.info("instantiate: Instantiating object " + entityName + 
        " with id - " + id + " in mode " + entityMode);
        return null;
}

public int[] findDirty(Object entity, Serializable id,
        Object[] currentState, Object[] previousState,
        String[] propertyNames, Type[] types) {
    logger.info("findDirty: Detects if object is dirty " + entity + " with id " + id);
    final int length = currentState.length;
    logger.info("findDirty: Object Details are as below: ");
    for (int i = 0; i < length; i++) {
        logger.info("findDirty: propertyName : " + propertyNames[i] 
                + " ,type :  " + types[i] 
                + " , previous state : " + previousState[i]
                + " , current state : " + currentState[i]);
    }
    return null;
}
The postFlush method is  called after a flush that actually ends in execution of the SQL statements required to synchronize in-memory state with the database.
The preFlush method on the other hand is called before a flush occurs.
The isTransient method is called to distinguish between transient and detached entities. The return value determines the state of the entity with respect to the current session. A true indicates the entity is transient and false that the the entity is detached. 
The instantiate method is used to instantiate the entity class. It returns null to indicate that Hibernate should use the default constructor of the class. The identifier property of the returned instance should be initialized with the given identifier. 
The findDirty method is called by Hibernate from flush method. Its return value determines whether the entity is updated. It returns an array of property indices indicating the entity is dirty. An empty array indicates that object has not been modified.
public String getEntityName(Object entity) {
    logger.info("getEntityName: name for entity " + entity); 
    return null;
}

public Object getEntity(String entityName, Serializable id) {
    logger.info("getEntity: Returns fully loaded cached entity with name  " + entityName + " and id " + id);
    return null;
}
The getEntityName method gets the entity name for a persistent or transient instance. The getEntity method get a fully loaded entity instance that is cached externally
public void afterTransactionBegin(Transaction tx) {
    logger.info("afterTransactionBegin: Called for transaction " + tx);
}

public void afterTransactionCompletion(Transaction tx) {
    logger.info("afterTransactionCompletion: Called for transaction " + tx);
}

public void beforeTransactionCompletion(Transaction tx) {
    logger.info("beforeTransactionCompletion: Called for transaction " + tx);
}
The above methods are a part of Hibernate's transaction mechanism.They are called before/ after transaction completion and after transaction beginning.
public String onPrepareStatement(String sql) {
    logger.info("onPrepareStatement: Called for statement " + sql);
    return sql;
}

public void onCollectionRemove(Object collection, Serializable key)
        throws CallbackException {
    logger.info("onCollectionRemove: Removed object with key " + key
            + " from collection " + collection);
}

public void onCollectionRecreate(Object collection, Serializable key)
        throws CallbackException {
    logger.info("onCollectionRemove: Recreated collection " + collection + " for key " + key);
}

public void onCollectionUpdate(Object collection, Serializable key)
        throws CallbackException {
    logger.info("onCollectionUpdate: Updated collection " + collection + " for key " + key);
}
The onPrepareStatement method is called when sql queries are to be executed. The other methods are called when collections are fetched/updated/deleted.
To use the interceptor we need to assign it to a session when we open it.
public static void main(String[] args) {
    Configuration configuration = new Configuration();
    configuration = configuration.configure();
    sessionFactory = configuration.buildSessionFactory();
    final CustomInterceptor customInterceptor = new CustomInterceptor();
    final Session session = 
             sessionFactory.openSession(customInterceptor);
}
The interceptors are active with the session now. Now to test our interceptor.

1 comment:

  1. Good article.
    Will the Tx interceptor work for each object being committed in a transaction or just the last object? If I save object A, update object B, delete object C, save object C etc.. will it run the methods for each object?

    ReplyDelete