Using Hibernate Events with PersistenceEventListener

23 / Jul / 2016 by Sandeep Poonia 0 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:

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.

FOUND THIS USEFUL? SHARE IT

Leave a comment -