History Interceptor

History Interceptor

原文: https://www.hibernate.org/195.html 

 

 

I've made some enhancements to the AuditInterceptor so that not only the creation- and modification times were recorded.

The following Interceptor can be used to record all modifcations on an Object in a seperate table. The main pitfall is the problem, that you cannot modify the set of history entries of an object within onFlushDirty since you will get an TransientObjectException. The workaround is to record the old history entries in preFlush() in a new set and using this one in onFlushDirty.

package de.micromata.hibernate;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import net.sf.hibernate.CallbackException;
import net.sf.hibernate.Interceptor;
import net.sf.hibernate.type.Type;

import org.apache.log4j.Priority;

/**
 * Implementation of an Interceptor recording all changes on an Object in a
 * seperate table.
 *
 * @author Wolfgang Jung (w.jung@micromata.de)
 *
 */
public class HistoryInterceptor implements Interceptor {

  /** New history entries, found in onFlushDirty */
  private final Map histories = new HashMap();

  /** Our Logger */
  private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(HistoryInterceptor.class);

  /** The user who changes the objects */
  private final String userName;

  /**
   * Object formatter, should use something like
   * org.apache.log4j.spi.RendererSupport
   */
  private String format(Object obj) {
    if (obj == null) {
      return null;
    }
    return obj.toString();
  }

  /** Create a new Interceptor, recording the changes under the given userName */
  public HistoryInterceptor(final String userName) {
    this.userName = userName;
  }

  /** the username */
  private String getUser() {
    return userName;
  }

  /**
   * @see Interceptor#onLoad(java.lang.Object, java.io.Serializable,
   *           java.lang.Object[], java.lang.String[], net.sf.hibernate.type.Type[])
   */
  public boolean onLoad(Object obj, Serializable id, Object[] values, String[] properties, Type[] types) throws CallbackException {
    return false;
  }

  /**
   * Record the changes in the HashMap. Unfortunately, these changes can't be
   * done immediately (TransientObjectException), so they are recorded in a
   * seperate Set.
   * 
   * @see Interceptor#onFlushDirty(java.lang.Object, java.io.Serializable,
   *           java.lang.Object[], java.lang.Object[], java.lang.String[],
   *           net.sf.hibernate.type.Type[])
   */
  public boolean onFlushDirty(Object obj, Serializable id, Object[] newValues, Object[] oldValues, String[] properties, Type[] types)
      throws CallbackException {
    if (log.isEnabledFor(Priority.INFO)) {
      log.info("Updating " + obj + " with id " + id + " new=" + Arrays.asList(newValues) + " old=" + Arrays.asList(oldValues) + " props="
          + Arrays.asList(properties));
    }

    if (!(obj instanceof Historizable)) {
      return false;
    }
    Historizable h = (Historizable) obj;

    // Won't work:
    // net.sf.hibernate.TransientObjectException: object references an unsaved
    // transient instance - save the transient instance before flushing:
    // de.micromata.hibernate.HistoryEntry
    // Set entries = h.getHistoryEntries();
    //
    // get the copy from the map
    Set entries = (Set) histories.get(obj);
    for (int i = 0; i < properties.length; i++) {
      // Skip the historyEntries
      if (properties[i].equals("historyEntries") == true) {
        continue;
      }
      Object oldOne = oldValues[i];
      Object newOne = newValues[i];
      // Check for changes
      if (oldOne == null && newOne == null) {
        continue;
      }
      if (newOne instanceof PersistentCollection) {
        // Collections must be compared against the snapshot
        PersistentCollection collection = (PersistentCollection) newValues[i];
        if (collection.isDirectlyAccessible() == false) {
          continue;
        }
        // retrieve Snapshot
        oldOne = collection.getCollectionSnapshot().getSnapshot();
        if (oldOne instanceof Map && newOne instanceof Set) {
          // a Set is internally stored as Map
          oldOne = ((Map) oldOne).values();
        }
      }
      if (oldOne != null && oldOne.equals(newOne) == true) {
        continue;
      }
      // Generate a new entry

      HistoryEntry entry = new HistoryEntry();
      entry.setWho(getUser());
      entry.setTimestamp(new Timestamp(new Date().getTime()));
      entry.setWhat("update");
      entry.setProperty(properties[i]);
      entry.setOldValue(format(oldOne));
      entry.setNewValue(format(newOne));
      if (log.isDebugEnabled()) {
        log.debug("Changed " + properties[i] + " from " + oldOne + " to " + newOne);
      }
      // and store it.
      entries.add(entry);
    }
    // h.setHistoryEntries(entries);

    return false;
  }

  /**
   * Record the creation of the object.
   * 
   * @see net.sf.hibernate.Interceptor#onSave(java.lang.Object,
   *           java.io.Serializable, java.lang.Object[], java.lang.String[],
   *           net.sf.hibernate.type.Type[])
   *  
   */
  public boolean onSave(Object obj, Serializable id, Object[] newValues, String[] properties, Type[] types) throws CallbackException {
    if (!(obj instanceof Historizable)) {
      return false;
    }
    Historizable h = (Historizable) obj;
    if (log.isDebugEnabled()) {
      log.debug("Inserting " + obj + " with id " + id + " new=" + Arrays.asList(newValues) + " props=" + Arrays.asList(properties));
    }

    // Ensure that the set is not null
    Set entries = h.getHistoryEntries();
    if (entries == null) {
      entries = new HashSet();
      h.setHistoryEntries(entries);
    }
    HistoryEntry entry = new HistoryEntry();
    entry.setWho(getUser());
    entry.setTimestamp(new Timestamp(new Date().getTime()));
    entry.setWhat("created");
    entries.add(entry);
    return false;
  }

