HOAB

History of a bug

Hibernate Envers - Audit de table - Relation 1-N

Rédigé par gorki Aucun commentaire

Le problème :

Envers est un module core d'Hibernate qui permet d'auditer des entités, i.e, historiser toutes les modifications de l'objet.

Le principe est de stocker dans une table quasiment identique les différentes versions de l'objet.

Exemple :

Table source : UTILISATEUR(id, version, nom, adresse)

Table audit : UTILISATEUR_AUD(id, rev, nom, adresse)

Le champ version de la table source est utilisé pour l'optimist locking, on n'a donc pas besoin de ce champ dans la tableau auditée.

Par contre la table auditée a un champ un peu équivalent : "révision" qui permet de stocker les différentes versions de l'objet.

Attention cette révision est différente de la version de l'objet, en effet cela correspond plus à un numéro de commit global : tous les objets modifiés et audités dans une même transaction auront la même révision (un peu comme un commit SVN).

Coté code source :

@Audited
@Entity
public class UTILISATEUR {

    @Id
    private Long id;

    @Version
    private Long version;

    private String nom;

    private String adresse;
}

Facile comme tout (il manque des trucs bien sur : création de la table d'audit, des séquences qui vont bien, des informations supplémentaires trackées avec l'audit, etc...)

A chaque sauvegarde d'une instance UTILISATEUR, on aura une ligne dans UTILISATEUR_AUD.

Bon, mais que se passe-t-il si au lieu d'avoir 1 adresse, UTILISATEUR a N adresses ?

@Audited
@Entity
public class UTILISATEUR {

    @Id
    private Long id;

    @Version
    private Long version;

    private String nom;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "UTILISATEUR_ID")
    private List<String> adresses;
}

Solution :

Déjà lire la documentation.

Ensuite choisir : est-ce que les adresses doivent être auditée ? Si oui, on continue.

Evidemment il va y a voir une deuxième table ADRESSE_AUD.

Mais un des points forts d'envers est d'auditer seulement les deltas : une ADRESSE modifiée ne veut pas dire que UTILISATEUR est aussi modifié.
La conséquence importante est que là où on a une relation 1..N, on passe à une relation N..N. Pourquoi ?

Voici ce qui se passe :

// Sauvegarde d'un utilisateur avec une adresse, dans les tables d'audit on obtient

U1 (rev1) -> A1 (rev1)

// on modifie l'adresse (toujours du 1..N)

U1 (rev1) -> A1 (rev1)

U1 (rev1) -> A1 (rev2)

// on modifie l'utilisateur (la relation devient N..N ! deux utilisateurs pointent vers la même adresse)

U1 (rev1) -> A1 (rev1)

U1 (rev1) -> A1 (rev2)

U1 (rev3) -> A1 (rev2)

P.S : Notez bien que les objets modifiés dans une même transaction ont la même révision

Les conséquences directes sont :

- une table d'association est nécessaire entre UTILISATEUR_AUD et ADRESSE_AUD (alors que ce n'est pas le cas dans le modèle normal).

- en 1..N, le lien est porté par la table fille, on a donc dans ADRESSE une colonne qui référence l'utilisateur (UTILISATEUR_ID) ; dans la table d'audit, c'est la table d'association qui porte ce lien.

@Audited
@Entity
public class UTILISATEUR {

    @Id
    private Long id;

    @Version
    private Long version;

    private String nom;

    private List<String> adresses;
}

 

@Audited
@Entity
public class ADRESSE {

    @Id
    private Long id;

    @Version
    private Long version;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "UTILISATEUR_ID")
    @AuditJoinTable(name = "UTILISATEUR_ADRESSE_AUD", inverseJoinColumns = {
        @JoinColumn(name = "ADRESSE_ID")
    })
    private List<String> adresses
}

Je mettrais bien le DDL, mais j'ai la flemme :)

Trucs à savoir

  • évidemment Envers utilise beaucoup de ressource, attention aux performances
  • en plus des tables d'audit, il y a une table pour référencer toutes les révisions, cette table est customisable pour y ajouter des informations (utilisateur, objets, etc...)
  • il est possible d'utiliser des EJB dans les EntityTrackingRevisionListener, en faisant un lookup JNDI.(entre autre pour retrouver un EJB qui possède une variable @RequestScoped). Si j'ai le temps, je ferai une description rapide du truc.
  • Best practice : il y a des fonctions pour recharger les révisions, mais ça ne marche bien que si l'objet "chapeau" est modifié à chaque révision. Sinon c'est compliqué de retrouver l'historique à partir de l'objet chapeau. (Il suffit par exemple de positionner une date de modification) => ça n'aide pas les performances.

 

Fil RSS des articles de ce mot clé