Aus der Geschichte lernen

Automatische Historisierung mit Hibernate Envers

Hansjörg Wingeier, Stefan Wohlgemuth und Patrick Brügger

In vielen Branchen müssen auf Grund gesetzlicher Vorgaben Mutationen von Daten nachvollziehbar sein. Das heißt, es muss für jeden vergangenen Zeitpunkt möglich sein, den damaligen Stand der Daten zu ermitteln. Allgemein wird das als Auditing oder Historisierung der Daten bezeichnet. Umgesetzt in einer Applikation bedeutet das, dass anstelle des Löschens von Datensätzen diese irgendwie als „gelöscht“ markiert werden müssen oder auch dass eine einfache Mutation eines Datensatzes sicherstellen muss, dass der alte Zustand in irgendeiner Art vorhanden bleibt.

Der Standardbenutzer arbeitet in erster Linie mit den aktuellen Daten. Für ihn ist es daher wichtig, dass durch die Historisierung die Performance nicht merklich sinkt. Für einen Applikationsentwickler sollte die Historisierung der Daten möglichst transparent erfolgen. Er möchte sich nicht explizit darum kümmern müssen, er erwartet, dass das verwendete Framework diese Aufgabe automatisch erledigt. Dieser Artikel zeigt, wie diese Anforderungen mit Envers umgesetzt werden können.

Die Entstehung von Envers

Das Projekt Envers (Entity Versioning) wurde von Adam Warski im Jahr 2008 ins Leben gerufen. In seinen Anfängen – namentlich als Preview-Versionen – war Envers befähigt, einfache Properties wie Strings, Numbers und Dates zu versionieren. Später wurde dann das Audit von 1:1- und 1:n-Relationen unterstützt.

Ab dem Envers-1.0.0Beta1-Release konnten Queries auf den historisierten Daten ausgeführt werden. Es war damit möglich, den Zustand einer Entität auf Grund einer Revisionsnummer abzufragen oder die Zustandsentwicklung einer Entität über ihre gesamte Lebensdauer zu verfolgen. Die Envers 1.0.0GA-Version wurde im Juli 2008 veröffentlicht. Sie stellte eine stabile Version der bis dahin entwickelten Features zusammen. Version 1.1.0GA erschien im Oktober 2008. Nebst Codeverbesserungen und Bugfixes lag der Fokus dieses Release in der Unterstützung von persistenten Collections. Mit den steten Verbesserungen im Laufe der Zeit und der zunehmenden Akzeptanz war es nicht weiter verwunderlich, dass Envers Ende Oktober 2008 ein offizielles Hibernate-Modul wurde. Im Frühjahr 2009 erschien die Version 1.2.0GA. Diese enthält in erster Linie die Änderungen, die den Wechsel zu einem offiziellen Hibernate-Modul nötig machten. Die erste offizielle Hibernate-Core-Version, die das Envers-Modul integriert hat, wird Version 3.4.0 GA sein.

Was ist Envers?

Envers ist eine Zusatzbibliothek zu Hibernate, die sich um das Auditieren der Daten kümmert. Das bemerkenswerte an Envers ist, dass es das Auditieren praktisch völlig transparent geschehen lässt. So können Daten, wie mit Hibernate üblich, gelesen und mutiert werden. Ebenso muss das bestehende Datenbankschema nicht verändert, sondern nur durch zusätzliche Tabellen ergänzt werden. Wie im Folgenden gezeigt wird, sind auch die notwendigen Anpassungen im Code nur minimal.

[ header = Seite 2: Funktionsweise ]

Funktionsweise

Soll eine persistente Klasse (eine mit @Entity annotierte Klasse) auditiert werden, so muss diese zusätzlich mit der Annotation @Audited versehen werden (Listing 1).

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

private String name;
...
}	

Zur Laufzeit wird dann für jede dieser Klassen eine entsprechende Audit-Entität erstellt. Diese dient als Container für die historisierten Daten. Die gleiche Abbildung findet auch auf der Datenbank statt. Zum Beispiel wird für eine Tabelle PERSON eine entsprechende PERSON_AUD-Tabelle erstellt. In der Datenbank sind somit die aktuellen Daten komplett von den historisierten Daten getrennt (Listing 2).

create table Person (
id integer not null,
name varchar(255),
surname varchar(255),
C_VERSION_NUM bigint,
primary key (id)
);

create table Person_AUD (
id integer not null,
REV integer not null,
REVTYPE tinyint,
name varchar(255),
surname varchar(255),
primary key (id, REV)
);

Ähnlich wie bei Subversion werden alle in einer Transaktion durchgeführten Änderungen mit der gleichen globalen Revisionsnummer versehen. Jede Revision definiert also einen konsistenten Zustand der Daten.

Konfiguration

