Hibernate Envers – Ejemplo

Hace unos días me surgió la necesidad de tener que guardar un histórico de todos los cambios que se realizaran en la base de datos, así que investigando como hacerlo descubrí una auténtica maravilla para realizar esta tarea. Se trata de Hibernate Envers, se encarga de ir guardando información histórica de todas tus entidades cada vez que realices cambios sobre ellas y para hacer esto, solo tienes que etiquetar tus entidades para ser auditadas con la anotación @Audited.

Bueno, como el movimiento se demuestra andando, vamos a ver un pequeño ejemplo de cómo utilizarlo.
Para eso vamos a tener una entidad Cliente y una relación 1-N con la entidad Coche, los coches que le hemos vendido a un cliente.
envers1
Si utilizamos Maven, añadimos la siguiente dependencia a nuestro pom.xml

<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-core</artifactId>
	<version>4.1.3.Final</version>
</dependency>
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-entitymanager</artifactId>
	<version>4.1.3.Final</version>
</dependency>
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-envers</artifactId>
	<version>4.1.3.Final</version>
</dependency>

Creamos la clase Cliente

@Entity
@Audited
public class Cliente {

	@Id
	private int idCliente;

	@Column
	private String nombre;

	@OneToMany(mappedBy="cliente")
	private List coches;

	// setter y getters …
}

Y la clase Coche

@Entity
@Audited
public class Coche {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int idCoche;

	@Column
	private String matricula;

	@ManyToOne
	@JoinColumn(name = "idcliente")
	private Cliente cliente;

	// setter y getters …
}

Como podéis ver, son clases JPA normales y corrientes, con la diferencia que le hemos añadido la anotación @Audited
Pues bien, con las dependencias y la anotación de las clases, ya tenemos todo lo necesario para usar Envers, a partir de ahora, cada vez que cambiemos un dato en un objeto de tipo Cliente o Coche, quedará guardado el histórico de dicha modificación. Así de fácil 😀
Si solo hacemos esto, se crearán una tabla terminada en _AUD por cada entidad, donde se guardaran los datos de dicha entidad más un código de revisión.
Vamos a cambiar esta propiedad modificando la estrategia de auditoría de Envers añadiendo lo siguiente a nuestro fichero persitence.xml

<property name="org.hibernate.envers.audit_strategy" value="org.hibernate.envers.strategy.ValidityAuditStrategy" />
<property name="org.hibernate.envers.audit_strategy_validity_store_revend_timestamp" value="true" />
<!-- -->

Con esta estrategia, se añade en las tablas de auditoría información sobre el final de la revisión. Esto hace un poco más lenta la inserción o modificación de datos, pero mucho más rápida la consulta de información de datos históricos. De esta manera nos queda el esquema de la siguiente forma:
envers2
La tabla REVINFO, es utilizada para guardar un identificador por cada revisión junto con su fecha. Una revisión es todos los cambios que se realicen en una transacción.
También tenemos una tabla terminada en _AUD por cada tabla propia. En esta tabla se guardan todos los datos de la tabla original, más los datos propios de Envers, que son:

REV Identificador de la revisión
REVTYPE Tipo de modificación, puede tener los siguientes valores:

  • 0 – Creado
  • 1 – Modificado
  • 2 – Borrado
REVEND Identificador de la siguiente modificación para ese identificador. En caso de ser la última modificación, este campo está a NULL
REVEND_TSTMP TimeStamp con la fecha del final de la revisión. Es la fecha en la que se generó REVEND

Los campos REVEND y REVEND_TSTMP solo existirán si configuramos la estrategia de Envers como comente más arriba, en el persistence.xml

Generar esquema
Para generar el esquema de BBDD, según la documentación, se puede crear con una tarea Ant
Puedes usar este código, pero tiene código Deprecated que desaparecerán en Hibernate 5

Ejb3Configuration jpaConfiguration = new Ejb3Configuration()
	.configure("persistenceUnitName", null);
jpaConfiguration.buildMappings();
Configuration hibernateConfiguration = jpaConfiguration
		.getHibernateConfiguration();
AuditConfiguration.getFor(hibernateConfiguration);
EnversSchemaGenerator esg = new EnversSchemaGenerator(
		hibernateConfiguration);
