Sunday, November 21, 2010

Hibernate : Pre Database Opertaion Event Listeners



Hibernate provides two (logical) ways of interfering with the operations performed on entities. It is either through interceptors and/or its the event handling mechanism. Just as a point, as it initially sounds event handling mechanism of hibernate is not asynchronous, rather it is synchronous and same as interceptors. Interceptors are internally used by the default event listeners. Event Listener is a modern and basic infrastructure of hibernate used by the interceptors too.

The differences are not much and most of the common needs could be full filled by either using any one of them or a combination of both interceptors or event listeners. Its not actually quite clear when one should use which mechansim. I would personally suggest to stick with Event Listeners as they are much more flexible and robust. This is a very broad area of discussion and here we focus our discussion only on some specific types of event listeners ( pre database operations ).

Hibernate provides an event handling framework for various kind of methods on the session interface. Whenever an operation is performed through the session, an event is generated and a corresponding listener is evoked to take some action on the generated event. These events could be events on entity, like on load, pre load, pre insert, post insert etc. (Look at the subclasses of AbstractEvent class to get a complete list of all events). These events could be divided into two basic categories, pre database operations and post database operations.

For me pre database operation events were of major interest because I needed a general mechanism to stamp ,update and insert user and time, on all my persistent objects.

All the events are subclasses of org.hibernate.event.AbstractEvent. The parent of all pre database operation events is the class AbstractPreDatabaseOperation. This has three children PreUpdateEvent, PreInsertEvent and PreDeleteEvent. Here after, we will be focusing our discussion on the usage of PreUpdate and PreInsert Events.

Given : The problem was to add insert and update user and timestamp information to all of my entities.

Approach 1 :

____________________________________________________________________


public class MyPreInsertEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreInsert(PreInsertEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               if (entity.getInsertUser() == null) {
               String currentUser = UtilityClass.currentUser();
               Date currentTime = new Date();
               // inserts
               entity.setInsertUser(currentUser);
               entity.setInsertDateTime(dayTime);
               }
          }
          return false;
     }
}

public class MyPreUpdateEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreUpdate(PreUpdateEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               String currentUser = UtilityClass.currentUser();
               Date currentTime = new Date();
               // updates
               entity.setUpdateUser(currentUser);
               entity.setUpdateDateTime(currentTime);
          }
          return false;
     }
}

____________________________________________________________________

Now when the entity object inside the event is updated in the pre listener, the expectation is that the updated information is persisted in the database too. But this does not happen and for a given entity you would see these fields being null (or unchanged) on the database. So where is the problem ?

The idea of the pre database operation event listeners was probably not to change the entities itself, but to perform some operations around the state of the entitiy, just prior to its insert or update into the database. For e.g. you could get the name of the class of an entity and decide whether the given user could update this entity or not and you could throw an exception on an illegal access. The idea is to save the state of the entity at the time of the commit and not take into account any of the changes made to this entity object after the commit (for e.g. here we set insert and update information of entities in our listeners).

These pre events contain variety of information around the entity. The ones in which we are interested are listed below.

1. The pre events have a object array called 'state'. These are values of all the attributes of a given entity at the time of calling commit.
2. The entity object itself.
3. The entity persister used by the hibernate to perform operations on the given entity.
4. The event source (which is the assosiated session in the current context).

At this stage, the entity object assosiated with the event could be thought of as a detached object. Any changes made to this object will never be reflected on the database. The state that will finally be reflected in the database after the commit is contained in the 'state' object array.

In order to commit any changes to the entity object, you could get hold of the session object and use it to save changes to the database. This could be done as follows :

Approach 2.
____________________________________________________________________


public class MyPreUpdateEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreUpdate(PreUpdateEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               String currentUser = UtilityClass.currentUser();
               Date currentTime = new Date();
               // updates
               entity.setUpdateUser(currentUser);
               entity.setUpdateDateTime(currentTime);
               Transaction txn = event.getSession().beginTransaction();
               event.getSession().save(entity);
               txn.commit();

          }
          return false;
     }
}

____________________________________________________________________

I read this solution in some thread on hibernate forum. This approach may work in many cases but it may fail in equal number of cases (or more). This could also result into recursive calls to this onPreUpdate method and thus resulting into stackoverflow exception. I won't be discussing this issue over here. This would be a working but not a clean and standard solution. Personally I would not recommend this approach until I know all possible side effects the different possible scenarios could cause.

In order to make changes to entities, the right approach would be to make changes to the object array 'state' present in the associated event. The 'state' is an object array containing values of attributes of an entitiy and hence it would be difficult to know and replace the correct value. But fortunately these values are in a particular order and this order does not change.

The entity persister has an entitymodel which contains a lot of information about the entity. For e.g. it contains an array called propertyNames. This array has the propertyNames in the same order as the values of the properties present in the 'state' array in event. Hence our modified code would look like :

Approach 3.
____________________________________________________________________