Damit Envers beim Mutieren, Erzeugen oder Löschen von Daten informiert wird, muss der AuditEventListener von Envers für verschiedene Hibernate Events registriert werden. Dies geschieht in der Datei persistence.xml (Listing 3).

<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" />

Sollen nicht alle Attribute und Relationen einer persistenten Klasse auditiert werden, so muss die @Audit-Annotation nur auf den gewünschten Attributen und Relationen angegeben werden. Die@Audited-Annotation auf der Klasse ist in diesem Fall zu entfernen.

DB-Skript generieren

In Envers ist ein erweiterter HibernateAntTask – sinnigerweise EnversHibernateAntTask genannt – enthalten. Mit diesem kann das Datenbankschema inklusive der zusätzlichen Tabellen für die Auditierung vollständig erzeugt werden. Natürlich werden dabei auch die JPA-Annotationen wie @Table, @Column, @JoinColumn usw. berücksichtigt.

Um 1:n-, n:1- und n:m-Beziehungen auditieren zu können, muss für jede Beziehung eines solchen Typs eine eigene Audit-Tabelle vorhanden sein. Ohne weitere Angaben benennt derEnversHibernateAntTask diese mit der Kombination aus den Namen der beteiligten Klassen. Für eine 1:n-Beziehung zwischen Person und Adresse würde also die Tabelle PERSON_ADRESSE_AUDerzeugt werden. Ein Problem ergibt sich daraus, wenn mehrere Beziehungen zwischen 2 Klassen vorhanden sind. Ohne weitere Maßnahmen kommt es in diesem Fall beim Generieren des DB-Skripts zu einem Fehler. Durch verwenden der Annotation @AuditJoinTable, kann dieser behoben werden. Mit ihr kann der Name der Audit-Tabelle einer Beziehung explizit gesetzt werden (Listing 4).

@OneToMany
@JoinColumn(name = "C_UNIDIRECTMANY_FK")
@AuditJoinTable(name = "T_RELATIONPARTNER1_UNIDIRECTMANY_AUD")
private List<RelationPartner2> m_unidirectMany =
new ArrayList<RelationPartner2>();

@ManyToMany(targetEntity = entity.RelationPartner2.class)
@JoinTable( name = "T_RELATIONPARTNER1_BIDIRECTMANYMANY",
joinColumns = {@JoinColumn(name = "C_BIDIRECTMANYMANY_FK1") },
inverseJoinColumns = {@JoinColumn(name = "C_BIDIRECTMANYMANY_FK2") })
@AuditJoinTable(name = "T_RELATIONPARTNER1_BIDIRECTMANYMANY_AUD")
private List<RelationPartner2> m_bidirectManyMany =
new ArrayList<RelationPartner2>(); 

[ header = Seite 3: Abfragen der historisierten Daten ]

Abfragen der historisierten Daten

Grundsätzlich gibt es zwei Arten von Abfragen auf historisierte Daten auszuführen. Erstere ist eine Abfrage aller Änderungen eines Objekts über dessen Lebenszeit. Dabei interessiert uns die Frage, wann das Objekt wie verändert wurde (Abb. 1). Die zweite Art ist die Frage nach dem Zustand eines oder mehrerer Objekte zu einem bestimmten Zeitpunkt bzw. für eine bestimmte Revision (Abb. 2).

Abb. 1: Historie eines Objekts

Abb. 2: Navigation über das Datenmodell in der Vergangenheit

Für beide Arten von Abfragen können entsprechende Instanzen von AuditQuery erstellt werden. Dies geschieht über die Methoden forRevisionsOfEntity und forEntitiesAtRevision vonAuditQueryCreator. Ähnlich wie mit Hibernate Criteria können zusätzliche Bedingungen mit Instanzen von AuditQuery verknüpft werden. Listing 5 zeigt einige Beispiele.

AuditQuery query1 = ... .forEntitiesAtRevision(clazz, revNumber);
query1.add(AuditEntity.property("name").equals("Hans"));

AuditQuery query2 = ... .forEntitiesAtRevision(clazz, revNumber);
query2.add(AuditEntity.id().eq(id.getPk())).getSingleResult();

AuditQuery query3 = ... .forEntitiesAtRevision(clazz, revNumber);
query3.addOrder(AuditEntity.property("name").desc());
Performance

Wir haben den Performanceverlust mit Historisierung gegenüber ohne Historisierung anhand von verschiedenen, einfachen Objekt-Relationen-Szenarien ermittelt. Dabei haben wir für jedes Szenario das Insert-, Update– und Delete-Verhalten untersucht. Die definierten Szenarien sind in den Abbildungen 3 bis 6 gezeigt.

Abb. 3: Szenario 1 – Objekt ohne Relation

Abb. 4: Szenario 2 – Objekt mit 1:1-Relation zu anderem Objekt

Abb. 5: Szenario 3 – Objekt mit 1:n-Relation zu anderem Objekt

