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
@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.
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.
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...
Bravo pour la qualité de ce post !
Super article. Ça donne vraiment envie d’essayer