Skip to content
 

Versionierung mit Hibernate Envers

Das Ziel von Envers (Entity Versioning) ist es eine Historisierung von Entity-Klassen anzubieten. Um die Versionierung einzuschalten müssen nur die Klassen oder deren Felder mit @Audited annotiert werden.

Envers kennt, ähnlich wie Subversion, eine globale Revisionsnummer. Jede Transaktion, die eine Änderung auf der Datenbank vornimmt, entspricht einer Revision und inkrementiert die Revisionsnummer. Wenn man die Revisionsnummer kennt kann man damit die Entitäten für diese Revision abfragen. Man kann zu jeder Revisionsnummer herausfinden zu welchem Zeitpunkt diese erstellt wurde und umgekehrt kann für einen Zeitpunkt die entsprechende Revisionsnummer herausgefunden werden.

Envers funktioniert zusammen mit Hibernate und JPA. Ab Hibernate 3.5 ist die Envers Library in der Hibernate Core Library integriert.

Um Envers einzuschalten müssen als ersten Schritt Event Listeners konfiguriert werden. Diese werden in die die Datei persistence.xml (JPA) oder hibernate.cfg.xml (Hibernate) eingetragen.

Für dieses Beispiel wird JPA verwendet, deshalb werden die Einträge für den AuditEventListener in die Datei persistence.xml eingetragen.

<?xml version="1.0" encoding="UTF-8"?>
<persistence ... >

  <persistence-unit name="defaultDatabase" ...>
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>

      <property name="hibernate.hbm2ddl.auto" value="update" />

      <property name="hibernate.ejb.event.post-insert"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.post-update"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.post-delete"
                value="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>

Wird nun eine Klasse mit @Audited annotiert werden alle Felder versioniert. Alternativ können auch nur bestimme Properties annotiert werden, dann werden auch nur diese historisiert.

In diesem Beispiel werden zwei Entity Klassen Firma und Mitarbeiter verwendet. Jede Firma kennt mehrere Mitarbeiter. In der Entität Firma wird nur das Feld strasse und in der Mitarbeiter Klasse werden alle Properties versioniert.

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

  private String name;

  @Audited
  private String strasse;

  private String ort;

  @OneToMany(mappedBy = "firma", cascade=CascadeType.ALL, orphanRemoval=true)
  private Set<Mitarbeiter> mitarbeiter;

  //getter und setter Methoden

}
@Entity
@Audited
public class Mitarbeiter {
  @Id
  @GeneratedValue
  private int id;

  private String name;
  private String vorname;
  private String strasse;
  private String ort;

  @ManyToOne
  private Firma firma;

  //getter und setter Methoden
}

Es kann nun ganz normal mit den Entitäten gearbeitet werden. Erstellen, Modifizieren und Löschen funktioniert wie bisher. Das Schema das für diese beiden Entitäten erstellt wird ist das gleiche was auch ohne Versionierung erstellt würde. Zusätzlich erstellt Envers automatisch drei Tabellen: Firma_AUD, Mitarbeiter_AUD und REVINFO. In diese Tabellen werden die Versionsinformationen abgespeichert.

Mit dem folgenden Programm wird eine Firma und zwei Mitarbeiter, die für diese Firma arbeiten, in die Datenbank eingefügt. Die Transaktion erhält die Revisionsnummer 1.

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("defaultDatabase");
    EntityManager em = emf.createEntityManager();

    //Revision 1
    em.getTransaction().begin();

    Firma firma = new Firma();
    firma.setName("Company A");
    firma.setOrt("Zurich");
    firma.setStrasse("Street");

    Set<Mitarbeiter> mitarbeiterSet = new HashSet<Mitarbeiter>();

    Mitarbeiter mitarbeiter = new Mitarbeiter();
    mitarbeiter.setFirma(firma);
    mitarbeiter.setName("Muster");
    mitarbeiter.setVorname("Felix");
    mitarbeiter.setOrt("Zurich");
    mitarbeiter.setStrasse("Seestrasse 1");
    mitarbeiterSet.add(mitarbeiter);

    mitarbeiter = new Mitarbeiter();
    mitarbeiter.setFirma(firma);
    mitarbeiter.setName("Meier");
    mitarbeiter.setVorname("Jolanda");
    mitarbeiter.setOrt("Bern");
    mitarbeiter.setStrasse("Bahnhofstrasse 10");
    mitarbeiterSet.add(mitarbeiter);

    firma.setMitarbeiter(mitarbeiterSet);

    em.persist(firma);    

    em.getTransaction().commit();

    em.close();
    emf.close();