Abb. 6: Szenario 4 – Objekt mit n:m-Relation zu anderem Objekt

Die gleichen Messungen wurden für zwei Datenbanktypen von Oracle und IBM durchgeführt. Die Datenbanken wurden mit der Default-Konfiguration ohne zusätzliche Administrationsmaßnahmen betrieben. Die Datenbanktabellen waren jeweils vor jedem Messdurchgang leer. Tabelle 1 zeigt die ermittelten Messresultate.

[ header = Seite 4: Tabelle 1 + Fazit ]

 

 

 

 

 

Oracle 10g Express

IBM DB2/NT 9.5.0

 

Anzahl Personen

Anzahl Adressen

Anzahl Personen_ Adressen

Anzahl Transaktionen

Faktor

Faktor

Insert

 

 

 

 

 

 

Szenario 1

2000

 

 

2000

5.5

5.6

Szenario 2 (1:1)

1000

1000

 

1000

3.7

3.7

Szenario 3 (1:n)

500

1500

 

500

2.9

2.8

Szenario 4 (n:m)

200

200

600

1000

4.4

4.6

Update

 

 

 

 

 

 

Szenario 1

2000

 

 

2000

8.0

8.6

Szenario 2 (1:1)

1000

1000

 

1000

4.9

5.0

Szenario 3 (1:n)

500

1500

 

500

3.6

3.8

Szenario 4 (n:m)

200

200

0

200

6.5

6.8

Delete

 

 

 

 

 

 

Szenario 1

2000

 

 

2000

19.9

21.6

Szenario 2 (1:1)

1000

1000

 

1000

10.4

10.7

Szenario 3 (1:n)

500

1500

 

500

5.9

6.5

Szenario 4 (n:m)

200

200

600

400

11.8

13.7

Tabelle 1: Ergebnisse der Performancemessungen

Beim Löschen mit Historisierung war Envers so konfiguriert, dass die gesamte Historie bestehen blieb.

Fazit

Wir haben Envers in unser Applikations-Generator-Framework QiQuGen integriert. Ein Teil der Ergebnisse sieht man in Abbildung 1 und 2. Die Integration gestaltete sich dabei sehr einfach und auch das Resultat hat uns überzeugt. Besonders gut gefallen hat uns die Tatsache, dass beim Lesen der aktuellen Daten keine Performanceeinbußen feststellbar waren.

Laut Dokumentation von Envers werden neben den Standarddatentypen und Relationen auch Collections und Hibernate Custom Types unterstützt. Inwieweit aber andere exotische Datentypen auch auditiert werden können, haben wir nicht untersucht. Je nach Projekt sollte dieser Punkt vorab geklärt werden.

Die wichtigste Einschränkung, auf die wir gestoßen sind, ist im Moment die fehlende Unterstützung von anderen Mapping-Möglichkeiten außer JPA. Wenn man aber damit leben kann, sollte dem Einsatz von Envers nichts im Weg stehen.

Bezüglich Performance ist festzustellen, dass vor allem beim Ändern und Löschen mit Historisierung ein erheblicher Mehraufwand mit entsprechendem Geschwindigkeitsverlust zu erkennen ist. Bei Applikationen, wo ein Benutzer z. B. über eine Weboberfläche Daten eingibt und ändert, ist aber der Geschwindigkeitsverlust im Verhältnis zur gesamten Request-Response-Zeit in der Regel nicht markant.

Hansjoerg Wingeier ist ein Senior Java Entwickler bei der aloba ag in Burgdorf, Schweiz. Er besitzt einen Master in IT, kann auf 10 Jahre Erfahrung im Java-Umfeld zurückgreifen und beschäftigt sich seit fast 2 Jahrzehnten mit objektorientierten Technologien. Er interessiert sich im speziellen für modellgetriebene Ansätze, sowie für Entwicklungsvorgehen. Zudem ist er Committer bei den Open-Source Projekten QiQu undQiQuGen.

Stefan Wohlgemuth ist ein Senior Java Entwickler bei der aloba ag in Burgdorf, Schweiz. Er ist Ingenieur HTL mit Fachrichtung Informatik und Telekommunikation und arbeitet seit 10 Jahren mit Java Technologien. Zu seinen Kernkompetenzen gehört die Anbindung an Datenbanken wozu er unter anderem auch immer wieder Hibernate verwendet.

Patrick Brügger arbeitet als Software Entwickler bei der aloba ag in Burgdorf, Schweiz. Er hat einen Abschluss als Bachelor of Science in Informatik und weist eine dreijährige Berufserfahrung im objektorientierten Umfeld auf. Darüber hinaus ist er Committer bei den Open-Source Projekten QiQu und QiQuGen.

Geschrieben von
Hansjörg Wingeier, Stefan Wohlgemuth und Patrick Brügger
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.