Using Hibernate Events with PersistenceEventListener

23 / Jul / 2016 by Sandeep Poonia 1 comments

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:

[code lang=”groovy”]
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
}
}
[/code]

And to register the listener from the plugin do:

[code lang=”groovy”]
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)
}
}
[/code]

Another limitation with Audit Logging Plugin is that it works on PreUpdate and not on PostUpdate which may be required in some cases.

FOUND THIS USEFUL? SHARE IT

comments (1 “Using Hibernate Events with PersistenceEventListener”)

  1. Antonia Engfors

    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!

    Reply

Leave a Reply

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