Platinum Partner
java,sql,groovy,hibernate,persistence,data access

Using a Hibernate Interceptor To Set Audit Trail Properties

In almost every application I've done, the database tables have some kind of audit trail fields. Sometimes this is a separate "audit log" table where all inserts, updates, deletes, and possibly even queries are logged. Other times there are the four typical audit trail fields in each table, for example you might have created_by, created_on, updated_by, and updated_on fields in each table. The goal in the latter case is to update those four fields with the appropriate information as to who created or updated a record and when they did it. Using a simple Hibernate Interceptor this can be accomplished with no changes to your application code (with several assumptions which I'll detail next). In other words, you won't need to and definitely should not be manually setting those audit properties littered around your application code.

The basic assumptions I'll make for this simple audit interceptor are that: (1) model objects contain the four audit properties mentioned above, and (2) there is an easy way to obtain the current user's information from anywhere in the code. The first assumption is needed since you need some way to identify which properties constitute the audit trail properties. The second assumption is required because you need some way to obtain the credentials of the person making the change in order to set the createdBy or updatedBy property in your Hibernate Interceptor class.

So, for reference purposes, assume you have a (Groovy) base entity like this with the four audit properties:

@MappedSuperclassclass BaseEntity implements Serializable {  String createdBy  Date createdOn  String updatedBy  Date updatedOn}

I'm using the Hibernate ImprovedNamingStrategy so that camel case names are translated to underscored names, e.g. "createdBy" becomes "created_by". Next assume there is a BlogEntry entity class that extends BaseEntity and inherits the audit trail properties:

@Entityclass BlogEntry extends BaseEntity {  @Id @GeneratedValue (strategy = GenerationType.IDENTITY)  Long id  @Version  Long version  String title  @Column (name = "entry_text")  String text  @Temporal (TemporalType.TIMESTAMP)  Date publishedOn}

To implement the interceptor, we need to implement the aforementioned Interceptor interface. We could do this directly, but it is better to extend EmptyInterceptor so we need only implement the methods we actually care about. Without further ado, here's the implementation (excluding package declaration and imports):

class AuditTrailInterceptor extends EmptyInterceptor {  boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,                      Object[] previousState, String[] propertyNames,                      Type[] types) {    setValue(currentState, propertyNames, "updatedBy", UserUtils.getCurrentUsername())    setValue(currentState, propertyNames, "updatedOn", new Date())    true  }  boolean onSave(Object entity, Serializable id, Object[] state,                 String[] propertyNames, Type[] types) {    setValue(state, propertyNames, "createdBy", UserUtils.getCurrentUsername())    setValue(state, propertyNames, "createdOn", new Date())    true  }  private void setValue(Object[] currentState, String[] propertyNames,                        String propertyToSet, Object value) {    def index = propertyNames.toList().indexOf(propertyToSet)    if (index >= 0) {      currentState[index] = value    }  }}

So what did we do? First, we implemented the onFlushDirty and onSave methods because they are called for SQL updates and inserts, respectively. For example, when a new entity is first saved, the onSave method is called, at which point we want to set the createdBy and properties. And if an existing entity is updated, onFlushDirty is called and we set the updatedBy and updatedOn.

Second, we are using the setValue helper method to do the real work. Specfically, the only way to modify the state in a Hibernate Interceptor (that I am aware of anyway) is to dig into the currentState array and change the appropriate value. In order to do that, you first need to trawl through the propertyNames array to find the index of the property you are trying to set. For example, if you are updating a blog entry you need to set the updatedBy and updatedOn properties within the currentState array. For a BlogEntry object, the currentState array might look like this before the update (the updated by and on propertes are both null in this case because the entity was created by Bob but has not been updated yet):

{   "Bob",   2008-08-27 10:57:19.0,   null,    null,    2008-08-27 10:57:19.0,    "Lorem ipsum...",   "My First Blog Entry",   0}

You then need to look at the propertyNames array to provide context for what the above data represents:

{  "createdBy",  "createdOn",  "updatedBy",  "updatedOn",  "publishedOn",  "text",  "title",  "version"}

So in the above updatedBy is at index 2 and updatedOn is located at index 3. setValue() works by finding the index of the property it needs to set, e.g. "updatedBy," and if the property was found, it changes the value at that index in the currentState array. So for updatedBy at index 2, the following is the equivalent code if we had actually hardcoded the implementation to always expect the audit fields as the first four properties (which is obviously not a great idea):

// Equivalent hard-coded code to change "updatedBy" in above example// Don't use in production!currentState[2] = UserUtils.getCurrentUsername()

To actually make your interceptor do something, you need to enable it on the Hibernate Session. You can do this in one of several ways. If you are using plain Hibernate (i.e. not with Spring or another framework) you can set the interceptor globally on the SessionFactory, or you can enable it for each Session as in the following example code:

// Configure interceptor globally (applies to all Sessions)sessionFactory =  new AnnotationConfiguration()    .configure()    .setNamingStrategy(ImprovedNamingStrategy.INSTANCE)    .setInterceptor(new AuditTrailInterceptor())    .buildSessionFactory()// Enable per SessionSession session = getSessionFactory().openSession(new AuditTrailInterceptor())

If you enable the interceptor globally, it must be thread-safe. If you are using Spring you can easily configure a global interceptor on your session factory bean:

<bean id="sessionFactory"      class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">  <property name="entityInterceptor">    <bean class="com.nearinfinity.hibernate.interceptor.AuditTrailInterceptor"/>  </property>  <!-- additional Hibernate configuration properties --></bean>

On the other hand, if you would rather enable the interceptor per session, you either need to use the openSession(Interceptor) method to open your sessions or alternatively implement your own version of CurrentSessionContext to use the getCurrentSession() method in order to set the interceptor. Using getCurrentSession() is preferable anyway since it allows several different classes (e.g. DAOs) to use the same session without needing to explicitly pass the Session object around to each object that needs it.

At this point we're done. But, if you know about the Hibernate eventing system (e.g. you can listen for events such as inserts and updates and define event listener classes to respond to those events), you might be wondering why I didn't use that mechanism rather than the Interceptor. The reason is that, to the best of my current knowledge, you cannot alter state of objects in event listeners. So for example you would not be able to change an entity's state in a PreInsertEventListener implementation class. If anyone knows this is incorrect or has implemented it, I'd love to hear about it. Until next time, happy auditing!

Originally posted on Scott Leberknight's blog

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}