Auditoría de entidades con Hibernate Envers y Spring Data JPA.

1
16003

En este tutorial veremos cómo configurar un sistema de auditoría de cambios en las entidades
de nuestra capa de persistencia con el soporte de Hibernate Envers y Spring Data JPA.

Auditoría de entidades con Hibernate Envers y Spring Data JPA.

0. Índice de contenidos.


1. Introducción

Antes o después, en cualquier aplicación de gestión empresarial, se plantea la necesidad
de mantener un histórico de quién ha hecho qué, cuándo y desde dónde con la información
que se mantiene en nuestro sistema. No solo por aspectos legales, sino de simple control de cambios, en algún momento nos
pedirán mantener una auditoría de cambios sobre esa información.

A nivel técnico podemos implementarlo de muchas maneras:

  • a bajo nivel directamente en la base de datos con triggers,
  • enganchándonos con los eventos del ORM que nos de soporte a persistencia,
  • con el soporte de AOP si usamos algún framework que nos de soporte para ello,
  • ensuciando el código con consultas innecesarias, consumo de memoria y objetos en sesión para realizar
    comparaciones y la persistencia a mano,…

Entendiendo que la última opción es inadmisible, en este tutorial vamos a ver cómo usando Hibernate o
el soporte de éste para JPA, podemos implementar un control de cambios de una manera muy limpia y poco intrusiva en
nuestro código, con la extensión Hibernate Envers.

Para enriquecer un poco más el tutorial además harenos uso de la librería Spring Data Envers, que extiende
los repositorios de Spring Data para permitir, de una manera sencilla, el acceso al histórico de modificaciones de una entidad.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • Hibernate 4.3.11.Final
  • Spring Data 1.10.2.RELEASE
  • Spring Data Envers 1.0.2.RELEASE


3. Estrategia de versionado.

Hibernate Envers extiende el soporte por defecto del ORM para engancharse en el ciclo de vida de persistencia de una entidad;
sabiendo que una entidad está marcada como detached y los cambios realizados sobre la misma, puesto que tiene que
realizar un update de esos cambios, «aprovecha» para almacenar la información de los mismos en una tabla de auditoría.

Esa estrategia pasa por mantener una tabla espejo de la original añadiendo dos columnas adicionales por defecto:

  • REV: identificador de la revisión
  • REVTYPE: tipo de revisión (1 inserción, 2 modificación o 3 borrado)

El identificador de la revisión mantiene una clave foránea con una tabla con el histórico de información
de todas las revisiones que podemos enriquecer con la información que estimemos necesaria: timestamp, user_name, remote_address,…

hibernate-envers-spring-data-01

Si estamos usando el soporte de Hibernate para generar el modelo en nuestro entorno de tests, para crear las tablas automáticamente
solo debemos hacer uso de la propiedad: <property name=»hibernate.hbm2ddl.auto» value=»update» />


4. Configuración.

Lo primero, como no podía ser de otra forma es añadir las dependencias de las librerías que vamos
a necesitar en nuestro pom.xml:

  <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-jpa</artifactId>
      <version>1.10.2.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-envers</artifactId>
    <version>1.0.2.RELEASE</version>
  </dependency>

Es importante la correlación de versiones, una versión inferior de la librería spring-data-jpa hará que
los repositorios no funcionen correctamente.

Desde el punto de vista estríctamente de Hibernate Envers lo único que debemos hacer es configurar, si lo estimamos necesario,
las propiedades que nos permiten renombrar las tablas o sufijos de las mismas donde se realizará la auditoría.
Así, en la declaración de la factoría de entityManagers para JPA.

  <bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="rbacDataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="databasePlatform" value="${hibernate.dialect}" />
        <property name="showSql" value="${hibernate.show_sql}" />
      </bean>
    </property>
    <property name="jpaProperties" >
      <map>
        <entry key="org.hibernate.envers.audit_table_suffix" value="_AUDIT" />
        <entry key="org.hibernate.envers.revision_field_name" value="REVISION_ID" />
        <entry key="org.hibernate.envers.revision_type_field_name" value="REVISION_TYPE" />
      </map>
      </property>
    <property name="packagesToScan"
      value="com.autentia.tnt.persistence.**.domain" />
  </bean>

Desde el punto de vista de los repositorios de Spring Data debemos añadir la siguiente
configuración:

<jpa:repositories entity-manager-factory-ref="entityManagerFactory"
  base-package="com.autentia.tnt.persistence.**.repository"
  transaction-manager-ref="transactionManager"
  factory-class="org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean"/>

Cuando se generen las implementaciones de los repositorios, la factoría EnversRevisionRepositoryFactoryBean
añadirá las implementación de las operaciones necesarias para recuperar el listado de revisiones
de una entidad y permitirá el acceso al detalle de una revisión concreta.


5. Uso.

