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.
| id | name | ort | strasse |
|---|---|---|---|
| 1 | Company A | Zurich | Street |
| id | REV | REVTYPE | strasse |
|---|---|---|---|
| 1 | 1 | 0 | Street |
| id | name | ort | strasse | vorname | firma_id |
|---|---|---|---|---|---|
| 1 | Muster | Zurich | Seestrasse 1 | Felix | 1 |
| 2 | Meier | Bern | Bahnhofstrasse 10 | Jolanda | 1 |
| 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 |
| 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
| id | name | ort | strasse |
|---|---|---|---|
| 1 | Company A | Zurich | Neue Strasse |
| 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.
| 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.
| 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.
| 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.