Hibernate Envers : Audit et Suivi de version

Hibernate Envers permet de tracer les modifications sur les objets métiers d'une application mappés en base de données. Le suivi de modifications repose sur le principe de révisions. Chaque sauvegarde (transaction commitée) donne lieu à la création d'une nouvelle version qui regroupe l'ensemble des données modifiées.

Chaque entité auditée va être représentée par deux tables :

  • Une table pour les données actuelles de l'entité
  • Une table d'historique contenant le suivi des modifications de l'entité

L'exemple suivant montre les étapes à suivre pour mettre en place Hibernate Envers.

Avant toute chose, il faut récupérer le module Envers d'Hibernate. Télécharger le jar hibernate-envers-3.6.4.Final.jar ou ajouter la dépendance suivante dans le pom :

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>3.6.4.Final</version>
    <scope>compile</scope>
</dependency>

A partir de là, la mise en place d'Hibernate Envers se résume en deux points :

  • Une annotation sur les beans métiers à auditer : @Audited
  • La déclaration des listeners spécifiques dans la configuration Hibernate

Dans notre exemple, 2 entités sont auditées : Livre et Critique
Exemple

@Entity
@Audited
public class Livre extends BeanObject {

   // Titre
   private String titre;

   // Liste de critiques.
   @OneToMany(mappedBy = "livre", cascade = CascadeType.ALL, orphanRemoval = true)
   @LazyCollection(LazyCollectionOption.TRUE)
   private Set<Critique> critiques = new HashSet<Critique>();
}

@Entity
@Audited
public class Critique extends BeanObject {

   // Libellé
   private String libelle;

   // Livre
   @ManyToOne(optional = false)
   @JoinColumn(name = "id_livre")
   private Livre livre;
}

C'est dans le fichier de configuration persitence.xml que les listeners spécifiques à Envers doivent être déclarés :

<persistence 
 xmlns="http://java.sun.com/xml/ns/persistence"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
 version="1.0">
	
 <persistence-unit name="hibernateEnvers" transaction-type="RESOURCE_LOCAL" >
     <properties>
       <!-- Autodetection des classes annotées @Entity -->
       <property name="hibernate.archive.autodetection" value="class"/>
       <!-- Configuration Envers -->
       <property name="hibernate.ejb.event.post-insert"
         value="org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener" />
       <property name="hibernate.ejb.event.post-update"
         value="org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener" />
       <property name="hibernate.ejb.event.post-delete"
         value="org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener" />
       <property name="hibernate.ejb.event.pre-collection-update"
         value="org.hibernate.envers.event.AuditEventListener" />
       <property name="hibernate.ejb.event.pre-collection-remove"
         value="org.hibernate.envers.event.AuditEventListener" />
       <property name="hibernate.ejb.event.post-collection-recreate"
         value="org.hibernate.envers.event.AuditEventListener" />			
   </properties>
 </persistence-unit>
		
</persistence>

Tout est désormais prêt pour gérer l'audit sur les livres et critiques.
Le code suivant permet de tester la création, la modification et la suppression d'un livre.

/**
 * Méthode de création d'un livre avec une critique.
 */
@Test
public void createLivre() {
    final Livre livre = new Livre();
    livre.setTitre("Hibernate Envers");
    final Critique critique = new Critique();
    critique.setLivre(livre);
    critique.setLibelle("Vraiment top");
    livre.getCritiques().add(critique);
    final Livre result = this.livreDao.save(livre);
    Assert.assertNotNull(result);
}

/**
 * Méthode de modification de livre.
 */
@Test
public void modifyLivre() {
    final Livre livre = this.livreDao.get(1L);
    livre.setTitre("Hibernate Envers - Nouvelle Version");
    final Livre result = this.livreDao.save(livre);
    Assert.assertNotNull(result);
}

/**
 * Méthode de suppression de livre.
 */
@Test
public void removeLivre() {
    final Livre livre = this.livreDao.get(1L);
    this.livreDao.remove(livre);
}

Côté base de données, il y a 5 tables :

  • Une table critique
  • Une table critique_aud
  • Une table livre
  • Une table livre_aud
  • Une table revision

A la fin du scénario de création, modification et suppression de livre, les tables "livre" et "critique" sont vides.
Les tables "livre_aud" et "critique_aud" contiennent l'historique des opérations effectuées.
Table livre_aud

Table critique_aud

Le champ revtype indique la nature de l'opération effectuée sur l'entité. Ce champ peut prendre 3 valeurs :

  • La valeur 0 indique une création
  • La valeur 1 indique une modification
  • La valeur 2 indique une suppression

Le champ rev contient l'identifiant de la révision à l'origine de l'opération.
Une table révision est présente en base de données. Par défaut, elle contient un identifiant et une date mais il est possible d'y ajouter des informations supplémentaires. Dans notre exemple, nous avons voulu rajouter l'identité de la personne à l'origine de l'opération.
Table revision

Pour ajouter le stockage d'informations spécifiques dans une révision, il faut :

  • Sous classer le bean de stockage par défaut d'Hibernate Envers (DefaultRevisionEntity) en y ajoutant les informations (dans notre cas le nom de l'utilisateur)
  • Ecrire un listener spécifique en charge de populater les champs supplémentaires à chaque création de révision
// Entité utilisée pour la gestion des audits (création d'une révision)
@Entity(name = "revision")
@RevisionEntity(AuditListener.class)
public class Revision extends DefaultRevisionEntity {

  // Nom de l'utilisateur
  private String username;

  // Getters / Setters
}

// Listener spécifique appelé dans la gestion des audits des entités
public class AuditListener implements RevisionListener {
  /**
   * {@inheritDoc}
   */
  @Override
  public void newRevision(final Object revisionEntity) {
      final Revision revision = (Revision) revisionEntity;
      // Récupération de l'utilisateur connecté
      final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      revision.setUsername(auth.getName());
  }
}

Hibernate Envers propose également une interface AuditReader pemettant de :

  • Récupérer toutes les révisions effectuées sur une entité
  • Récupérer l'état d'une entité associé à une révision spécifique
// Méthode en charge de retourner la factory de AuditReader
private AuditReader getAuditReader() {
  return AuditReaderFactory.get(this.entityManager);
}

// Méthode de récupération des révisions d'un objet en fonction d'un id
public List<Number> getRevisions(final Long id) {
  return this.getAuditReader().getRevisions(this.persistentClass, id);
}

// Méthode de récupération d'un objet en fonction d'un id et d'une révision
public BEAN retrieve(final Long id, final Number revision) {
  return this.getAuditReader().find(this.persistentClass, id, revision);
}

L'ensemble du code de l'exemple est téléchargeable dans le fichier zip attaché à ce post : hibernate-envers-exemple.zip.

La documentation officielle Hibernate Envers est disponible sur le site de jboss en cliquant ici.
Beaucoup d'éléments sont configurables : le nom des tables d'historique, le nom du schéma/catalogue où stocker ces tables...

2 commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.