Para enriquecer la tabla de auditoría debemos primero mapearla añadiendo una
entidad como la que sigue, con las propiedades que estimemos necesarias para almacenar:

  package com.autentia.tnt..persistence.domain;

  import java.io.Serializable;
  import java.util.Date;

  import javax.persistence.Column;
  import javax.persistence.Entity;
  import javax.persistence.GeneratedValue;
  import javax.persistence.GenerationType;
  import javax.persistence.Id;
  import javax.persistence.SequenceGenerator;
  import javax.persistence.Table;
  import javax.persistence.Temporal;
  import javax.persistence.TemporalType;

  import org.hibernate.envers.DefaultRevisionEntity;
  import org.hibernate.envers.RevisionEntity;
  import org.hibernate.envers.RevisionNumber;
  import org.hibernate.envers.RevisionTimestamp;

  @Entity
  @Table(name="REVISION_INFO", schema="TEST")
  @RevisionEntity(CustomRevisionListener.class)
  public class Revision implements Serializable {

  	@Id
  	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="revision_seq")
  	@SequenceGenerator(
  		name="revision_seq",
  		sequenceName="rbac.seq_revision_id"
  	)
  	@RevisionNumber
  	private int id;

  	@Column(name="REVISION_DATE")
  	@Temporal(TemporalType.DATE)
  	@RevisionTimestamp
  	private Date date;

  	@Column(name="USER_NAME")
      private String userName;

      public void setUserName(String userName) {
  		this.userName = userName;
  	}

      public String getUserName() {
  		return userName;
  	}

      public Date getDate() {
  		return date;
  	}

  }

Con la anotación @RevisionNumber marcamos la propiedad con la clave única de la revisión y
con la anotación @RevisionTimestamp la propiedad que almacenará la fecha de modificación.

Con la anotación @RevisionEntity haremos referencia a un listener que se ejecutará
previo a las operaciones de auditoría que será el que realmente dote de contenido
dicha información.

Como se puede ver a continuación podemos añadir información sobre el usuario
obteniéndolo del contexto.

package com.autentia.tnt.persistence.domain.listeners;

import org.hibernate.envers.RevisionListener;

import com.autentia.tnt.context.AccountThreadContext;

public class CustomRevisionListener implements RevisionListener {

	public void newRevision(Object revisionEntity) {
		final Revision revision = (Revision) revisionEntity;
		revision.setUserName(getThreadAccountUserName());
    }

	private String getThreadAccountUserName() {
		if (AccountThreadContext.getAccount() != null){
			return AccountThreadContext.getAccount().getCode();
		}
		return "NOT_FOUND";
	}

}

Lo interesante es que este listener es capaz de engancharse con Spring Security para recuperar información
del usuario conectado, así como de la ip remota de acceso.

En este punto ya podemos añadir la configuración necesaria a nuestras entidades para auditarlas, bastaría
la anotación @Audited, como se puede ver a continuación:

@Entity
@Table(name="GROUPS_VERSION", schema="RBAC")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
@AuditTable(value="GROUPS_VERSION_AUDIT", schema="TEST")
public class Book extends BaseDomain{

A más podemos indicar que solo audite las relaciones de la entidad, no las entidades relacionadas con targetAuditMode = RelationTargetAuditMode.NOT_AUDITED.

También podemos renombrar la estrategia por defecto de nombres indicando con la anotación @AuditTable el nombre y esquema de la tabla de auditoría.

Por último, para habilitar el acceso a las operaciones de recuperación de información en el
repositorio de Spring Data basta con extender de la interfaz RevisionRepository.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.history.RevisionRepository;
import org.springframework.data.repository.query.Param;

import com.autentia.tnt.persistence.domain.Book;

public interface BookRepository extends RevisionRepository, JpaRepository {

}

Esta interfaz añade operaciones como las siguientes:

hibernate-envers-spring-data-02

6. Tests.

Como no podía ser de otra forma comprobaremos que todo funciona correctamente con un
test de integración que validará que la información de las revisiones
se almacena correctamente.

Podría tener un código como el siguiente:

  @Test
	public void shouldPersistRevisionInfo() throws Exception {

		TestTransaction.flagForCommit();

		final Book book = repository.findById(ID);

    book.setSomeThing("SOME_THING");

		repository.save(book);

		TestTransaction.end();

		TestTransaction.start();
		final List> revisions = repository.findRevisions(ID).getContent();
		assertThat(revisions.size(), equalTo(1));

		TestTransaction.end();

	}

Tres cuestiones a destacar:

  • con la operación save se almacenará no solo la información
    de la entidad sino de la revisión, pero al enganchar con el ciclo de vida de
    las operaciones de persistencia solo será efectiva cuando se haga un flush o se fuerce un commit.
  • en el entorno de tests con el soprote de Spring podemos hacer uso de las operaciones de la clase de utilidades TestTransaction para forzar la apertura y cierre de transacciones.
  • en los repositorios de Spring Data habilitados como hemos visto ahora tendemos operaciones del tipo repository.findRevisions(…)
    para recuperar la información del histórico de revisiones.


7. Referencias.


8. Conclusiones.

Ahora solo nos faltaría programar un proceso de historificación de los datos de las tablas de auditoría… ups!

Un saludo.

Jose

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad