SQL Zone is brought to you in partnership with:

Scott is a Senior Software Architect at Altamira Corporation. He has been developing enterprise and web applications for over 15 years professionally, and has developed applications using Java, Ruby/Rails, Groovy/Grails and Python. His main areas of interest include object-oriented design, system architecture, testing, and frameworks of all types including Spring, Hibernate, Ruby on Rails, Grails, and Django. In addition, Scott enjoys learning new languages to make himself a better and more well-rounded developer a la The Pragmatic Programmers' advice to "learn one language per year." Scott is a DZone MVB and is not an employee of DZone and has posted 43 posts at DZone. You can read more from them at their website. View Full User Profile

Using a Hibernate Interceptor To Set Audit Trail Properties

08.27.2008
| 81647 views |
  • submit to reddit

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:

@MappedSuperclass
class 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:

@Entity
class 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 Session
Session 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

Published at DZone with permission of Scott Leberknight, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Blake Minghelli replied on Thu, 2008/08/28 - 11:48am

We implemented a similar interceptor-based approach.  But one requirement we had is that not all entities should be auditable.  So a couple of changes we did are:

  • Encapsulated the audit trail fields into a component AuditTrail object
  • Created an Auditable interface to declare which entities support auditing
  • Created a resuable XML fragment for the AuditTrail mapping that all auditable entities can include in their mapping file (we don't use annotations for hibernate mappings)

This approach lets us decide which entities need auditing support at a granular level.  The disadvantage is that we must add an AuditTrail instance to all auditable entities. Although you could still put the AuditTrail in a base class that others extend if you don't need that flexibility.

The AuditTrail component.  All entities that need audit support should include this component:

public class AuditTrail{
private Date createDate;
private Long createAgentId;
private Date lastModifiedDate;
private Long lastModifiedAgentId;
...
}

The Auditable interface:

public interface Auditable
{
public AuditTrail getAuditTrail();
}

An auditable entity:

public class Foo implements Auditable
{
...
public AuditTrail getAuditTrail()
{
return auditTrail;
}
...
}

 Reusable XML mapping for the AuditTrail component (AuditTrailComponent.xml):

<?xml version="1.0" encoding="UTF-8"?>
<component name="auditTrail" access="field">
<property name="createDate" column="create_date" type="timestamp" />
<property name="createAgentId" column="create_agent_id" type="long" />
<property name="lastModifiedDate" column="last_modified_date" type="timestamp" />
<property name="lastModifiedAgentId" column="last_modified_agent_id" type="long" />
</component>

 Include the above XML fragment into your entity mapping document:

<?xml version="1.0" encoding='UTF-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"
[<!ENTITY auditTrail SYSTEM "classpath://com/vegas/hibernate/mapping/AuditTrailComponent.xml">]>
<hibernate-mapping package="...">

<class name="Foo" table="foo">
...
&auditTrail; <!-- Note the ENTITY declared in the DOCTYPE -->
</class>
</hibernate-mapping>

 Finally, our AuditInterceptor is pretty much the same as above but we check first that the entity is an instance of Auditable:

public class AuditInterceptor extends EmptyInterceptor
{
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames,
Type[] types)
{
if (entity instanceof Auditable)
{
AuditTrail auditTrail = getAuditTrail(state);
auditTrail.setCreateDate(new Date());
auditTrail.setCreateAgentId(getAgentIdFromContext());
return true;
}
return false;
}

/**
* Called by hibernate right before updating an entity in the database
*/
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
Object[] previousState, String[] propertyNames, Type[] types)
{
if (entity instanceof Auditable)
{
AuditTrail auditTrail = getAuditTrail(currentState);
auditTrail.setLastModifiedDate(new Date());
auditTrail.setLastModifiedAgentId(getAgentIdFromContext());
return true;
}

return false;
}

/**
* Loops through the array of state objects and returns the AuditTrail
* instance
*
* @param state
* @return
*/
private AuditTrail getAuditTrail(Object[] state)
{
AuditTrail auditTrail = null;
for (Object o : state)
{
if (o instanceof AuditTrail)
auditTrail = (AuditTrail) o;
}
return auditTrail;
}
...
}

Hopefully this is useful for someone else too.

Poornima Singh replied on Tue, 2008/09/23 - 11:23am

This article has been very helpful to understand what the interceptor does. My question is, where did you configure the entityInteceptor. I am using a combination of hibernate and spring, I have added the below in the sessionFactory part of the applictaioncontext-dao.xml. I have the same situation as you do where not all entities are auditable.

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="entityInterceptor">
            <bean class="org.common.hibernate.AuditInterceptor"/>
        </property>       

....

</bean

Blake Minghelli replied on Tue, 2008/09/23 - 11:44am

We don't use Spring but if it's helpful, we create our configuration programmatically and set the interceptor like this:

Configuration cfg = new Configuration();
cfg.setInterceptor(new AuditInterceptor());

 

Poornima Singh replied on Wed, 2008/09/24 - 10:48am in response to: Blake Minghelli

Thank you for the response, I was able to get the Interceptor working succesfully. I do have to still get a hold of the userid though, how does your getAgentIdfromcontext method do it. Can you please share that info.

Blake Minghelli replied on Wed, 2008/09/24 - 11:07am

At a high level, we store the agent in a ThreadLocal variable so that it can be easily accessed at any point in our code (like the interceptor).  When an agent logs in, we store it in the Session and in the ThreadLocal variable.  Then we have a web Filter that, at the start of every request, grabs the agent out of the Session and adds it again to ThreadLocal.  So the getAgentIdFromContext() method is simply grabbing the agent from the ThreadLocal variable. 

(We hide the ThreadLocal storage/retreival behaind a sort of "authentication service", e.g. service.initializeAgent(agent) would add the agent to ThreadLocal and service.getAgent() would return the agent from ThreadLocal)

