原文: 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 ([email protected]) * */ 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 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>
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 ([email protected]) */ 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(); } }