org.hibernate.tool.hbm2ddl.SchemaExport se = esg.export();
se.setOutputFile("/tmp/schema.sql");
se.setFormat(true);
se.setDelimiter(";");
se.drop(true, false);
se.create(true, false);

O simplemente, añadir esto al persistence.xml para que te genere las tablas al arrancar:

<property name="hibernate.hbm2ddl.auto" value="update" />

Bueno, vale ya de teoría y vamos a ver cómo funciona esto de Envers.

Ya tenemos todo configurado, así que creamos unos cuantos clientes con sus respectivos coches:

// em es el EntityManager
em.getTransaction().begin();

for (int i = 0; i < NUM_CLIENTES; i++) {
	Cliente cliente = new Cliente();
	cliente.setIdCliente(i);
	cliente.setNombre("Cliente" + i);

	for (int j = 0; j < NUM_COCHES; j++) {
		// Una matricula algo chapuza 😉
		String letra = String.valueOf(Character.toChars(65 + j));
		final String matricula = "" + i + i + i + i + "-" + letra;

		Coche coche = new Coche();
		coche.setMatricula(matricula);
		coche.setCliente(cliente);

		cliente.addCoche(coche);
		em.persist(coche);
	}
em.persist(cliente);
}

em.getTransaction().commit();

Como es de esperar, se han grabado en nuestra tabla Cliente los respectivos clientes

IDCLIENTE NOMBRE
0 Cliente0
1 Cliente1

Y él solito, sin decirle nada, ha grabado lo siguiente en la tabla CLIENTE_AUD

IDCLIENTE REV REVTYPE REVEND REVEND_TSTMP NOMBRE
0 1 0 (null) (null) Cliente0
1 1 0 (null) (null) Cliente1

Ha grabado todos los datos de los clientes, más los datos de Envers:

  • REVTYPE 0, quiere decir que se ha dado de alta ese cliente
  • REVEND y REVEND_TSTMP, están a null, porque es la última actualización que se han realizado sobre ese cliente.

En la tabla REVINFO guarda la fecha de esa revisión

REV REVTSTMP
1 1383074696133

Bueno, como es evidente, esto mismo pasa con las tablas COCHE y COCHE_AUD.
Si mostramos los datos del cliente 0, mostrará a ese cliente con sus respectivos coches:

[INFO ] Cliente: Cliente0
[INFO ] Coches ....
[INFO ] Coche 0000-A
[INFO ] Coche 0000-B
[INFO ] Coche 0000-C
[INFO ] Coche 0000-D
[INFO ] Coche 0000-E

Venga, pues vamos a modificar algún dato a ver qué pasa. Vamos a eliminar todos los coches del cliente 0

public void borraCoches(int idCliente) {
	// Obtenemos todos los coches de ese cliente
	TypedQuery<Cliente> queryCliente = em.createQuery(
			"from Cliente c where c.idCliente = :idCliente", Cliente.class);
	queryCliente.setParameter("idCliente", idCliente);

	Cliente c = queryCliente.getSingleResult();

	em.getTransaction().begin();
	List<Coche> lstCoches = c.getCoches();

	for (Iterator<Coche> itCoche = lstCoches.iterator(); itCoche.hasNext();) {
		Coche coche = itCoche.next();
		itCoche.remove();
		em.remove(coche);
	}
	em.merge(c);
em.getTransaction().commit();
}

Si ahora mostramos los datos del cliente 0, mostrará algo así:

[INFO ] Cliente: Cliente0
[INFO ] Coches ....

Como es lógico, ese cliente ya no tiene coches. ¿Y qué ha pasado en las tablas de auditoría?

CLIENTE_AUD

IDCLIENTE REV REVTYPE REVEND REVEND_TSTMP NOMBRE
1 1 0 (null) (null) Cliente1
0 1 0 2 2013-10-29 20:24:56 Cliente0
0 2 1 (null) (null) Cliente0

Se ha creado una revisión nueva para el cliente 0 con REVTYPE 1 (Modificado) y la anterior revisión, se han rellenado los campos REVEND y REVEND_TSTMP con los datos de la siguiente revisión. Ahora si vemos la tabla de auditoria de Coches, vemos lo siguiente:

IDCOCHE REV REVTYPE REVEND REVEND_TSTMP MATRICULA IDCLIENTE
1 1 0 2 2013-10-29 20:24:56 0000-A 0
2 1 0 2 2013-10-29 20:24:56 0000-B 0
3 1 0 2 2013-10-29 20:24:56 0000-C 0
4 1 0 2 2013-10-29 20:24:56 0000-D 0
5 1 0 2 2013-10-29 20:24:56 0000-E 0
1 2 2 (null) (null) (null) (null)
2 2 2 (null) (null) (null) (null)
3 2 2 (null) (null) (null) (null)
4 2 2 (null) (null) (null) (null)
5 2 2 (null) (null) (null) (null)

Vemos que en la revisión 1, campo REV, se crearon 5 coches con REVTYPE 0 (Alta), y en la revisión 2, se han creado otra vez los 5 coches pero con REVTYPE 2 (Borrado).
¿Lioso? Pues no te preocupes, Envers se encargará de todo esto por nosotros.

Hasta ahora hemos visto como Envers guarda los datos, pero ¿Cómo los recuperamos? Para eso disponemos de la clase org.hibernate.envers.AuditReader que nos facilita el recuperar el estado de un objeto determinado en una fecha o revisión.
Por ejemplo, para recuperar la revisión 1 del cliente 0 lo hacemos a través de la función find

….
muestraCliente(0, 1);
…..

// reader es una instancia de AuditReader
public void muestraCliente(int idCliente, Number rev) {
	Date fechaRevision = reader.getRevisionDate(rev);
	Cliente cli = reader.find(Cliente.class, idCliente, rev);
	log.info("Fecha de revision: " + fechaRevision);
	muestraDatos(cli);
}

private void muestraDatos(Cliente c) {
	log.info("Cliente: " + c.getNombre());
	log.info("Coches ....");
	for (Coche coche : c.getCoches()) {
		log.info("\t Coche " + coche.getMatricula());
	}
}

A la función find le pasamos la clase que queremos recuperar, el ID a recuperar y el número de revisión que queremos. El resultado de la ejecución anterior es el siguiente:


[INFO ] Fecha de revision: Tue Oct 29 20:24:56 CET 2013
[INFO ] Cliente: Cliente0
[INFO ] Coches ....
[INFO ] Coche 0000-A
[INFO ] Coche 0000-B
[INFO ] Coche 0000-C
[INFO ] Coche 0000-D
[INFO ] Coche 0000-E

Como vemos, aunque el Cliente 0 ya no tiene coches asociados, en la revisión 1 tenemos todos los coches que tenía en ese momento.
También podemos ver la fecha de creación de un determinado registro:

// reader es una instancia de AuditReader
public void fechaAltaCliente(int idCliente) {
	AuditQuery query = reader.createQuery().forRevisionsOfEntity(
			Cliente.class, false, true);
	query.add(AuditEntity.revisionType().eq(RevisionType.ADD));
	query.addProjection(AuditEntity.revisionNumber().min());
	query.add(AuditEntity.id().eq(idCliente));
		
	Number revision = (Number) query.getSingleResult();
	
	log.info("Cliente creado el: " + reader.getRevisionDate(revision));
}

Resultado:

[INFO ] Cliente creado el: Tue Oct 29 20:24:56 CET 2013

¿Ahora, que pasa si queremos ver todos los Coches que se han borrado hasta la fecha actual?

public void cochesBorrados() {
	AuditQuery query = reader.createQuery().forRevisionsOfEntity(
			Coche.class, false, true);
	query.add(AuditEntity.revisionType().eq(RevisionType.DEL));

	query.add(AuditEntity.revisionNumber().le(
			reader.getRevisionNumberForDate(new Date())));

	List<Object[]> results = query.getResultList();
	for (Object[] obj : results) {
		Coche coche = (Coche) obj[0];
		DefaultRevisionEntity dre = (DefaultRevisionEntity) obj[1];
		RevisionType revType = (RevisionType) obj[2];

		log.info("Coche borrado " + coche.getIdCoche() + " en fecha "
				+ dre.getRevisionDate());
		log.info("Matricula: " + coche.getMatricula());
	}
}

Aquí nos construimos un objeto AuditQuery para recuperar objetos de tipo Coche y le añadimos un par de condiciones, la primera es que solo me devuelva objetos cuya revisión sea borrado ( RevisionType.DEL ) y la segunda, que sean todas las revisiones cuya fecha sea menor a la actual.

