UserType for audit info loggingMany applications require that certain information like datetimes and usernames are persisted when data changes. Here is a design that has certain advantages over other more common approaches. (Other approaches include inheriting a base class, modelling AuditInfo as a component, etc.) Source for this can be downloaded from http://www.indigoegg.com/hibernate/addon/auditinfo.zip (was not able to access this site as of 2003/02/15). We start with an interface to be implemented by persistent classes that require audit info tracking.
public interface Auditable {
/**
* Instances must always return a non-null instance of AuditInfo
*/
public AuditInfo getAuditInfo();
}
and a class that holds the audit data (we are being fine-grained again).
public final class AuditInfo implements Serializable {
private Timestamp lastUpdated ;
private Timestamp created ;
private Long updatedBy ;
private Long createdBy ;
public Timestamp getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Timestamp lastUpdated) { this.lastUpdated = lastUpdated; }
public Timestamp getCreated() { return created; }
public void setCreated(Timestamp created) { this.created = created; }
public Long getUpdatedBy() { return updatedBy; }
public void setUpdatedBy(Long updatedBy) { this.updatedBy = updatedBy; }
public Long getCreatedBy() { return createdBy; }
public void setCreatedBy(Long createdBy) { this.createdBy = createdBy; }
}
We will create a custom type for AuditInfo.
public class AuditInfoType implements UserType {
private static final int[] SQL_TYPES = new int[]{
Types.TIMESTAMP,
Types.TIMESTAMP,
Types.BIGINT,
Types.BIGINT
};
public int[] sqlTypes() {
return SQL_TYPES;
}
public boolean isMutable() {
return true;
}
public Class returnedClass() {
return AuditInfo.class;
}
public boolean equals(Object x, Object y) {
if( x == y ) return true;
if( x == null || y == null ) return false;
AuditInfo aix = (AuditInfo) x;
AuditInfo aiy = (AuditInfo) y;
try {
return Hibernate.TIMESTAMP.equals(aix.getLastUpdated(), aiy.getLastUpdated()) &&
Hibernate.TIMESTAMP.equals(aix.getCreated(), aiy.getCreated()) &&
Hibernate.LONG.equals(aix.getUpdatedBy(), aiy.getUpdatedBy()) &&
Hibernate.LONG.equals(aix.getCreatedBy(), aiy.getCreatedBy());
} catch( HibernateException e ) {
return false;
}
}
public Object deepCopy(Object value) {
if( value == null ) return null;
AuditInfo ai = (AuditInfo) value;
AuditInfo result = new AuditInfo();
try {
result.setLastUpdated((Timestamp) Hibernate.TIMESTAMP.deepCopy(ai.getLastUpdated()));
result.setCreated((Timestamp) Hibernate.TIMESTAMP.deepCopy(ai.getCreated()));
result.setUpdatedBy(ai.getUpdatedBy());
result.setCreatedBy(ai.getCreatedBy());
return result;
} catch( HibernateException e ) {
e.printStackTrace();
return result;
}
}
public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
throws HibernateException, SQLException {
//AuditInfo can't be null
AuditInfo ai = new AuditInfo();
System.out.println(Hibernate.TIMESTAMP.nullSafeGet(rs, names[0]).getClass()) ;
ai.setLastUpdated((java.sql.Timestamp) Hibernate.TIMESTAMP.nullSafeGet(rs, names[0]));
ai.setCreated((java.sql.Timestamp) Hibernate.TIMESTAMP.nullSafeGet(rs, names[1]));
ai.setUpdatedBy((Long) Hibernate.LONG.nullSafeGet(rs, names[2]));
ai.setCreatedBy((Long) Hibernate.LONG.nullSafeGet(rs, names[3]));
return ai;
}
public void nullSafeSet(PreparedStatement st, Object value, int index)
throws HibernateException, SQLException {
//AuditInfo can't be null
AuditInfo ai = (AuditInfo) value;
Hibernate.TIMESTAMP.nullSafeSet(st, ai.getLastUpdated(), index);
Hibernate.TIMESTAMP.nullSafeSet(st, ai.getCreated(), index + 1);
Hibernate.LONG.nullSafeSet(st, ai.getUpdatedBy(), index + 2);
Hibernate.LONG.nullSafeSet(st, ai.getCreatedBy(), index + 3);
}
}
Finally, we create a session-level Interceptor to set the AuditInfo properties when an instance of Auditable is updated or created. Because of the design of the custom type, we can avoid iterating property names inside the Interceptor. We should stress the point that the following is not a typical Interceptor implementation; it makes use of "special" behaviour of custom types.
public class AuditInterceptor implements Interceptor, Serializable {
private Long _user;
public AuditInterceptor(Long user) {
_user = user;
}
public boolean onLoad(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
return false;
}
public boolean onFlushDirty(
Object entity,
Serializable id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) {
if( entity instanceof Auditable ) {
AuditInfo ai = ((Auditable) entity).getAuditInfo();
try {
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
ai.setLastUpdated(timestamp);
} catch( Exception e ) {
e.printStackTrace();
}
ai.setUpdatedBy(_user);
}
return false;
}
public boolean onSave(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if( entity instanceof Auditable ) {
AuditInfo ai = ((Auditable) entity).getAuditInfo();
try {
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
ai.setCreated(timestamp);
} catch( Exception e ) {
e.printStackTrace();
}
ai.setCreatedBy(_user);
}
return false;
}
public void onDelete(
Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
}
public void preFlush(Iterator entities) { }
public void postFlush(Iterator entities) { }
}
Your application code that opens the session must be sure and use the openSession method that passes in the interceptor. For example:
Long user = (Long) savedUserId;
Session session = sf.openSession ( new AuditInterceptor(user) );
An auditable class Widget would be mapped as follows (note that AuditInfoType is the correct class to specify, not AuditInfo):
<class name="Widget" table="WIDGETS">
<id name="id">
<generator class="seqhilo.long"/>
</id>
<property name="auditInfo" type="AuditInfoType">
<column name="LAST_UPDATED"/>
<column name="CREATED"/>
<column name="UPDATED_BY"/>
<column name="CREATED_BY"/>
</property>
.....
</class>
|