Die erstellten Tabellen und deren Inhalt sehen wie folgt aus.

Firma
id name ort strasse
1 Company A Zurich Street


Firma_AUD
id REV REVTYPE strasse
1 1 0 Street


Mitarbeiter
id name ort strasse vorname firma_id
1 Muster Zurich Seestrasse 1 Felix 1
2 Meier Bern Bahnhofstrasse 10 Jolanda 1


Mitarbeiter_AUD
id REV REVTYPE name ort strasse vorname firma_id
1 1 0 Muster Zurich Seestrasse 1 Felix 1
2 1 0 Meier Bern Bahnhofstrasse 10 Jolanda 1


REVINFO
REV REVTSTMP
1 1260247116265



Die *_AUD Tabellen enthalten die Versionen der Entitäten. Zusätzlich wird die Revisionsnummer (REV) und der Typ der Revision (0=ADD, 1=MODIFY, 2=DELETE) abgespeichert. Die Tabelle REVINFO speichert zu jeder Revision den Zeitpunkt ab wann diese erstellt wurde.

Updates

Mit dem folgenden Program werden verschiedene Updates und Delete Operationen ausgeführt.

  • Das Feld Strasse der Firma wird von “Street” auf “Neue Strasse” geändert
  • Es wird ein neuer Mitarbeiter mit Namen Andreas Müller (id=3) eingefügt
  • Im Mitarbeiter Felix Muster (id=1) wird die Strasse auf “Seestrasse 1″ und der Ort auf “Biel” geändert.
  • Die Mitarbeiterin Jolanda Meier (id=2) wird gelöscht

Jedes dieser Updates wird in einer eigenen Transaktion ausgeführt. Es werden deshalb 4 Revisionen erstellt.

 public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("defaultDatabase");
    EntityManager em = emf.createEntityManager();

    //Update Firma Revision 2
    em.getTransaction().begin();
    Firma firma = updateFirma(em);
    em.getTransaction().commit();

    //Neuer Mitarbeiter Revision 3
    em.getTransaction().begin();
    addMitarbeiter(firma);
    em.getTransaction().commit();

    //Mitarbeiter updaten Revision 4
    em.getTransaction().begin();
    updateMitarbeiter(em);
    em.getTransaction().commit();

    //Mitarbeiter löschen Revision 5
    em.getTransaction().begin();
    deleteMitarbeiter(em);
    em.getTransaction().commit();

    em.close();
    emf.close();

  }

  private static Firma updateFirma(EntityManager em) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Firma> c = cb.createQuery(Firma.class);
    Root<Firma> p = c.from(Firma.class);

    Predicate condition = cb.equal(p.get(Firma_.name), "Company A");
    c.where(condition);

    TypedQuery<Firma> q = em.createQuery(c);
    Firma firma = q.getSingleResult();

    firma.setStrasse("Neue Strasse");
    return firma;
  }

  private static void addMitarbeiter(Firma firma) {
    Mitarbeiter mitarbeiter = new Mitarbeiter();
    mitarbeiter.setFirma(firma);
    mitarbeiter.setName("Müller");
    mitarbeiter.setVorname("Andreas");
    mitarbeiter.setOrt("Olten");
    mitarbeiter.setStrasse("Rheinstrasse 10");
    firma.getMitarbeiter().add(mitarbeiter);
  }

  private static void updateMitarbeiter(EntityManager em) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Mitarbeiter> cq = cb.createQuery(Mitarbeiter.class);
    Root<Mitarbeiter> root = cq.from(Mitarbeiter.class);
    Predicate condition = cb.and(cb.equal(root.get(Mitarbeiter_.name), "Muster"),
                          cb.equal(root.get(Mitarbeiter_.vorname), "Felix"));
    cq.where(condition);

    TypedQuery<Mitarbeiter> q = em.createQuery(cq);
    Mitarbeiter mitarbeiter = q.getSingleResult();
    mitarbeiter.setStrasse("Hauptstrasse 10");
    mitarbeiter.setOrt("Biel");
  }

  private static void deleteMitarbeiter(EntityManager em) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Mitarbeiter> cq = cb.createQuery(Mitarbeiter.class);
    Root<Mitarbeiter> root = cq.from(Mitarbeiter.class);
    Predicate condition = cb.and(cb.equal(root.get(Mitarbeiter_.name), "Meier"),
                          cb.equal(root.get(Mitarbeiter_.vorname), "Jolanda"));
    cq.where(condition);

    TypedQuery<Mitarbeiter> q = em.createQuery(cq);
    Mitarbeiter mitarbeiter = q.getSingleResult();
    mitarbeiter.getFirma().getMitarbeiter().remove(mitarbeiter);
    em.remove(mitarbeiter);
  }

