
Auditoría con Hibernate
10 Marzo 2008He estado intentando implementar un sistema de auditoría con Hibernate, utilizando un interceptor, migrando desde un sistema basado en Spring AOP, en el cual es más complicado saber qué objetos son nuevos y cuales se actualizan en base de datos.
Un interceptor Hibernate (org.hibernate.Interceptor) permite ejecutar código en diferentes momentos de la transacción, de manera que resulta idóneo para informar o registrar datos de auditoría.
El interceptor lo he creado a partir de EmptyInterceptor, que es un Interceptor vacío, pero que retorna los valores adecuados para asegurar el buen funcionamiento de Hibernate, y sólo hay que sobreescribir los métodos necesarios.
En mi caso, he sobreescrito onSave y onDirtyFlush, para guardar una referencia a los objetos que se insertan o se actualizan. Hibernate se encarga de llamar a estos métodos sólo para los objetos afectados. También he sobreeescrito preFlush y postFlush, para informar realmente los datos y que Hibernate propague el cambio a la base de datos.
Los datos de usuario los he inyectado con Spring, de manera que sólo hay que preocuparse de actualizar los datos en los objetos Hibernate, que todos implementan una interfaz (ModelBase), de forma que esta tarea es sencilla.
El código final queda de la siguiente forma:
public class MiInterceptor extends EmptyInterceptor implements Serializable { private User usuario; private Set<Object> inserts = new HashSet<Object>();
private Set<Object> updates = new HashSet<Object>(); private boolean noInserts = false; public boolean onSave(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) throws CallbackException {
if(entity instanceof ModelBase) {
inserts.add(entity);
}
return false;
} public boolean onFlushDirty(Object entity, Serializable id,
Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) throws CallbackException {
if(entity instanceof ModelBase) {
updates.add(entity);
}
return false;
}
public void preFlush(Iterator entities) {
if(inserts.size() > 0) {
for(Object entity : inserts) {
((SirgoModelBase)entity).setUCreacion(usuario.getNUsuario());
((SirgoModelBase)entity).setFCreacion(new Date());
}
noInserts = false;
} else {
noInserts = true;
}
} public void postFlush(Iterator entities) {
log.debug("postFlush para entities " + updates.size());
if(noInserts && inserts.size() > 0) {
preFlush(entities);
}
for(Object entity: updates) {
if(!inserts.contains(entity)) {
log.debug("update para " + entity);
((SirgoModelBase)entity).setUModificacion(usuario.getNUsuario());
((SirgoModelBase)entity).setFModificacion(new Date());
}
}
inserts.clear();
updates.clear();
} public void afterTransactionCompletion(Transaction tx) {
log.debug("llamado clear en afterTransactionCompletion");
inserts.clear();
updates.clear();
} public void setUsuario(Usuario usuario) {
this.usuario = usuario;
}
}
Lo único raro es esa variable noInserts, ¿verdad? Pues es que aunque Hibernate es escueto en la descripción de preFlush y postFlush. Creo que debería extender la documentación, porque el comportamiento no es tan evidente como ellos documentan.
Lo que he observado es que cuando los inserts lo son de objetos que están relacionados con objetos existentes, como añadir un hijo a un padre, sólo tiene efecto la modificación de ese objeto a insertar si se realiza en el momento de postFlush.
También he comprobado que en algunos casos, la información de auditoría de postFlush no se guarda en base de datos, pero no he conseguido saber porqué.
Otro tema es cómo configurar el Interceptor. En mi caso, quería que fuera SessionFactory-scope, y tenía configurado que Spring se encargase de instanciar la SessionFactory. He tenido que extender LocalSessionFactoryBean, e inyectar mi interceptor en la configuración.
En la documentación de Hibernate sobre este tema no explica que hay que reconstruir la configuración para que los cambios tengan efecto. Quedaría así:
protected SessionFactory buildSessionFactory() throws Exception {
SessionFactory res = super.buildSessionFactory();
Configuration config = getConfiguration();
config.setInterceptor(auditoriaInterceptor);
this.configurationHolder.setConfig(config);
res = getConfiguration().buildSessionFactory();
return res;
}
En resumen, la documentación de Hibernate es en general buena, pero en relación a los interceptores pienso que debería mejorar.