Auditierung mit Hibernate Envers – eine Einführung
Seite 2: Auditinformationen auslesen
Hibernate Envers bietet eine einfache, aber funktionsreiche API, die den Zugriff auf und die Suche innerhalb der auditierten Daten ermöglicht. Dazu stehen zwei unterschiedliche Ansätze zur Verfügung, die die auditierten Daten aus verschiedenen Perspektiven betrachten.
Durch die Auditierung aller Änderungen wird das Datenmodell um eine zusätzliche Dimension erweitert. Dadurch lässt sich zum einen der zu einem gegebenen Zeitpunkt gültige Datenbankstand rekonstruieren. Das wird als horizontale Perspektive bezeichnet. Zum anderen kann man die Änderungen an einem Datensatz im zeitlichen Verlauf betrachten. Das heißt vertikale Perspektive.
Die folgende Abbildung zeigt die Struktur eines Auditlogs mit ausgewählten Änderungen der Author-Entitäten mit den IDs 1, 2 und 3.
Mithilfe des AuditReader-Interfaces lässt sich das Auditlog aus beiden Perspektiven betrachten und die gewünschten Informationen extrahieren. Eine Implementierung des Interfaces geschieht über die AuditReaderFactory-Klasse.
Sie bietet zwei statische Methoden, die einen AuditReader auf Basis einer Hibernate-Session oder eines JPA-EntityManager erzeugt. Anschließend lässt sich mit der createQuery-Methode des AuditReader eine vertikale oder eine horizontale Abfrage erzeugen.
Vertikale Auswertung von Auditdaten
Aus der vertikalen Perspektive kann man vom Erstellen bis zum Löschen alle an einer Entität erfolgten Änderungen betrachten. Der folgende Code zeigt ein einfaches Beispiel für eine Abfrage, die alle Versionen der Author-Entität mit der id 1 aus dem Auditlog liest:
AuditReader auditReader = AuditReaderFactory.get(em);
AuditQuery q = auditReader.createQuery().forRevisionsOfEntity(Author.class,
true, true);
q.add(AuditEntity.id().eq(1L));
List<Author> audit = q.getResultList();
Wie der Name vermuten lässt, liest die Methode forRevisionsOfEntity(Class c, boolean
selectedEntitiesOnly, boolean selectDeletedEntities) alle Revisionen einer Entität aus. Dazu ist zusätzlich zur Klasse zu definieren, ob ausschließlich die zum jeweiligen Zeitpunkt gültige Entität oder ein Tupel aus Revisionsinformationen und Entität zurückgegeben werden soll. Das Beispiel selektiert lediglich die unterschiedlichen Versionen der Author-Entität. Außerdem lässt sich die gelöschte Variante der Entität aus dem Abfrageergebnis ausschließen. Bei der Auditierung einer Löschoperation persistiert Hibernate Envers alle Eigenschaften der Entität als null. Daher kann es sinnvoll sein, diese Version im Abfrageergebnis auszuschließen.
Nachdem die Art der Abfrage und das Rückgabeergebnis definiert sind, ist die WHERE-Klausel der Abfrage zu definieren. Dazu lassen sich mithilfe der add-Methode ein oder mehrere Ausdrücke definieren. Auf die Eigenschaften der auditierten Entität kann man dabei mit der AuditEntity zugreifen. Insgesamt ähnelt die API der JPA Criteria API.
Um das Beispiel möglichst verständlich zu gestalten, verwendet das vorige Codebeispiel nur eine einfache WHERE-Klausel. Die Abfrage selektiert alle Versionen der auditierten Entität, in diesem Fall Author, mit der id 1.
Zusätzlich zur id-Methode bietet die AuditEntity-Klasse einige weitere Methoden, mit denen sich auch komplexere WHERE-Klauseln und Projektionen definieren lassen. Dazu gehören zum Beispiel die Methode property(String propertyName), mit der man auf alle auditierten Eigenschaften der Entität zugreifen kann, und die Methoden revisionNumber und revisionType zum Zugriff auf Revisionsinformationen.
Beim folgenden Beispiel kommen diese Methoden zum Einsatz, um die Nummer der Revision zu finden, in der die firstName-Eigenschaft der Author-Entität mit der id 1 den Wert "Thorben" enthält:
AuditQuery q = auditReader.createQuery().forRevisionsOfEntity(Author.class,
false, true);
q.addProjection(AuditEntity.revisionNumber().min());
q.add(AuditEntity.id().eq(1L));
q.add(AuditEntity.property("firstName").eq("Thorben"));
List<Number> rev = q.getResultList();
Horizontale Auswertung von Auditdaten
Dieselbe API lässt sich für das Erstellen horizontaler Abfragen verwenden. Der einzige Unterschied dabei ist, dass die forEntitiesAtRevision(Class<?> c, Number revision) anstelle der forRevisionsOfEntity-Methode zum Erzeugen der AuditQuery einzusetzen ist. Das folgende Beispiel zeigt eine Abfrage, die alle Author-Entitäten in Revision 1 selektiert, deren lastName-Eigenschaft mit "J" beginnt und diese in alphabetischer Reihenfolge ihrer Nachnamen zurückgibt:
AuditQuery q = auditReader.createQuery().forEntitiesAtRevision
(Author.class, 1);
q.add(AuditEntity.property("lastName").ilike("J", MatchMode.START));
q.addOrder(AuditEntity.property("lastName").asc());
List<Author> audit = q.getResultList();