El método AuditQuery.getResultList, devuelve una lista de objetos con todos los registros encontrados, cada elemento de la lista, es un array de 3 objetos, el elemento 0 es nuestro objeto JPA, el elemento 1 es un objeto del tipo DefaultRevision y el objeto 2 es del tipo RevisionType.
Si ejecutamos esto nos encontramos con esta sorpresa:


[INFO ] Cliente creado el: Tue Oct 29 20:24:56 CET 2013
[INFO ] Coche borrado 1 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: null
[INFO ] Coche borrado 2 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: null
[INFO ] Coche borrado 3 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: null
[INFO ] Coche borrado 4 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: null
[INFO ] Coche borrado 5 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: null

¿Por qué el atributo matricula es null? Como vimos antes, cuando un objeto se borra, se guardan todos los atributos a null. Pero no hay problema, para recuperar los datos que tenía un registro cuando fue borrado, lo que tenemos que hacer es un pequeño truco, es recuperar el registro de auditoria justo anterior a ser borrado:

public void cochesBorrados() {
	AuditQuery query = reader.createQuery().forRevisionsOfEntity(
			Coche.class, false, true);
	query.add(AuditEntity.revisionType().eq(RevisionType.DEL));

	query.add(AuditEntity.revisionNumber().le(
			reader.getRevisionNumberForDate(new Date())));

	List<Object[]> results = query.getResultList();
	for (Object[] obj : results) {
		Coche coche = (Coche) obj[0];
		DefaultRevisionEntity dre = (DefaultRevisionEntity) obj[1];
		RevisionType revType = (RevisionType) obj[2];

		log.info("Coche borrado " + coche.getIdCoche() + " en fecha "
				+ dre.getRevisionDate());

		AuditQuery queryAux = reader.createQuery().forRevisionsOfEntity(
				Coche.class, false, true);
		queryAux.add(AuditEntity.revisionType().ne(RevisionType.DEL));
		queryAux.addProjection(AuditEntity.revisionNumber().max());
		queryAux.add(AuditEntity.id().eq(coche.getIdCoche()));
			
		Number idUltimaRevision = (Number) queryAux.getSingleResult();
		Coche cocheAux = reader.find(Coche.class,
				coche.getIdCoche(), idUltimaRevision);
			
		log.info("Matricula: " + cocheAux.getMatricula());
	}
}

Eso lo hacemos entre las líneas 18 y 22. Y el resultado es el siguiente:


[INFO ] Cliente creado el: Tue Oct 29 20:24:56 CET 2013
[INFO ] Coche borrado 1 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: 0000-A
[INFO ] Coche borrado 2 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: 0000-B
[INFO ] Coche borrado 3 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: 0000-C
[INFO ] Coche borrado 4 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: 0000-D
[INFO ] Coche borrado 5 en fecha Tue Oct 29 20:24:56 CET 2013
[INFO ] Matricula: 0000-E

En fin, Hibernate Envers ha sido un gran descubrimiento para mí, sencillo y potente. Por supuesto con Envers puedes hacer mucho más, pero aquí ya os dejo una buena introducción de como poder utilizarlo.

El código fuente completo del ejemplo lo podéis descargar de aquí : pruebaenvers o directamente zip

Anuncios

5 Responses to Hibernate Envers – Ejemplo

  1. Jesus says:

    Hola, excelente tutorial. Pero me podrias ayudar con un problema? La tabla REVINFO no se genera automáticamente y no e deja insertar datos porque pide dicha tabla. La estrategia esta implementada tal cual.

    • stufftic says:

      Puedes añadir la siguiente linea en el fichero persistence.xml

      <property name=”hibernate.hbm2ddl.auto” value=”update” />

      • Jesus says:

        Hola, gracias! Era un error con el hibernate.dialect, estoy usando sql server y pues habia dejado el que venia en el archivo. Ya me funciona! Aprovecho para preguntarte si es posible generar esas tablas de auditorias en un esquema aparte. Tener las tablas _aud en otra base de datos. Gracias

      • stufftic says:

        Según la documentación si se puede, pero nunca lo he probado.

        Tienes la anotacion @AuditTable(schema=”…”) o la propiedad org.hibernate.envers.default_schema para indicar el nombre del esquema.

  2. Jesus says:

    Muchas gracias de verdad por la atención, el tuto esta super y me ha servido de mucho! Saludos

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: