Beim Entity Auditing geht es darum Veränderungen an unseren JPA-Objekten festzuhalten um Nachverfolgen zu können wie sich ein Datensatz im Laufe der Zeit verändert hat. Bereits seit der Hibernate Version 3.5 steht „Envers“ im HibernateCore-Module bereit und bietet Out-Of-The-Box eine eben solche Lösung.
Die Funktionsweise von Envers ist schnell erklärt: intern arbeitet Envers mit Listenern die während einem merge / persist / remove aktiv werden und den Zustand vor der Aktion in einer separaten Tabelle festhalten. Dabei verwendet Envers so genannte Revisions, eine Art Versionsnummer für Entitäten die pro Transaktion generiert wird und anhand der die unterschiedlichen Stände der Entitäten ermittelt werden können.
Unsere hier gezeigten Beispiele zielen auf den Wildfly 10 als Laufzeitumgebung ab der die benötigen Bibliotheken bereits (in Version 5.0.7) mit bringt. Um Envers im Projekt ein setzen reicht demnach eine provided-Abhängigkeit in unserer pom.xml:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>5.0.7.Final</version> <scope>provided</scope> </dependency>
Um nun ganze Entitäten oder wahlweise nur einzelne Attribute der Historisierung zu unterwerfen reicht es mittels „@Audited“ Annotation dieses Hibernate mit zu teilen:
Auditing @Entity @Table(name = "ORDERTBL") @Audited public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer orderID; @OneToMany(mappedBy = "order") @NotAudited private List<OrderDetail> orderDetails;
Das obige Beispiel zeigt die einfache Verwendung von Hibernate Envers. Mittels „@Audited“ markieren wir unsere Order-Entität für die Historisierung. Jegliche Transaktionen die Änderungen vornimmt führt nun zu einer neuen Revision. Dabei berücksichtigt Envers auch abhängige Objekte und würde diese, wenn nicht wie im obigen Beispiel mittels „@NotAudited“ annotiert, ebenfalls historisieren.
Neben der zu erwartenden Tabelle „ORDERTBL“ die unseren aktuellen Datensatz beinhaltet führt Envers nun eine weitere Tabelle mit dem default-Suffix „_AUD“ ein:
Hierbei handelt es sich um die Revisionstabelle für unsere Entität. Diese beinhaltet die Revisionnummer, einen Revisiontyp (ADD,MOD, DEL) und alle Daten der Entität, bzw. der Attribute die zur Historisierung markiert wurden. Eine entsprechende API zum Zugriff auf die Revisionsdaten stellt Envers ebenfalls bereit, dabei ist die Synatx sehr stark an die CriteriaAPI angelehnt.
Selektion der ersten Revision einer Bestellung mit bestimmter ID:
AuditReader auditReader = AuditReaderFactory.get(entityManager); List<Number> revisions = auditReader.getRevisions(Order.class, order.getOrderID()); Order revOrder = auditReader.find(Order.class, order.getOrderID(), revisions.get(0)); Assert.assertTrue(revOrder.getShipName().equals(oldShipName));
Wer zum Teufel war das?
Eine häufige Anforderung ist ebenfalls das zu einer Revision zusätzliche Daten abgelegt werden sollen, z.B. das aktuelle Datum oder der Benutzer der die Änderung vollzogen hat. Hierzu bietet Envers ebenfalls eine Lösung. Zwei Klassen sind dazu zu implementieren
- RevisonEntity
- Entität welche die zusätzlichen Daten für jede Revision bereit hält
- Revision Listener
- Ermittlung der Daten
@Entity @RevisionEntity(RevisionDataListener.class) public class RevisionData extends DefaultRevisionEntity { @Temporal(TemporalType.TIMESTAMP) private Date changeDate; private String username; //Getter und Setter }
public class RevisionDataListener implements RevisionListener { @Override public void newRevision(Object o) { RevisionData revData = (RevisionData) o; revData.setChangeDate(new Date()); revData.setUsername(getUsername()); } private String getUsername() { return "some username" } }
Jede Revision die erzeugt wird, durchläuft nun unserer Listener und wird mit den Informationen angereichert. Diese Informationen werden in einer separaten Tabelle mit entsprechender Referenz zur Revision in der Datenbank abgelegt und können mittels AuditReader ausgelesen werden:
// Revision-Objekt und zusätzliche Daten auf Basis der ID (8) und der Revision (55) ermitteln. AuditQuery revQuery = auditReader.createQuery().forRevisionsOfEntity(Product.class, false, true); revQuery.add(AuditEntity.revisionNumber().eq(55)); revQuery.add(AuditEntity.id().eq(8)); // liefert Objekt Array mit Entität, RevisionObjekt und RevisionType Object[] revObject = (Object[]) revQuery.getSingleResult(); Product revProduct = (Product) revObject[0]; RevisionData revData = (RevisionData) revObject[1]; RevisionType revType = (RevisionType) revObject[2];
Fazit
Man sollte sich schon gut überlegen welche Daten wirklich dem Auditing unterworfen werden sollen, da dieser augenscheinlich so leichtgewichtige Ansatz natürlich bei schreibenden Operationen zu einem wesentlich höheren Aufwand auf Seiten der Datenbank führt. Davon abgesehen ist Envers eine sehr einfache Möglichkeit die Historisierung von Entitäten zu realisieren. Ein paar Annotationen reichen aus und Envers kümmert sich um den Rest.
Weite Informationen: Envers Docs