Nach diesen Operationen sehen die Inhalte der Tabellen wie folgt aus

Firma
id name ort strasse
1 Company A Zurich Neue Strasse


Mitarbeiter
id name ort strasse vorname firma_id
1 Muster Zurich Seestrasse 1 Felix 1
2 Meier Bern Bahnhofstrasse 10 Jolanda 1

Keine Überraschung in den beiden Tabellen Firma und Mitarbeiter. Diese enthalten den aktuellen Stand der Daten.

Firma_AUD
id REV REVTYPE strasse
1 1 0 Street
1 2 1 Neue Strasse

Die Firma_AUD Tabelle enthält einen zusätzlichen Eintrag für die Änderung der Strasse. REVTYPE=1 zeigt an das dies ein Update war. Die Änderung wurde in der Revision 2 durchgeführt.

Mitarbeiter_AUD
id REV REVTYPE name ort strasse vorname firma_id
1 1 0 Muster Zurich Seestrasse 1 Felix 1
2 1 0 Meier Bern Bahnhofstrasse 10 Jolanda 1
3 3 0 Müller Olten Rheinstrasse 10 Andreas 1
1 4 1 Muster Biel Hauptstrasse 10 Felix 1
2 5 2 NULL NULL NULL NULL NULL

In dieser Tabelle befinden sich drei zusätzliche Einträge. Der Eintrag mit der Revision 3 ist das Einfügen des neuen Mitarbeiters (REVTYPE=0). Die Revision 4 ist die Änderung der Strasse und des Ortes (REVTYPE=1). In der Revision 5 wurde der Mitarbeiter gelöscht (REVTYPE=2). Beim Löschen einer Entität sind alle Felder in der *_AUD Tabelle mit NULL abgefüllt.

REVINFO
REV REVTSTMP
1 1260247116265
2 1260248305640
3 1260248305671
4 1260248305687
5 1260248305703

Da jede Änderung in einer eigenen Transaktion abgewickelt wurde, sind 4 zusätzliche Revisionen entstanden.

Abfragen

Es gibt grundsätzlich zwei Abfragen die man über die historisierten Daten machen kann. Man kann sich den Stand einer Entität zu einer angegebenen Revisionsnummer geben lassen. Oder man erhält eine Liste von den Revisionen in denen eine bestimme Entität geändert wurde.

Im ersten Beispiel wird die Firma abgefragt, wie sie zum Zeitpunkt der Revision 1 ausgesehen hat.

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("defaultDatabase");
    EntityManager em = emf.createEntityManager();

    AuditReader reader = AuditReaderFactory.get(em);
    Firma oldFirma = reader.find(Firma.class, 1, 1);

    System.out.println(oldFirma.getStrasse()); //Output: street
    System.out.println(oldFirma.getName()); //Output: null

Der find Methode müssen drei Parameter übergeben werden. Die Klasse der Entität, der Primary Key und die Revisionsnummer. Das Feld strasse enhält nun die Information wie sie zum Zeitpunkt der Revision 1 (Insert) ausgesehen hat. Alle anderen Felder im Objekt sind null da wir Firma so konfiguriert haben das nur das Feld strasse versioniert wird.

