[Hibernate] Interceptor - Audit log

Hibernate có một tính năng mạnh mẽ gọi là ‘interceptor‘ để chặn hoặc hook các event của Hibernate, như hoạt động CRUD. Trong bài viết này, chúng ta sẽ tìm hiểu và cách làm ứng dụng với tính năng audit log bằng cách sử dụng Hibernate interceptor, nó sẽ ghi lại các hoạt động insert, update, delete của Hibernate vào trong cơ sở dữ liệu có tên bảng là ‘auditlog‘.


Hibernate interceptor – audit log


1. Tạo bảng
Tạo 1 bảng ‘auditlog’ để lưu tất cả các hoạt động của ứng dụng.

SQL:
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. Tạo marker interface
Tạo 1 maker interface, bất kì class nào implement interface này cũng sẽ được audit. Interface này yêu cầu các class con phải expose ra identifier – getId() và chứa nội dung ghi log – ‘getLogDeatil()‘. Tất cả các expose data sẽ được lưu lại trong cơ sở dữ liệu.

Java:
package com.mkyong.interceptor;
//market interface
public interface IAuditLog {

    public Long getId();
    public String getLogDeatil();
}

3. Map với bảng ‘auditlog’
File model annotation để mapping với bảng ‘auditlog’.

Java:
@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. Class implement IAuditLog
File model annotation để mapping bảng ‘stock’. nó sẽ được sử dụng cho interceptor demo sau. Nó phải implement interface IAuditLog và thực override 2 phương thức getId()getLogDeatil().

Java:
...
@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. Tạo class Helper
Class Helper giúp nhận dữ liệu từ interceptor và lưu nó trong cơ sở dữ liệu.

Java:
...
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. Tạo class Hibernate interceptor
Tạo 1 class interceptor và extends class Hibernate EmptyInterceptor. Đây là hàm interceptor phổ biến nhất.

  • onSave - Được gọi khi save đối tượng, đối tượng không được save vào trong cơ sở dữ liệu ngay.
  • onFlushDirty - Được gọi khi update đối tượng, đối tượng không được update vào trong cơ sở dữ liệu ngay.
  • onDelete - Được gọi khi delete đối tượng, đối tượng không được delete trong cơ sở dữ liệu ngay.
  • preFlush - Được gọi trước khi save, update, delete đối tượng được thực hiện trên cơ sở dữ liệu( thường trước postFlush).
  • postFlush - Được gọi sau khi save, update, delete đối tượng được thực hiện trên cơ sở dữ liệu.
Code có vẻ dài dòng

Java:
...
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. Enable interceptor
Có thể enable iterceptor bằng cách truyền nó vào openSession(interceptor);.

Java:
...
   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();
    }
   }
...
Test insert
Java:
session.saveOrUpdate(stockInsert); //it will call onSave
tx.commit(); // it will call preFlush follow by postFlush
Test update
Java:
session.saveOrUpdate(stockUpdate); //it will call onFlushDirty
tx.commit(); // it will call preFlush follow by postFlush
Test delete
Java:
session.delete(stockUpdate); //it will call onDelete
tx.commit();  // it will call preFlush follow by postFlush
Kết quả
Code:
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 (?, ?, ?, ?, ?)
Trong database
Code:
SELECT * FROM auditlog a;
Tất cả dữ liệu audit được insert vào cơ sở dữ liệu.

interceptor-example.jpg


Kết luận
Audit logs là tính năng hữu ích, nó thường được xử lý trong cở sở dữ liệu thông qua cách sử dụng triggers, nhưng nên implement nó trên ứng dụng để để có tính di động


Download source code – HibernateInterceptotExample.zip


Cám ơn các bạn đã theo dõi. Hẹn gặp lại các bạn trong các bài viết sau :D

Bài viết tham khảo tại: https://mkyong.com/hibernate/hibernate-interceptor-example-audit-log/
 

Bình luận