Using Hibernate Events with PersistenceEventListener
In my last blog, we discussed how to hook into GORM API to add some common custom functionality. We will refer the same problem that we discussed in my last blog. Here is the problem statement:
In my grails plugin I was needed to add some fields that were common to a set of domains. For eg: for some domains we wanted to store fields like createdBy and lastUpdatedBy to keep track of users who created and last updated each record in that domain.
We also discussed that a plugin named Audit Logging Plugin
already exist for the same purpose. The reason behind not using the audit plugin is that the updates made by this plugin to update createdBy and lastUpdatedBy fields is done via a separate query. So how does this affects us? If we have auto versioning enabled in our Grails application, then this will cause increment in version of the respective domain object. And if you are doing some operation in a separate thread upon insertion/updation of the domain objects then you might encounter StaleObjectStateException
. In our case, on each insert/update we were sending a message to Rabbit MQ queue which was being consumed by a separate application.
So how to handle this:
So instead of using the default grails events we used hibernate events to update the stamping fields. Here is the code to do this:
package com.verecloud.nimbus4.listener import groovy.util.logging.Commons import org.apache.commons.lang.ArrayUtils import org.grails.datastore.mapping.core.Datastore import org.grails.datastore.mapping.engine.event.* import org.hibernate.event.PreInsertEvent as HibernatePreInsertEvent import org.hibernate.event.PreUpdateEvent as HibernatePreUpdateEvent import org.hibernate.persister.entity.EntityPersister import org.springframework.context.ApplicationEvent @Commons class CustomPersistenceEventListenerImpl extends AbstractPersistenceEventListener { //Name of Stamp fields String stampCreatedBy = "createdBy" String stampLastUpdatedBy = "lastUpdatedBy" CustomPersistenceEventListenerImpl(Datastore datastore) { super(datastore) } @Override protected void onPersistenceEvent(AbstractPersistenceEvent event) { if (event.source != this.datastore) { log.trace("Event received for other datastore. Ignoring event") return } //Do stamping //event.nativeEvent will give us the actual HibernateEvent stamp(event.nativeEvent, event.eventType) } @Override //This listener will listen PreInsert and PreUpdate events only. boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { return eventType.isAssignableFrom(PreInsertEvent) || eventType.isAssignableFrom(PreUpdateEvent) } private stamp(Serializable event, EventType eventType) { if (EventType.PreInsert == eventType) { HibernatePreInsertEvent preInsertEvent = event as HibernatePreInsertEvent stampCreatedBy(preInsertEvent.entity, preInsertEvent.persister, preInsertEvent.state) stampLastUpdatedBy(preInsertEvent.entity, preInsertEvent.persister, preInsertEvent.state) } else if (EventType.PreUpdate == eventType) { //On update we need to update lastUpdatedBy field only HibernatePreUpdateEvent preUpdateEvent = event as HibernatePreUpdateEvent stampLastUpdatedBy(preUpdateEvent.entity, preUpdateEvent.persister, preUpdateEvent.state) } } private void stampCreatedBy(entity, EntityPersister persister, Object[] state) { if (entity.getProperty(stampCreatedBy) == null) { String currentUser = getActor() String[] propertyNames = persister.entityMetamodel.propertyNames // inserts entity.setProperty(stampCreatedBy, currentUser) setValue(state, propertyNames, stampCreatedBy, currentUser, entity) } } private void stampLastUpdatedBy(entity, EntityPersister persister, Object[] state) { String currentUser = getActor() String[] propertyNames = persister.entityMetamodel.propertyNames // inserts setValue(state, propertyNames, stampCreatedBy, entity.getProperty(stampCreatedBy), entity) // updates entity.setProperty(stampLastUpdatedBy, currentUser) setValue(state, propertyNames, stampLastUpdatedBy, currentUser, entity) } //Using this method we can directly set a value for a property which will update in the same query private void setValue(Object[] state, String[] properties, String propertyToSet, Object value, Object entity) { int index = ArrayUtils.indexOf(properties, propertyToSet) if (index >= 0) { state[index] = value } else { log.error("Field '" + propertyToSet + "' not found on entity '" + entity.getClass().getName() + "'.") } } private String getActor() { String actor = "Sandeep Poonia" //fetch the user who is currently logged in return actor } }
And to register the listener from the plugin do:
def doWithApplicationContext = { ctx -> registerAuditLogListener(ctx, application) } private void registerAuditLogListener(AbstractApplicationContext applicationContext, GrailsApplication application) { application.mainContext.eventTriggeringInterceptor.datastores.each { key, datastore -> CustomPersistenceEventListenerImpl listener = new CustomPersistenceEventListenerImpl(datastore) applicationContext.addApplicationListener(listener) } }
Another limitation with Audit Logging Plugin
is that it works on PreUpdate and not on PostUpdate which may be required in some cases.
Thank you for this great article!
a few notes:
The import needs to import from org.hibernate.event.spi
import org.hibernate.event.spi.PreInsertEvent as HibernatePreInsertEvent
import org.hibernate.event.spi.PreUpdateEvent as HibernatePreUpdateEvent
furthermore, the variables stampCreatedBy and stampLastUpdatedBy should be declared as final statics and renamed.
lastly, you need to implement a check for “hasProperty” otherwise you might get an exception here or there for the getProperty() calls.
if(!entity.hasProperty(STAMP_CREATED_BY)){
return
}
Thank you so much for this article!