Hibernate interceptor example – audit log

Hibernate has a powerful feature called ‘interceptor‘ to intercept or hook different kind of Hibernate events, like database CRUD operation. In this article, i will demonstrate how to implement an application audit log feature by using Hibernate interceptor, it will log all the Hibernate save, update or delete operations into a database table named ‘auditlog‘.

Hibernate interceptor example – audit log

1. Create a table

Create a table called ‘auditlog’ to store all the application audited records.


DROP TABLE IF EXISTS `mkyong`.`auditlog`;
CREATE TABLE  `mkyong`.`auditlog` (
  `AUDIT_LOG_ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `ACTION` varchar(100) NOT NULL,
  `DETAIL` text NOT NULL,
  `CREATED_DATE` date NOT NULL,
  `ENTITY_ID` bigint(20) unsigned NOT NULL,
  `ENTITY_NAME` varchar(255) NOT NULL,
  PRIMARY KEY (`AUDIT_LOG_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

2. Create a marker interface

Create a marker interface, any classes which implemented this interface will be audit. This interface requires that the implemented class to expose it identifier – getId() and the content to log – ‘getLogDeatil()‘. All exposed data will be store into database.


package com.mkyong.interceptor;
//market interface
public interface IAuditLog {
	
	public Long getId();	
	public String getLogDeatil();
}

3. Map the ‘auditlog’ table

A normal annotation model file to map with table ‘auditlog’.


@Entity
@Table(name = "auditlog", catalog = "mkyong")
public class AuditLog implements java.io.Serializable {

	private Long auditLogId;
	private String action;
	private String detail;
	private Date createdDate;
	private long entityId;
	private String entityName;
        ...
}

4. A class implemented the IAuditLog

A normal annotation model file to map with table ‘stock’, which will use for interceptor demo later. It have to implemented the IAuditLog marker interface and implement the getId() and getLogDeatil() method.


...
@Entity
@Table(name = "stock", catalog = "mkyong"
public class Stock implements java.io.Serializable, IAuditLog {
...
        @Transient
	@Override
	public Long getId(){
		return this.stockId.longValue();
	}
	
	@Transient
	@Override
	public String getLogDeatil(){
		StringBuilder sb = new StringBuilder();
		sb.append(" Stock Id : ").append(stockId)
		.append(" Stock Code : ").append(stockCode)
		.append(" Stock Name : ").append(stockName);

		return sb.toString();
	}
...

5. Create a Helper class

A helper class to accept the data from interceptor and store it into database.


...
public class AuditLogUtil{
	
   public static void LogIt(String action,
     IAuditLog entity, Connection conn ){
		
     Session tempSession = HibernateUtil.getSessionFactory().openSession(conn);
			
     try {

	AuditLog auditRecord = new AuditLog(action,entity.getLogDeatil()
		, new Date(),entity.getId(), entity.getClass().toString());
	tempSession.save(auditRecord);
	tempSession.flush();
			
     } finally {	
	tempSession.close();		
     }		
  }
}

6. Create a Hibernate interceptor class

Create a interceptor class by extends the Hibernate EmptyInterceptor. Here is the most popular interceptor function.

  • onSave – Called when you save an object, the object is not save into database yet.
  • onFlushDirty – Called when you update an object, the object is not update into database yet.
  • onDelete – Called when you delete an object, the object is not delete into database yet.
  • preFlush – Called before the saved, updated or deleted objects are committed to database (usually before postFlush).
  • postFlush – Called after the saved, updated or deleted objects are committed to database.

The code is quite verbose, it should self-exploratory.


...
public class AuditLogInterceptor extends EmptyInterceptor{
	
	Session session;
	private Set inserts = new HashSet();
	private Set updates = new HashSet();
	private Set deletes = new HashSet();
	
	public void setSession(Session session) {
		this.session=session;
	}
		
	public boolean onSave(Object entity,Serializable id,
		Object[] state,String[] propertyNames,Type[] types)
		throws CallbackException {
		
		System.out.println("onSave");
		
		if (entity instanceof IAuditLog){
			inserts.add(entity);
		}
		return false;
			
	}
	
	public boolean onFlushDirty(Object entity,Serializable id,
		Object[] currentState,Object[] previousState,
		String[] propertyNames,Type[] types)
		throws CallbackException {
	
		System.out.println("onFlushDirty");
		
		if (entity instanceof IAuditLog){
			updates.add(entity);
		}
		return false;
		
	}
	
	public void onDelete(Object entity, Serializable id, 
		Object[] state, String[] propertyNames, 
		Type[] types) {
		
		System.out.println("onDelete");
		
		if (entity instanceof IAuditLog){
			deletes.add(entity);
		}
	}

	//called before commit into database
	public void preFlush(Iterator iterator) {
		System.out.println("preFlush");
	}	
	
	//called after committed into database
	public void postFlush(Iterator iterator) {
		System.out.println("postFlush");
		
	try{
		
		for (Iterator it = inserts.iterator(); it.hasNext();) {
		    IAuditLog entity = (IAuditLog) it.next();
		    System.out.println("postFlush - insert");		
		    AuditLogUtil.LogIt("Saved",entity, session.connection());
		}	
			
		for (Iterator it = updates.iterator(); it.hasNext();) {
		    IAuditLog entity = (IAuditLog) it.next();
		    System.out.println("postFlush - update");
		    AuditLogUtil.LogIt("Updated",entity, session.connection());
		}	
			
		for (Iterator it = deletes.iterator(); it.hasNext();) {
		    IAuditLog entity = (IAuditLog) it.next();
		    System.out.println("postFlush - delete");
		    AuditLogUtil.LogIt("Deleted",entity, session.connection());
		}	
			
	} finally {
		inserts.clear();
		updates.clear();
		deletes.clear();
	}
       }		
}

7.Enabling the interceptor

You can enable the interceptor by pass it as an argument to openSession(interceptor);.


...
   Session session = null;
   Transaction tx = null;
   try {

	AuditLogInterceptor interceptor = new AuditLogInterceptor();
	session = HibernateUtil.getSessionFactory().openSession(interceptor);
	interceptor.setSession(session);
			
	//test insert
	tx = session.beginTransaction();
	Stock stockInsert = new Stock();
	stockInsert.setStockCode("1111");
	stockInsert.setStockName("mkyong");
	session.saveOrUpdate(stockInsert);
	tx.commit();
			
	//test update
	tx = session.beginTransaction();
	Query query = session.createQuery("from Stock where stockCode = '1111'");
	Stock stockUpdate = (Stock)query.list().get(0);
	stockUpdate.setStockName("mkyong-update");
	session.saveOrUpdate(stockUpdate);
	tx.commit();
			
	//test delete
	tx = session.beginTransaction();
	session.delete(stockUpdate);
	tx.commit();

   } catch (RuntimeException e) {
	try {
		tx.rollback();
	} catch (RuntimeException rbe) {
		// log.error("Couldn’t roll back transaction", rbe);
   }
	throw e;
   } finally {
	if (session != null) {
		session.close();
	}
   }
...

In insert test


session.saveOrUpdate(stockInsert); //it will call onSave
tx.commit(); // it will call preFlush follow by postFlush

In update test


session.saveOrUpdate(stockUpdate); //it will call onFlushDirty
tx.commit(); // it will call preFlush follow by postFlush

In delete test


session.delete(stockUpdate); //it will call onDelete
tx.commit();  // it will call preFlush follow by postFlush
Output

onSave
Hibernate: 
    insert into mkyong.stock
    (STOCK_CODE, STOCK_NAME) 
    values (?, ?)
preFlush
postFlush
postFlush - insert
Hibernate: 
    insert into mkyong.auditlog
    (ACTION, CREATED_DATE, DETAIL, ENTITY_ID, ENTITY_NAME) 
    values (?, ?, ?, ?, ?)
preFlush
Hibernate: 
    select ...
    from mkyong.stock stock0_ 
    where stock0_.STOCK_CODE='1111'
preFlush
onFlushDirty
Hibernate: 
    update mkyong.stock 
    set STOCK_CODE=?, STOCK_NAME=? 
    where STOCK_ID=?
postFlush
postFlush - update
Hibernate: 
    insert into mkyong.auditlog
    (ACTION, CREATED_DATE, DETAIL, ENTITY_ID, ENTITY_NAME) 
    values (?, ?, ?, ?, ?)
onDelete
preFlush
Hibernate: 
    delete from mkyong.stock where STOCK_ID=?
postFlush
postFlush - delete
Hibernate: 
    insert into mkyong.auditlog 
    (ACTION, CREATED_DATE, DETAIL, ENTITY_ID, ENTITY_NAME) 
    values (?, ?, ?, ?, ?)
In database

SELECT * FROM auditlog a;

All audited data are inserted into database.

interceptor-example

Conclusion

The audit logs is a useful feature that is often handled in database by using triggers, but i would recommend to use application to implement it for the portability concern.

Download this example – Hibernate interceptor example.zip

40 comments on “Hibernate interceptor example – audit log

  1. Hello,
    Just to inform that ‘postFlush’ is not triggered after committing to database. It is triggered when the session flushed. If a rollback occurs postFlush will be triggered so the comment should be changed 🙂

    Thanks for the great post.

    Reply
  2. Is there a way to get the transient field value in the interceptor

    Reply
  3. Hello! excelente post.

    I have a problem:

    tempSession.save(auditRecord);
    tempSession.flush();

    No save the object in database 🙁 and I have inifinite loop in the next code:

    for (Iterator it = updates.iterator(); it.hasNext();) {
    IAuditLog entity = (IAuditLog) it.next();
    System.out.println(“postFlush – update”);
    AuditLogUtil.LogIt(“Updated”,entity);
    }

    Reply
  4. Otherwise for activating interceptor, insert this line into persistence.xml

    Reply
    1. but i have many interceptors but i dont wanna conf in the xml. i wanna put in the injection of depencies, i cant find a solution for this

      Reply
  5. For Hibernate 4.5 that parameter in session = HibernateUtil.getSessionFactory().openSession(interceptor); is deprecated or not exist, how can y set de interceptor?

    Reply
  6. Hi! i try to the same but show me the next error:
    The method openSession() in the type SessionFactory is not applicable for the arguments

    I use hibernate 4.2.5, thanks for your help

    Reply
    1. Hi all ! I’m using hibernate 4.2.5 from hibernate 3.5.2 , i’m getting The method openSession() in the type SessionFactory is not applicable for the arguments…can you any one solve this problem…pls let me know resolution..im having same issue right now

      Reply
  7. In AuditLogInterceptor class. postFlush() method,

    AuditLogUtil.LogIt(“Saved”,entity, session.connection());
    AuditLogUtil.LogIt(“Updated”,entity, session.connection());
    AuditLogUtil.LogIt(“Deleted”,entity, session.connection());

    is not working. Can you help me please?
    The error says that the method connection() is undefined for the type Session..

    Reply
  8. hey mk test your damn app before you copy past from others

    Reply
  9. Hi,
    I have an entity with a parameter of type Set.
    When the control comes to interceptor the value for this parameter comes to be null even if I have set the values for this parameter.
    Could someone please help.

    Reply
  10. Hello,

    Thanks for the application. I have a small concern when running the above code.

    When I right click on App.java file and say run as Java application it is printing below output in the console.But there are no new tables created such as stock_daily_record,category,auditlog,stock_category, stock,stock_daily_record,stock_detail.

    I only have one table in mkyong database that is customer.When I run the code it is supposed to generate the above mentioned tables automatically as we used annotations, but it is not. Can you let me know how to run the application.

    onSave
    Hibernate:
    insert
    into
    mkyong.stock
    (STOCK_CODE, STOCK_NAME)
    values
    (?, ?)
    preFlush
    postFlush
    postFlush – insert
    Hibernate:
    insert
    into
    mkyong.auditlog
    (ACTION, CREATED_DATE, DETAIL, ENTITY_ID, ENTITY_NAME)
    values
    (?, ?, ?, ?, ?)
    preFlush
    Hibernate:
    select
    stock0_.STOCK_ID as STOCK1_0_,
    stock0_.STOCK_CODE as STOCK2_0_,
    stock0_.STOCK_NAME as STOCK3_0_
    from
    mkyong.stock stock0_
    where
    stock0_.STOCK_CODE=’1111′
    preFlush
    onFlushDirty
    Hibernate:
    update
    mkyong.stock
    set
    STOCK_CODE=?,
    STOCK_NAME=?
    where
    STOCK_ID=?
    postFlush
    postFlush – update
    Hibernate:
    insert
    into
    mkyong.auditlog
    (ACTION, CREATED_DATE, DETAIL, ENTITY_ID, ENTITY_NAME)
    values
    (?, ?, ?, ?, ?)
    onDelete
    preFlush
    Hibernate:
    delete
    from
    mkyong.stock
    where
    STOCK_ID=?
    postFlush
    postFlush – delete
    Hibernate:
    insert
    into
    mkyong.auditlog
    (ACTION, CREATED_DATE, DETAIL, ENTITY_ID, ENTITY_NAME)
    values
    (?, ?, ?, ?, ?)

    Reply
    1. Please ignore the above post. The issue is fixed. The tables got created successfully. I had another database with the similar name. Thanks for all the support.

      Reply
  11. Above code is subjected to a problem when there are more than one transaction are in process concurrently , and both transactions creates entities.

    Let me take a example:

    Tx_A create entity EA1, EA2.

    Tx_B creates entity EB1, EB2.

    Now let say execution flow happens this way:

    [1] onSave for EA1, we add EA1 id to inserts set

    [2] onSave for EB1, we add EB1 id to inserts set

    [3] postFlush(tx) for Tx_A

    Now at this point we will flush EA1 and EB1 id and now Tx_B rollbacks

    we will log EB1 as created .. but it is not.

    What can be the strategy to solve that ?

    Reply
    1. Am getting the following exception : java.util.ConcurrentModificationException

      When multiple tx are in process.

      Can you please help

      Reply
  12. Hello,
    this does not work for me i am using hibbernate 4.1 can you help me thanks.

    Reply
  13. Does interceptor work globally or per session?

    I have this trigger:

    CREATE TRIGGER mytable_BI BEFORE INSERT ON mytable
    FOR EACH ROW BEGIN
    SET NEW.rank = (SELECT IFNULL(MAX(rank),-1) + 1 FROM channelGroupItem WHERE parentId = NEW.parentId);
    END;

    I’ve tried to use interceptor but I need a session inside an interceptor to check the maximum value in the DB.

    Reply
    1. Same problem here…did you get an answer?
      I need to invoke StoredProc from interceptor onSave and postFlush.
      I am using spring configuration – AnnotationSessionFactoryBean for session factory.
      something like this….

      Not able to bind sessionFactory or session back to auditInterceptor.

      Reply
  14. can you pls guide me implement this in C#.net pls…. Thank in advance….

    Reply
  15. Hi, I am not much into hibernate but would like to know how different this is using AcpectJ/AOP to implement intercept functionality ? i think this can be easily achieved with aspects.

    Reply
  16. Nice Article MKYONG !!
    I have one query is it mandatory to use the same session for audit ?
    If yes, then if I configure interceptor through xml , how will I pass current session to interceptor.
    If you know then please tell us.

    Thanks !!

    Reply
  17. Hi mkyong,

    I have downloaded the above project file to my workplace. But in that i didn’t find the client program.
    Can you please tell me steps how i can run in my eclipse workplace.

    whether i have to use Junit for running this example?

    Thanks.

    Reply
  18. I hardly write comments, but I browsed some responses on Spreading Holiday Cheer at Neiman Marcus. I do have 2 questions for you if it’s allright. Is it only me or do a few of these comments look as if they are written by brain dead individuals? And, if you are posting at other social sites, I’d like to follow you. Could you make a list of the complete urls of all your shared sites like your twitter feed, Facebook page or linkedin profile?

    Reply
  19. I tried it. The method onSave in the interceptor is called twice, and the main thread goes to AbstractSaveEventListener. The thread then keeps running infinitely.

    Reply
  20. I’m sorry MyKong. I understand you are trying to teach people Hibernate and I am learning a lot very quickly. It surely beats trying to read 1000 page book. However as I opined once before this example might send wrong message to fresh developers.

    I shouldn’t be mangling my domain because I want to Audit something. I shouldn’t have to open my Stock class and implement IAuditLog. Interception should happen externally. Only thing I should do is perhaps create an @Auditable annotation on my domain classes. When application loads, it should simply make sure methods such as getId() and/or getLogDetail() are implemented.

    As was already pointed out in another reply, neither is this a marker interface, but more importantly I should not have to implement this interface at all. Furthermore, maybe I can simply use toString() instead of getLogDetail(). THis about this. Then I don’t even need getId(). IF my class uses getIdentity() instead it will still work.

    The whole point of interceptors is to keep class it is intercepting OBLIVIOUS. Class being intercepted should not know IF and WHY and HOW intercepting.

    Just my 2 cents

    Reply
    1. Thanks for your invaluable inputs and concerns. This article is demonstrated that Hibernate is able to handle interceptor task only.

      In production , Spring AOP is a better choice to act as the interceptor, and it matched what you mention above 🙂

      Reply
      1. Yes, Spring interceptor is good choice. But if using Hibernate without Spring, I would like to make use of Hibernate interceptor. I think we can make it work without having Model class implement IAuditLog. I will think about this.

        Reply
        1. @Arby,
          Can you post example what you have thought of changing this example?
          I am beginner in hibernate and dont understand what u are talking about.By example it will be more clear.

          Thanks.

          Reply
  21. Very Nice document…
    Just including the jars that will be needed for compilation/execution…

    * hibernate-annotations.jar
    * hibernate-core.jar
    * javax.persistence.jar
    * junit-3.8.1.jar
    * hibernate3.jar

    Regards
    Hrishikesh

    Reply
      1. how a marker interface will have operations ? The interface you have created can not be marker interfce .

        Reply
        1. Ignore Marker keyword here.. it is “Market” interface

          Reply
  22. Nice tutorial, just a small comment:
    “2. Create a marker interface”

    AFAIK Marker interface is the interface, which does not have any methods in it.
    So this is not a marker interface.

    Reply
  23. Good info for how to use interceptors. But for auditing … Envers.

    Reply

Leave a Comment

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