fr.romain:blog:3.0

Un blog qu'il est bien pour le Java

Devoxx 2012 - Présentation d'Hibernate Envers

| Comments

Hibernate Envers

Moi, à Devoxx

Difficile de ne pas parler de la meilleure conférence à Devoxx celle présentée par Romain Linsolas sur Hibernate Envers. Bien entendu, le fait que ce soit moi n’a aucune incidence sur cette considération :) Trève de plaisanterie. J’ai donc présenté un Quickie, à savoir 15 minutes, sur cette librairie d’Hibernate qui permet d’auditer ses entités (classes de persistence). Par audit on entend la conservation en base des enregistrements à chaque fois qu’une modification y est apportée. Voyez cela comme Subversion par exemple : si je commite une nouvelle version d’un fichier, alors SVN va conserver son historique, et permettre de “remonter” dans le temps et de voir les évolutions apportées à ce fichier.

Ma présentation s’est donc déroulée en 4 chapitres.

Activer Envers

Tout d’abord, le plus simple, consiste à activer Envers, ce qui se fait extrêmement facilement en ajoutant simplement la librairie dans le classpath (ajout de la dépendance hibernate:hibernate-envers dans son pom.xml par exemple). C’est tout. On notera toutefois qu’il est nécessaire d’utiliser une version 3 ou 4 d’Hibernate ainsi que d’Hibernate Annotations, Envers ne supportant pas (encore ?) la configuration par XML.

Démarrer l’audit

Ensuite, on passe à l’audit à proprement parlé. Du côté Java, c’est très simple, l’annotation principale étant @Audited, qui indiquera à Envers qu’il faudra auditer cette entité. Voilà un petit exemple d’entité auditée :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Audited
@Entity
@Table(name = "T_PERSON")
public class Person {

    @Id @GeneratedValue
    private int id;

    private String name;

    private String surname;

    @NotAudited
    private String comments;

    // Getter, setter, etc.

}

Le @NotAudited permet d’exclure complètement le champ de l’audit : non seulement Envers ne conservera pas sa valeur dans la table d’audit, mais également si c’est la seule valeur qui est modifiée dans un update de l’enregistrement, alors Envers n’ira pas ajouter de nouvelle révision dans la piste d’audit.

Une nouveauté apparue a priori récemment (c’est d’ailleurs une fonctionnalité “expérimentale”) dans Envers est la possibilité de tracer quels champs ont été modifiés. Pour cela, on ajoutera au choix la propriété suivante :

1
<property name="org.hibernate.envers.global_with_modified_flag" value="true"/>

pour avoir la fonctionnalité globalement, ou alors on choisira au cas par cas les champs à suivre, comme ceci :

1
2
@Audited(withModifiedFlag = true)
private String monChamp;

Du côté de la base de données, voilà comment ça se passe. Envers nécessite une table d’audit par entité auditée. Par exemple, si j’audite ma table T_PERSON, j’aurais alors besoin d’une table T_PERSON_AUD (par défaut, Envers ajoute _AUD à la fin du nom de la table). Cette table d’audit est un quasi-clône de la table originelle, à quelques exceptions près :

  • Elle contient 2 champs supplémentaires, à savoir REV, qui est l’ID de la révision, et REVTYPE qui contient le type d’opération qui a créé la révision (0 pour une addition, 1 pour une modification, 2 pour une suppression).
  • Les contraintes ne sont plus les mêmes, parce que lorsque l’on supprime une donnée, on créera une révision avec tous les champs vides (à l’exception de la clé primaire de l’objet supprimé). Attention donc avec les not null !
  • La clé primaire de cette table d’audit sera la même clé primaire que celle d’origine, à laquelle on ajoutera le champ REV.
  • Dans le cas où l’on active la fonctionnalité du traçage des champs modifiés, il faudra ajouter pour chaque champ concerné (donc tous si la fonctionnalité est activée de façon globale) un champ xxx_MOD qui pourra valoir 0 (le champ n’a pas été modifié) ou 1 (le champ a été modifié).
  • Optionnellement, tout champ dont la propriété liée est marquée comme non auditée (@NotAudited) peut être supprimé de la table d’audit.

Envers a également besoin d’une table globale pour stocker les informations de révision. Cette table, nommée REVINFO, ne contient initiallement que 2 champs : REV qui est l’ID de la révision (et que l’on retrouve dans toutes les tables d’audit), ainsi que REVTSTMP. Il est toutefois possible d’ajouter des informations pour cette table, voici comment y ajouter le nom de l’utilisateur connecté qui a déclenché la révision. Tout d’abord, il faut créer une entité pour cela, étendant simplement DefaultRevisionEntity :

1
2
3
4
5
6
7
@Entity
@RevisionEntity(UsernameRevisionListener.class)
public class MyEntityRevision extends DefaultRevisionEntity {

  private String username;  // + getter / setter

}

Cette entité est liée à un listener qui sera appelé à la création de chaque nouvelle révision. Le listener s’écrit ainsi (la méthode getCurrentUsername() est à écrire soi-même, mais généralement le container - comme Spring MVC - propose des fonctionnalités pour ça) :

1
2
3
4
5
6
7
8
9
public class UsernameRevisionListener implements RevisionListener {

  @Override
  public void newRevision(Object revisionEntity) {
      String theUser = getCurrentUsername();
      ((MyEntityRevision) revisionEntity).setUsername(theUser);
  }

}

Requêter les données d’audit

Créer les données d’audit c’est bien, les utiliser c’est encore mieux. Heureusement Envers dispose d’une API pour ça. Voyons ça avec quelques exemples. Tout d’abord, nous voulons récupérer la liste des révisions pour une entité donnée, puis on affichera l’historique de cette entité :

1
2
3
4
5
6
7
8
int personId = somePerson.getId();
AuditReader auditReader = AuditReaderFactory.get(entityManager);
List<Number> allRevisions = auditReader.getRevisions(Person.class, personId);

for (Number n: allRevisions) {
  Person p = auditReader.find(Person.class, personId, n);
   System.out.printf("\t[Rev #%1$s] > %2$s\n", n, p);
}

Le résultat obtenu est le suivant :

1
2
3
[Rev #1] > Person { id=10, name='Romain', surname='', comments=''}
[Rev #3] > Person { id=10, name='Romain', surname='Linsolas', comments=''}
[Rev #4] > null

On devine ainsi que l’on a créé l’entité “Romain”, puis qu’on lui a affecté une valeur pour le surname, pour enfin la supprimer (d’où le null lors de la 4e révision). Envers propose également la classe AuditQuery qui permet de requêter plus précisément les données d’audit. Ici, nous allons récupérer toutes les entités modifiées lors d’une révision donnée (disons 42) :

1
2
AuditQuery query1 = auditReader.createQuery().forEntitiesAtRevision(Person.class, 42);
List<Person> persons = query1.getResultList();

Le résultat :

1
2
Person { id=11, name='Chuck', surname='Norris', comments=''}
Person { id=10, name='Romain', surname='Linsolas', comments=''}

On voit que l’on a 2 entités qui ont été modifiées. Jusqu’à présent, nous avons utilisé l’API pour récupérer les entités telles qu’elle étaient à un moment donnée dans leur histoire. Mais nous n’avons pas d’informations quant à la modification qu’ils ont subi. C’est bien entendu possible :

1
2
AuditQuery query2 = auditReader.createQuery().forRevisionsOfEntity(Person.class, false, true);
List<Object[]> revisions = query2.getResultList();

Le résultat sera donc une liste de tableaux d’objets. Chaque tableau contient 3 éléments : l’entité elle-même, l’objet représentant la révision (avec l’ID et le timestamp, ainsi que des informations additionnelles si cela avait été paramétré comme nous l’avons vu précédemment), et enfin le type de révision. Le retour du code ci-dessus sera celui-ci :

1
2
3
4
| Person { id=10, name='Romain', surname='', comments='' }         | EntityRevision {id=1, timestamp=1352936106653, username='Devoxx'} | ADD |
| Person { id=11, name='Chuck', surname='Norris', comments='' }    | EntityRevision {id=1, timestamp=1352936106669, username='Devoxx'} | ADD |
| Person { id=10, name='Romain', surname='Linsolas', comments='' } | EntityRevision {id=1, timestamp=1352936106687, username='Devoxx'} | MOD |
| Person { id=10, name='', surname='', comments='' }               | EntityRevision {id=1, timestamp=1352936106734, username='Devoxx'} | DEL |

L’intérêt de la classe AuditQuery est qu’elle propose une API pour affiner sa requête. Par exemple :

1
2
3
4
5
6
7
List<Person> persons = auditReader.createQuery()
      .forEntitiesAtRevision(Person.class, 42)
      .addOrder(AuditEntity.property("surname").desc())
      .add(AuditEntity.relatedId("address").eq(theAddressId))
      .setFirstResult(4)
      .setMaxResults(2)
      .getResultList();

Si vous connaissez Criteria, alors vous êtes en terrain connu. Bien entendu, cette API gère aussi la fonctionnalité de traçage des champs modifiés :

1
2
3
4
AuditQuery query = auditReader().createQuery()
      .forEntitiesAtRevision(Person.class, 42)
      .add(AuditEntity.property("surname").hasChanged())
      .add(AuditEntity.property("name").hasNotChanged());

Démonstration

La dernière partie de ce Quickie est une courte démonstration. Le code est récupérable sur GitHub : https://github.com/linsolas/devoxx-envers Dans ce petit projet, je démarre une base en mémoire (H2) contenant 3 tables (T_PERSON, T_PERSON_AUD et REVINFO), puis je réalise quelques opérations basiques de CRUD sur quelques entités. Entre chaque étape, j’affiche le contenu de la base. A noter que j’utilise le projet p6spy pour logguer les requêtes exactes envoyées à la base.

Photo tirées des photos officielles de Devooxx

Comments