Hibernate Envers – Entity Auditing

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:

hibernate_envers

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:

hibernate_envers_revisiondata

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

Github Projekt: https://github.com/GEDOPLAN/hibernate-envers

Werbeanzeigen

Hibernate-Generator für das JPA-Metamodell schließt XML-Dateien nicht

Wenn man den Metamodell-Generator von Hibernate zur Erzeugung der JPA-Metamodellklassen nutzt, stellt man gelegentlich fest, dass Source- oder Target-Dateien über den Generatorlauf hinaus blockiert bleiben. Das äußert sich dann in den gängigen IDEs so, dass ein Project Clean (Eclipse) bzw. ein Clean (NetBeans) mit der Meldung fehlschlagen, dass die Datei persistence.xml (oder auch orm.xml) nicht gelöscht werden könnten. Der Effekt scheint nur unter Windows aufzutreten (bzw. aufzufallen).

Eine Lösung für das Problem existiert m. W. noch nicht. Es bleibt also nur der Wechsel des Generators. So kann bspw. der EclipseLink-Generator genutzt werden, wie ich dies in einem früheren Post beschrieben habe. Da die generierten Klassen nicht vom genutzten Provider abhängen, kann der EclipseLink-Generator auch genutzt werden, wenn im Projekt ansonsten z. B. Hibernate eingesetzt wird.

JPA 2.1 Entity Graphs: We’re getting even closer!

In addition to my last post about entity graph support in Hibernate:

The spec encourages fetching only those attributes which are part of an entity graph used as fetch graph (i. e. by using the hint javax.persistence.fetchgraph) and leave others unloaded even if they are declared EAGER. But „The persistence provider is permitted to fetch additional entity state beyond that specified by a fetch graph or load graph“ (section 3.7.4 Use of Entity Graphs in find and query operations).

That said, Hibernate does not violate the spec.javax.persistence.loadgraph and javax.persistence.fetchgraph are effectively implemented in the same way. It would be an improvement to make javax.persistence.fetchgraph more strict, i. e. make attributes not part of the graph effectively LAZY. Please vote for https://hibernate.atlassian.net/browse/HHH-8776!

JPA 2.1 Entity Graphs: We’re getting close!

EclipseLink and Hibernate are implementing JPA 2.1 entity graphs to a hight degree, but some issues are still open. I’ve tested with EclipseLink 2.5.1 and Hibernate 4.3.0.CR1. EclipseLink passes 15 of 19 tests, Hibernate even 17 of 19.

Open issues in EclipseLink:

  • Fetch graphs specified by name are not found.
    (Test: testHintWithFetchGraphAsString)
    Interesting: Load graphs work!
  • Fetch and load graphs do not force loading their attributes for simple entity graphs.
    (Tests: testQueryWithBasicFetchGraphLoadsGraph, testQueryWithBasicLoadGraphLoadsGraph)
    Astonishingly complex entity graphs work.
  • If a query uses an entity graph with the hint „javax.persistence.fetchgraph“, the provider should fetch only those attributes included in the entity graph (and id and version values). So attributes not included in the graph should stay unloaded, even if they are declared EAGER. EclipseLink fetches those attributes.
    (Test: testQueryWithBasicFetchGraphLoadsOther)
    Again the same test with a complex graph works.

Open issues in Hibernate (2 tests fail, but for the same reason):

  • If a query uses an entity graph with the hint „javax.persistence.fetchgraph“, the provider should fetch only those attributes included in the entity graph (and id and version values). So attributes not included in the graph should stay unloaded, even if they are declared EAGER. Hibernate fetches those attributes.
    (Tests: testQueryWithBasicFetchGraphLoadsOther and testQueryWithComplexFetchGraphLoadsOther. In both tests the attribute Publisher.categories is not part of the entity graph, but gets loaded nevertheless.)

The test source is on GitHub: https://github.com/dirkweil/jpa-entitygraph-test.

So were getting close to what the spec proposes, but there is still a way to go. Hibernate has the edge on EclipseLink even though it is only a release candidate – good work!