Envers est un projet open source dont le but est d'offrir un mécanisme d'historisation des données, une technique dont la réalisation était jusqu'à lors fastidieuse et qui demandait la collaboration du développeur et du DBA. Envers s'intègre naturellement à Hibernate. Il est d'ailleurs devenu, depuis peu, un module à part entière du projet Hibernate.

La présentation était assurée par Adam Warski, ancien employé de JBoss et créateur et développeur principal du projet. En guise d'introduction, l'orateur a commencé le séminaire en présentant les patterns d'historisation les plus connus et la difficulté de leur mise en œuvre. Parmi eux, on trouve le pattern AuditLog pattern ou « la piste d'audit ». Cette technique consiste à écrire dans un fichier texte les données modifiées, la date et l'auteur de la modification. Son avantage réside dans sa simplicité. En contrepartie, elle s'avère rapidement inefficace en raison de la quantité de données qu'elle produit. En effet, il devient difficile de corréler des entités ou de naviguer entre les relations.

De son côté, Envers permet de mettre en œuvre l'historisation de façon transparente et non intrusive (pas de modification du schéma, ni du code de l'application). En effet, son utilisation se limite à ajouter l'annotation @Audited à l'entité à historiser et à renseigner les évènements qui déclencheront l'audit dans le fichier persistence.xml.

De façon similaire au système de contrôle de version Subversion, Envers utilise le concept revision. Une révision correspond à une transaction. Pour ce faire, Envers crée une table identique à la table originale, avec des colonnes supplémentaires. On retrouve parmi celles-ci, la colonne réservée au numéro de révision et le type d'opération (ajout, modification, suppression). Le présentateur nous averti que seules les entités modifiées dans une transaction réussie (commit passé avec succès) seront historisées. Ce comportement est tout a fait normal puisqu'il garantit la cohérence des données historisées.

En plus des données de base telles que les strings, les nombres et les dates, il est possible de versionner les relations. De ce fait, il est possible d'explorer un graphe d'objet à un instant T. Voici un exemple, repris de la présentation de Adam, montrant le fonctionnement de Envers :

// Revision 1 : créer une personne et lui associer une adresse
// une adresse pour être associée à plusieurs personnes

entityManager.getTransaction().begin();

Address a1 = new Address("Rue de la boetïe", 15);
Person p = new Person("Martin", "Dupont");
p.setAddress(a1);
entityManager.persist(a1); 
entityManager.persist(p);

entityManager.getTransaction().commit();

// Revision 2 : On modifie l'adresse de la personne
// et son prénom
entityManager.getTransaction().begin();
Address a2 = new Address("Rue des bourets", 6);
entityManager.persist(a2);
// on suppose que le id de la personne est 1
int id_personne = 1;
p = entityManager.find(Person.class, id_personne);
p.setName("Paul");
p.setAddress(a2);
entityManager.getTransaction().commit();

Envers offre une API pour lire les données historisées. Il est, par exemple, possible de récupérer une entité qui correspond à telle ou telle révision.

// Lecture des données historisées
AuditReader auditReader = AuditReaderFactory.get(entityManager);
// Lire la personne de revision 1
int revision1 = 1;
Person oldPerson = auditReader.find(Person.class, id_personne, revision1);
assert "Martin".equals(oldPerson.getName());
assert a1.equals(oldPerson.getAddress());

// De même pour les adresses à la révision 1
int a1_id = 1;
Address old_a1 = auditReader.find(Address.class, a1_id, revision1);
assert old_a1.getPersons().size() == 1;
assert old_a1.getPersons().contains(p);

int a2_id = 2;
Address old_a2 = auditReader.find(Address.class, a2_id, revision1);
assert old_a2.getPersons().size() == 0;

Envers offre aussi une API similaire à l'API Criteria, en voici un exemple :

List personsAtAddress = getAuditReader().createQuery()
.forEntitiesAtRevision(Person.class, 12)
.addOrder(AuditEntity.property("surname").desc())
.add(AuditEntity.relatedId("address").eq(addressId))
.setFirstResult(4)
.setMaxResults(2)
.getResultList();

Adam a terminé sa présentation par un benchmark opposant la modification massive d'entités sans versionnng à des une modification avec versioning. Le résultat montre qu'avec l'historisation la modification coûte en moyenne 1,5 fois le temps d'une modification sans historisation, et ce, à cause des ordres insert dans les tables d'historisation.

Au moment des questions-réponses, une personne de l'assistance a demandé à l'orateur s’il était possible de configurer Envers avec XML. La réponse fut laconique : non ! Seules les annotations sont autorisées. L'auteur encourage même l'utilisation du standard JPA à la place d'API propriétaire de Hibernate. Il n'est pas possible non plus de stocker les tables d'audit dans un schéma autre que le schéma des tables à auditer.

Pour conclure, l'orateur a exposé les fonctionnalités futures à court et à long terme. Notons par exemple : le revert (remettre une entité à un état antérieur), les branches, appliquer un DIFF ou encore ne versionner que certaines propriétés de l'entité.

Cette présentation a été une bonne introduction au sujet. Il ne nous reste qu'à tester cette technologie grandeur nature et de vous faire partager, bientôt nous l'espérons, notre expérience sur le sujet.