Nitin Gupta replied on Fri, 2008/09/26 - 2:27am

Hi,

I have got a similar requirement. But insead of setting a single value, in one case I have to update a collection when an entity is saved, updated or deleted. The collection is of a another persistable entity.

 In the case of onSave, it worked fine. But it gave a classcast exception when I attempted to set a Set on a particular instance in onDirtyFlush method.

 

Any help would be highly appreciated.

 

thanks and regards,

Nitin 

Pawinder Gupta replied on Thu, 2008/12/25 - 11:50am

Thanks for this great article. I am using interceptor for exactly the same functionality i.e. logging user activities.

 However, I am stuck in a weird issue. I used thread local to store user id. But since my application is web based and hosted on java web server, it depends on server thread management for handling threads. Java web server uses the same thread to service multiple requests. The objects bound to thread are not garbage collected at the end of http request. Is there a way you found to get around this issue.

Blake Minghelli replied on Mon, 2008/12/29 - 10:27am in response to: Pawinder Gupta

Yes. We use a web Filter to store the user id in ThreadLocal and then use that same filter to remove the user id from ThreadLocal at the end of the request.

For example, in the filter's doFilter() method...

// store the user id in ThreadLocal...

filterChain.doFilter(request, response); // execute request

// remove user id from ThreadLocal...

Josh Durbin replied on Mon, 2009/02/09 - 11:03pm

I'm trying to setup an interceptor to take care of logical deletions.  I have the following code:

public class EntityStatusInterceptor extends EmptyInterceptor {

    public boolean onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        setValue(state, propertyNames, "entityStatus", EntityStatus.INACTIVE);
        return true;   
    }
   
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        setValue(state, propertyNames, "entityStatus", EntityStatus.ACTIVE);
        return true;       
    }
   
    private void setValue(Object[] currentState, String[] propertyNames, String propertyToSet, Object value) {
       
        Integer propertyPosition = 0;
        for (String property : propertyNames) {
           
            if (StringUtils.equals(property, propertyToSet)) {
                currentState[propertyPosition] = value;
            } else {
                propertyPosition++;
            }
           
        }       
    }
   
}

 ...................

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource"><ref local="dataSource"/></property>
        <property name="annotatedClasses">
            <list>
                <value>com.ktm.tt.entity.BaseEntity</value>
               </list>
        </property>
        <property name="entityInterceptor"><ref local="entityStatusInterceptor" /></property>       
        <property name="hibernateProperties">
            ...
        </property>
    </bean>

.................

 I am unable to get the Interceptor to fire.  I see it being registered but it never is invoked.  I tried removing all the logic from the method bodies are placing with log statements which too resulted in nothing...

 I've also tried setting:

        <property name="entityInterceptor"><ref local="entityStatusInterceptor" /></property>        

... on my transactionManager (org.springframework.orm.hibernate3.HibernateTransactionManager) which takes the above SessionFactory as a param.

 Any ideas???

trib bisht replied on Tue, 2009/04/28 - 6:46am

Unless method signature are matched with the method in Interceptor class (including Exception), Interceptor’s methods will not be invoked. In above scenario call back exception are missing for overridden methods.

Syed Mahdi replied on Wed, 2010/06/23 - 1:57am

Hi All, Neat article. I am using this and not tested it out yet but will be doing so within an hour or so. Just need some concepts here. I have the audittrailinterceptor class and it is referenced in the spring-config.xml as well. In my case We audit each and every update and save. So its exactly the same as the article as well. What I do not understand is how is the system going to fire the interceptor's onSave() method. What I am trying to ask is that just by only specifiying it globally in the spring-config the hibernate framework is going to take care of all that and will know if it is a save or update. Another conceptual question. We already had the audit trail fields in all the tables when we designed them so when we are updating or saving new why cant we fill the UpdatedBy and UpdatedOn fields or CreatedBy and CreatedOn fields respectively at that point. Then we dont really need an interceptor. What makes the interceptor more useful thet we have to handle these specific fields only through an interceptor. Thanks. Syed.

Syed Mahdi replied on Wed, 2010/06/23 - 2:03am in response to: trib bisht

Well, You do seem right but neither the article nor the example given by blake fullfill this requirement, they dont have exception either in their code.

Software Developer replied on Fri, 2010/10/08 - 12:28pm

I have a similar requirement, I have declared by intercepter on the persistence file as I also have Interceptor helper where i am doing my updated to the audit table in the database,I want to retreive data from HTTP request. Do you have any suggestions

Jagan Reddy replied on Fri, 2010/12/10 - 1:39am

I am validating the user id by getting it from TIM/TAM in Portal layer. This user is not available in Application layer. So I am passing the user id as a parameter. But onFlushDirty method is treating as the object is modifed eventhough the object is not modifed by the user because I am setting the user id. The database is getting updated with user id and time stamp even though the user is not modified anything.

Please help me how to resolve this issue.

 

Is there anyway to get the previous state of the each element in AuditIntercepter. I tried the below way but previousState[i] always comming as null.

 

if (AUDIT_USER_ID.equalsIgnoreCase(propertyNames[i]) ) {
                                                             
                                currentState[i] = previousState[i];

Carla Brian replied on Sat, 2012/06/16 - 8:35am

I am new to this one. I need more tutorials about this. But thanks for sharing your knowledge about this one. At least I have a hint on how to do this. - Joe Aldeguer

Gurpreet Singh replied on Wed, 2014/04/16 - 6:01pm

First of all.. Thanks for this. It worked like charm.

We have a requirement of keeping the historical data. To clarify, one record can be updated several times by several users. We need to keep track of all that. Is there any easy way to do that?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.