  public void onDelete(Object obj, Serializable id, Object[] newValues, String[] properties, Type[] types) throws CallbackException {
  }

  public void preFlush(Iterator it) throws CallbackException {
    log.debug("Pre-Flush");
    while (it.hasNext()) {
      Object obj = it.next();
      if (!(obj instanceof Historizable)) {
        continue;
      }
      Historizable h = (Historizable) obj;
      Set s = new HashSet();
      // Record all existing entries
      Set old = h.getHistoryEntries();
      if (old == null) {
        old = new HashSet();
        h.setHistoryEntries(old);
      }
      // s.addAll(h.getHistoryEntries());
      Set newEntries = (Set) histories.put(h, s);
    }
  }

  public void postFlush(Iterator it) throws CallbackException {
    log.debug("Post-Flush");
    while (it.hasNext()) {
      Object obj = it.next();
      if (!(obj instanceof Historizable)) {
        continue;
      }
      Historizable h = (Historizable) obj;
      Set newEntries = (Set) histories.get(h);
      if (newEntries == null) {
        continue;
      }
      h.getHistoryEntries().addAll(newEntries);
    }
    histories.clear();
  }

  public Boolean isUnsaved(Object arg0) {
    return null;
  }

  public int[] findDirty(Object obj, Serializable id, Object[] newValues, Object[] oldValues, String[] properties, Type[] types) {
    return null;
  }

  public Object instantiate(Class arg0, Serializable arg1) throws CallbackException {
    return null;
  }

}

  

The interface <pre>Historizable</pre> is used for accessing the history entries:

package de.micromata.hibernate;
import java.util.Set;
public interface Historizable {
  public Set getHistoryEntries();
  public void setHistoryEntries(Set set);
}

 

The implementation of HistoryEntry is straight forward:

package de.micromata.hibernate;
public class HistoryEntry {
  private Integer id;
  private String who;
  private String what;
  private String property;
  private String oldValue;
  private String newValue;
  private Timestamp timestamp;
  // getter and setter methods omitted.
}

 

Mapping for the HistoryEntry object:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
    PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
    <class name="de.micromata.hibernate.HistoryEntry" table="HISTORY_ENTRIES">
        <id name="id" type="int">
            <column name="HISTORY_ID" />
            <generator class="native"/>
        </id>
        <property name="who"/>
        <property name="what"/>
        <property name="property"/>
        <property name="timestamp"/>
        <property name="oldValue"/>
        <property name="newValue"/>
    </class>
</hibernate-mapping>

 

Sample usage

Sample object for the test:

package de.micromata.hibernate;
import java.util.Set;
public class Order implements Historizable {
  private Set historyEntries;  
  private Integer id;
  private String item;
  private int quantity;
  // getter and setter methods omitted.
}

 

Mapping file for the Order object:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
    PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>
    <class name="de.micromata.hibernate.Order" table="ORDERS">
        <id name="id" type="int">
            <column name="ORDER_ID" />
            <generator class="native"/>
        </id>
        <property name="item"/>
        <property name="quantity"/>
        <set name="historyEntries"
             table="T_ORDER_HISTORY"
             lazy="true"
             inverse="false"
             cascade="all">
          <key column="ORDER_ID" />
          <many-to-many class="de.micromata.hibernate.HistoryEntry"
                        column="HISTORY_ID"
                        outer-join="auto" />
        </set>
    </class>
</hibernate-mapping>

 

Testcase

A simple test-case shows the usage:

package de.micromata.hibernate;

import java.util.Set;

import junit.framework.TestCase;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.Transaction;
import net.sf.hibernate.cfg.Configuration;
import net.sf.hibernate.tool.hbm2ddl.SchemaExport;

/**
 * @author Wolfgang Jung (w.jung@micromata.de)
 */
public class HibernateTest extends TestCase {

  /** Our Logger */
  private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(HibernateTest.class);

  public void testHistory() throws Exception {
    // create clean database
    Configuration cfg = new Configuration();
    cfg.configure();
    SchemaExport export = new SchemaExport(cfg);
    export.drop(true, true);
    export.create(true, true);
    SessionFactory sf = cfg.buildSessionFactory();
    // Open the session with the Interceptor
    Session s = sf.openSession(new HistoryInterceptor("Admin"));

    Transaction tx = s.beginTransaction();
    Order o = new Order();
    o.setItem("Hurzel");
    o.setQuantity(27);
    // o.setHistoryEntries(new HashSet());
    Integer id = (Integer) s.save(o);
    s.flush();
    assertEquals(1, o.getHistoryEntries().size());
    tx.commit();

    tx = s.beginTransaction();
    o = (Order) s.load(Order.class, id);
    Set hist = o.getHistoryEntries();
    // Contains the creation
    assertEquals(1, hist.size());
    o.setQuantity(23);
    s.update(o);
    s.flush();
    assertEquals(2, o.getHistoryEntries().size());
    // Contains the creation and the modification
    tx.commit();

    tx = s.beginTransaction();
    o = (Order) s.load(Order.class, id);
    hist = o.getHistoryEntries();
    log.debug("hist=" + hist);
    // Contains the creation and the modification
    assertEquals(2, hist.size());
    s.delete(o);
    s.flush();
    tx.commit();

    s.close();
    sf.close();
  }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值