Im folgenden Beispiel werden alle Revisionen aufgelistet, welche eine Änderung der Firma mit Primary Key 1, enthalten.

  List<Number> revisions = reader.getRevisions(Firma.class, 1);
  for (Number rev : revisions) {
    System.out.println(rev);
  }
  //Output: 1 2

Das Datum zu einer bestimmten Revision erhält man mit folgender Methode. Als Parameter übergibt man die Revisionsnummer

reader.getRevisionDate(1);

Mit der getRevisionNumberForDate(Date) erhält man eine Revision zu einem bestimmten Datum. Diese Methode wirft eine org.hibernate.envers.exception.RevisionDoesNotExistException wenn zum angegebenen Datum keine Revision gefunden wurde.

    Calendar cal = new GregorianCalendar(2009, Calendar.DECEMBER, 6);
    System.out.println(reader.getRevisionNumberForDate(cal.getTime()));

Dieses Beispiel listet alle Mitarbeiter auf welche zu der Firma 1 gehören und zum Zeitpunkt der Revision 1 vorhanden waren:

    AuditQuery query = reader.createQuery().forEntitiesAtRevision(Mitarbeiter.class, 1);
    query.add(AuditEntity.relatedId("firma").eq(1));

    List<Mitarbeiter> mitarbeiterList = query.getResultList();
    for (Mitarbeiter mitarbeiter : mitarbeiterList) {
      System.out.println(mitarbeiter.getName());
    }
    //Output: Muster Meier

Wie man sieht wird hier auch der Mitarbeiter Meier angezeigt. Dieser wurde erst in der Revision 5 gelöscht.

Das folgende Beispiel listet mit Hilfe der Methode forRevisionsOfEntity alle Revisionen von allen Mitarbeitern auf. Das Query liefert eine Liste von Objektarrays mit drei Elementen zurück. Das erste Element beinhaltet die geänderte Entität. Das zweite Element ist eine Entität welche die Revsionsdaten (Nummer und Zeitpunkt) beinhaltet. Wenn nichts speziell konfiguriert wurde ist diese vom Typ DefaultRevisionEntity. Das dritte Element ist der Typ der Revision. Einer von drei Werten der RevisionType Enumeration (ADD, MOD, DEL).

    query = reader.createQuery().forRevisionsOfEntity(Mitarbeiter.class, false, true);
    List<Object[]> rersults = query.getResultList();
    for (Object[] result : rersults) {
      Mitarbeiter mitarbeiter = (Mitarbeiter)result[0];
      DefaultRevisionEntity revEntity = (DefaultRevisionEntity)result[1];
      RevisionType revType = (RevisionType)result[2];

      System.out.println("Revision     : " + revEntity.getId());
      System.out.println("Revision Date: " + revEntity.getRevisionDate());
      System.out.println("Type         : " + revType);
      System.out.println("Mitarbeiter  : " + mitarbeiter.getName());

      System.out.println("------------------------------------------------");
    }

Der Output für dieses Beispiel wird folgendermassen aussehen. Das Revision Date ist natürlich abhängig vom Zeitpunkt wann das Beispiel ausgeführt wird.

Revision     : 1
Revision Date: Tue Dec 08 05:38:36 CET 2009
Type         : ADD
Mitarbeiter  : Muster
------------------------------------------------
Revision     : 1
Revision Date: Tue Dec 08 05:38:36 CET 2009
Type         : ADD
Mitarbeiter  : Meier
------------------------------------------------
Revision     : 3
Revision Date: Tue Dec 08 05:58:25 CET 2009
Type         : ADD
Mitarbeiter  : Müller
------------------------------------------------
Revision     : 4
Revision Date: Tue Dec 08 05:58:25 CET 2009
Type         : MOD
Mitarbeiter  : Muster
------------------------------------------------
Revision     : 5
Revision Date: Tue Dec 08 05:58:25 CET 2009
Type         : DEL
Mitarbeiter  : null
------------------------------------------------

Weitere Informationen zu Envers findet man auf der Projektseite.

Leave a Reply