public class MyPreInsertEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreInsert(PreInsertEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               if (entity.getInsertUser() == null) {
                    String currentUser = UtilityClass.currentUser();
                    Date currentTime = new Date();
                    String[] propertyNames = event.getPersister().getEntityMetamodel.getPropertyNames();
                    Object[] state = event.getState();

                    // inserts
                    setValue(state, propertyNames, "insertUser", currentUser, entity);
                    setValue(state, propertyNames, "insertTime", currentTime, entity);
               }
          }
          return false;
     }
}

public class MyPreUpdateEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreUpdate(PreUpdateEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               String currentUser = UtilityClass.currentUser();
               Date currentTime = new Date();
               String[] propertyNames = event.getPersister().getEntityMetamodel().getPropertyNames();
               Object[] state = event.getState();

               // updates
               setValue(state, propertyNames, "updateUser", currentUser, entity);
               setValue(state, propertyNames, "updateTime", currentTime, entity);
          }
     return false;
     }
}

A common method in both the classes.

void setValue(Object[] currentState, String[] propertyNames, String propertyToSet, Object value, Object entity) {
     int index = ArrayUtils.indexOf(propertyNames, propertyToSet);
     if (index >= 0) {
          currentState[index] = value;
          } else {
               Log.error("Field '" + propertyToSet + "' not found on entity '" + entity.getClass().getName() + "'.");
          }
}

____________________________________________________________________

This solution will work in most of the cases, however there is still one case which is left out and will fail. Here is the trick described below :

Hibernate generates a prepared statement and fills in the parameters from the 'state' array present in the event. Hence any changes made to the this 'state' array are reflected in the sql statement generated by the hibernate and finally on the database. The insert and update events have a different copy of this states array.

The pre insert listener is called before the pre update event (if an insert as well as update happens). This happens when an entity is created, persisted and then modified in the same transaction. This will result into two seperate sql statements, first will be an insert statement and second one will be an update statement, on the same entitiy. With the insert statement as we set only the insertUser and insertTime in our PreInsertEventListener and not updateUser and updateTime. The generated statement will look like

insert into entity (id, .... , insert_user, insert_time, update_user, update_time) values (1, .... 'test', '21.11.2010 16:10:00', null, null)

with the PreUpdateEventListener the update sql generated will be like

update entity set id=1 .... , insert_user=null, insert_time=null, update_user='test', update_time='21.11.2010 16:10:00'

These two sqls will be generated in the same transaction and one after the other. The effect will be that the update sql will override the values in the insert sql and hence insert user and time will always be null in such cases. In order to avoid this, I modified the code as follows :

Approach 4.
____________________________________________________________________


public class MyPreInsertEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreInsert(PreInsertEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               if (entity.getInsertUser() == null) {
                    String currentUser = UtilityClass.currentUser();
                    Date currentTime = new Date();
                    String[] propertyNames = event.getPersister().getEntityMetamodel().getPropertyNames();
                    Object[] state = event.getState();
                    // inserts
                    entity.setInsertUser(currentUser);
                    entity.setInsertDateTime(dayTime);
                    setValue(state, propertyNames, "insertUser", currentUser, entity);
                    setValue(state, propertyNames, "insertTime", currentTime, entity);
               }
          }
          return false;
     }
}

public class MyPreUpdateEventListener implements PreInsertEventListener {
     @Override
     public boolean onPreUpdate(PreUpdateEvent event) {
          Object object = event.getEntity();
          if (object instanceof Auditable) {
               Auditable entity = (Auditable) object;
               String currentUser = UtilityClass.currentUser();
               Date currentTime = new Date();
               String[] propertyNames = event.getPersister().getEntityMetamodel().getPropertyNames();
               Object[] state = event.getState();
               // inserts
               setValue(state, propertyNames, "insertUser", entity.getInsertUser(), entity);
               setValue(state, propertyNames, "insertTime", entity.getInsertDateTime(), entity);

               // updates
               entity.setUpdateUser(currentUser);
               entity.setUpdateDateTime(currentTime);
               setValue(state, propertyNames, "updateUser", currentUser, entity);
               setValue(state, propertyNames, "updateTime", currentTime, entity);
          }
          return false;
     }
}

____________________________________________________________________

The entity object is common and shared between the two events (insert and update). However, each event has its own copy of 'state' array. Here I use this fact in my favour to pass in the insert information between the two event listeners through the entity object itself. Hence in the update event listener I do reset the insert information passed in from the insert event listeners and hence the generated sqls from the two listeners would look like :

insert into entity (id, .... , insert_user, insert_time, update_user, update_time) values (1, .... 'test', '21.11.2010 16:10:00', null, null)

update entity set id=1 .... , insert_user=test, insert_time='21.11.2010 16:10:00', update_user='test', update_time='21.11.2010 16:10:00'

This solution works fine and I have'nt seen yet, any problem or case which could not have been handled. However I would like to conclude by saying not to use Event listeners for any custom auditing. Envers comes bundled up with Hibernate 3.5.6 and beyond. Envers is an excellent framework for auditing and I have been using it successfully.

149 comments: