Adam Warski de la société Level N Consulting a présenté Hibernate Envers lors d’une présentation courte de 20 minutes à Jazoon. Petit retour sur Envers, vous allez tout comprendre.

Hibernate Envers est un système simple qui permet d’ajouter de l’Audit et la gestion de Version à des Entités Hibernate. L’audit est l’enregistrement automatique en base de données des différentes versions d’une entité. Envers permet aussi de loguer les modifications faites à une Entité Hibernate dans une base de données, pour stocker l’heure et le nom de la personne qui a changé une Entité par exemple.

L’utilisation est simple. Pour cela, il suffit d’ajouter l’annotation @Audited et de déclarer des listeners dans le fichier persistence.xml. Pour chaque entité auditée, une nouvelle table est créée qui va conserver l’historique des changements, un peu comme Subversion si vous voulez. Cela permet aussi de retrouver facilement l’historique des différentes versions d’une entité. Imaginez une Facture, ce système vous permet d’enregistrer les différentes versions de la Facture, sans aucuns efforts.
Sur cette photo prise durant la présentation, on voit l’annotation et la déclaration dans la configuration Hibernate.
img_envers

Que peut-on auditer?
Les types simples définis par JPA (String, Integer, Date), les composants et les relations entre les composants. Le support des Collections et des types spécifiques à Hibernate est aussi possible.

Voyons avec un peu de code comment cela fonctionne.
Si vous souhaitez auditer l’ensemble d’une Entité :


@Entity
@Audited // that's the important part :)
public class Person {
    @Id
    @GeneratedValue
    private int id;

    private String name;

    private String surname;

    @ManyToOne
    private Address address;

    // add getters, setters, constructors, equals and hashCode here
}

La déclaration s’effectue de la façon suivante si vous ne souhaitez auditez que certaines propriétés :

@Entity
public class Person {
    @Id
    @GeneratedValue
    private int id;

    @Audited
    private String name;

    @Audited
    @ManyToOne
    private Address address;

    // we don't care about this in Audit
    private String twitterName;
    private Date dateOfBirth;

}

Le développeur sélectionne les entités qu’il souhaite faire auditer. Pour chaque entité, une nouvelle entité auditée est créée dynamiquement. Par exemple Address aura une entité Address_AUD pour l’Audit, et c’est bien cette deuxième entité qui est persisté en base… avec Hibernate.

Pour chaque inser, update ou delete, une nouvelle révision est créée. Une Transaction est égale à une révision, si plusieurs entités sont embarquées dans une transaction. Le numéro de version est global si vous auditez plusieurs entités, comme le numéro SVN de votre code. Imaginez qu’une entité est un fichier Java que vous commitez, c’est exactement pareil.

Imaginons une entité Person et une entité Address, toutes les 2 annotées:
envers_uml

Déclarons tout d’abord 2 addresses sur une personne John Doe :

// Revision 1
em.getTransaction().begin();
Address a1 = new Address(“West st.”, 10);
Address a2 = new Address(“East st.”, 15);

Person p = new Person(“John”, “Doe”);
p.setAddress(a1);
entityManager.persist(a1);
entityManager.persist(a2);
entityManager.persist(p);
em.getTransaction().commit();

Si je modifie maintenant le nom de la personne et que j’utilise la deuxième adresse :

// Revision 2
em.getTransaction().begin();
p = entityManager.find(Person.class, id);
p.setName(“Paul”);
p.setAddress(a2); // declared in the other code part

em.getTransaction().commit();

Comme vous pouvez le constater, l’utilisation d’Envers est transparente pour le développeur, aucun bout de code supplémentaire n’est ajouté.
Voyons maintenant comment retrouver nos données. Je recharge la révision 1 et je vérifie que le nom de la personne est bien John, puis ensuite comme on le voit, le chargement de l’arbre de relation vers Address fonctionne sans problèmes.

AuditReader ar = AuditReaderFactory.get(em);

// Reading the person at revision 1
old_p = ar.find(Person.class, id, 1);
assert “John”.equals(old_p.getName());
assert a1.equals(old_p.getAddress());

// Reading the addresses at revision 1
old_a1 = ar.find(Address.class, a1_id, 1);
assert old_a1.getPersons().size() == 1;
assert old_a1.getPersons().contains(p);
old_a2 = ar.find(Address.class, a2_id, 1);
assert old_a2.getPersons().size() == 0;

La recherche est aussi très puissante et les systèmes de requêtes d’Hibernate permettent de déclarer par exemple:

auditReader.createQuery()
                   .forRevisionsOfEntity(Person.class, false, true)
                   .addProjection(AuditEntity.revisionNumber.count())
                   .add(AuditEntity.id().eq(person.getId()))
                   .getSingleResult()

Il est important de se souvenir que l’audit est effectué par une autre Entité Hibernate, et que la table originale n’est jamais affectée.

envers2

Comment écouter les changements sur une entité ?
Envers permet aussi de déclarer un Listener qui sera notifié lorsque l’entité est persistée vers la base de données. Je reprends le code directement des slides de la présentation, ne croyez pas que j’ai réussi à refaire cet article sans avoir les slides de la présentation 😉

Voyons tout d’abord une Entité de Révision. Elle est annotée avec @RevisionEntity, il faut aussi annoter un timestamp avec @RevisionTimeStamp et un numéro de révision, avec @RevisionNumber comme sur cet exemple:

@Entity
@RevisionEntity(ExampleListener.class)
public class ExampleRevEnt {
    @Id @GeneratedValue @RevisionNumber private Long id;
    @RevisionTimestamp private Long timestamp;

    @ManyToOne private User modifiedBy;

    // Getters, setters, equals, hashCode ...
}

Vous pouvez ensuite déclarer un RevisionListener qui sera activé lorsque l’entité décorée est persistée, afin par exemple d’ajouter le nom de l’utilisateur qui a changé une Entité dans votre interface Web. Cela permet de créer une fonction de Log rapidement, sans alourdir le reste de votre application.

public class ExampleListener implements RevisionListener {
    public void newRevision(Object revEntity) {
        ExampleRevEnt revEnt = (ExampleRevEnt) revEntity;
        User currentUser = (User) Component.getInstance("currentUser");


        revEnt.setModifiedBy(currentUser);
    }
}

Pour plus de détails sur cette fonctionnalité : voir la documentation d’Envers

En conclustion
Envers 1.2.1 GA est sorti il y a quelques semaines, c’est un projet qui fonctionne avec Hibernate 3.3 et donc Java 5. Je retiendrai la facilité de mise en place, la clareté de l’API. Pour peu que votre application nécessite de l’audit, il serait dommage de s’en passer.
Le site officiel : http://www.jboss.org/envers/

Crédit pour les extraits de code : Adam Warski de la société Level N Consulting ainsi que la documentation d’Envers.

2 réflexions sur « Jazoon 2009 : Hibernate Envers »

  1. Hello,
    présentation sympa qui permettra de faire connaître Envers.
    Cependant une partie du texte est un peu confuse:
    « Pour chaque entité, une nouvelle entité auditée est créée dynamiquement. Par exemple Address aura une entité Address_AUD pour l’Audit, et c’est bien cette deuxième entité qui est persisté en base… avec Hibernate. »
    Il y a bien une table technique qui est gérée de manière transparente mais du point de vue de l’utilisateur, pas de seconde « entité », ce qui casserait l’élégance du produit.

    De même:
    « Il est important de se souvenir que l’audit est effectué par une autre Entité Hibernate, et que la table originale n’est jamais affectée. »
    est confus.

    Peut être ai-je mal compris.

    Cheers,
    Anthony

Les commentaires sont fermés.