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

Advertisements

Lombok – oder die Geschichte von dem, der auszog, Boilerplate Code zu vertreiben

Der Sprache Java wird häufig angelastet, bei der Definition von Klassen zu viel unnützen – „Boilerplate“ – Code zu benötigen. So werden fachliche Attribute i. A. in privaten Instanzvariablen abgelegt, für die dann öffentliche Getter und/oder Setter benötigt werden. Zudem sollte nahezu jede Klasse eine Implementierung der Methoden equals und hashCode besitzen. Und toString wäre auch ganz schön. Neben dem eigentlich wichtigen Inhalt einer Klasse muss also noch eine Menge Code geschrieben werden, der auch generiert werden könnte. Dies kann man natürlich mit Hilfe der gängigen IDEs tun, was aber nur bei der ersten Erstellung der Klassen hilft. Bei späteren Änderungen – Attribute kommen hinzu oder fallen weg, Attributtypen ändern sich – ist eine aufwändigere, meist mauelle Anpassung unumgänglich.

Hier bietet das Projekt Lombok eine andere Herangehensweise: Die Klassen werden durch Annotationen angereichert und ein Annotation Processor erzeugt den Code zur Build-Zeit. Die wesentlichen Annotationen sind diese:

  • @Getter, @Setter
    fügen zu einem Attribut eine Getter- bzw. Setter-Methode hinzu. Auf Klassenebene angewendet, werden die Methoden für alle Attribute erzeugt, die keine eigene Annotation besitzen.
  • @EqualsAndHashCode
    erzeugt die Methoden equals und hashCode auf Basis aller oder einzelner Attribute.
  • @Data
    entspricht @Getter @Setter @EqualsAndHashCode. Zusätzlich wird auch ein Konstruktor erzeugt.

Eine detailierte Beschreibung dieser Annotationen sowie weiterer Bestandteile von Lombok findet man hier.

Zur Einbindung in Maven-Projekte reicht es, die folgende Dependency aufzunehmen:

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.16.8</version>
</dependency>

Neben den zur Laufzeit benötigten Klassen enthält die Bibliothek auch den erwähnten Annotation Processor, der die Generierung des zusätzlichen Programmcodes übernimmt.

Eclipse benötigt zur Integration von Lombok einen Java-Agenten. Er kann der Eclipse-Konfiguration durch Ausführung des Lombok-JAR-Files hinzugefügt werden: java -jar lombok-1.18.8.jar. Das Tool findet ggf. installierte Eclipse-Versionen nicht selbstständig. In dem Fall muss man im angezeigten Dialog das Eclipse-Verzeichnis angeben, in dem sich die Konfigurationsdatei eclipse.ini befindet (bzw. jbdevstudio.ini beim JBoss Developer Studio). Welche Änderungen das Tool an der Eclipse-Installation vornehmen wird, kann man sich zunächst anzeigen lassen.
NetBeans benötigt keine besondere Einstellung für Lombok, soweit die bearbeiteten Projekte Maven-Projekte sind (s. o.).
Für IntelliJ gibt es ein Lombok-Plugin, das aktiviert werden muss.

Für einfache Fachobjekte eignet sich @Data recht gut. Damit werden Getter, Setter, equals, hashCode und toString auf Basis aller Attribute bereitgestellt (s. Beispiel aus der Lombok-Doku).

Für JPA-Entities ist @Data aber zu starr: Zum einen wird ein Standardkonstruktor (ohne Parameter) benötigt, zum anderen sollen equals und hashCode nur die Id-Attribute benutzen. Daher sollten statt @Data eher die einzelnen Annotationen @Getter etc. verwendet und die Attributliste für @EqualsAndHashCode auf die Id-Attribute eingeschränkt werden:

@Entity
@Getter
@Setter
@EqualsAndHashCode(of = "isoCode")
@ToString
public class Country
{
  @Id
  private String    isoCode;

Für Entity-Klassen mit generierter Id reicht aber auch das nicht, da equals bei noch nicht gesetzter Id (null) in min.einem der beiden verglichenen Objekte false zurückgeben sollte. Die von Lombok generierte Methode liefert aber true.

Best Practice bei GEDOPLAN ist es daher, Entity-Klassen von einer der von uns geschriebenen Basisklassen SingleIdEntity, StringIdEntity, GeneratedIntegerIdEntity etc. abzuleiten. equals, hashCode und toString sind dann in der Basisklasse bereits korrekt implementiert. Die Lombok-Annotationen @EqualsAndHashCode und @ToString sind dann nutzlos bzw. kontraproduktiv. @Getter und @Setter können aber sinnvoll eingesetzt werden:

@Entity
@Access(AccessType.FIELD)
@Getter
@Setter
public class City extends GeneratedIntegerIdEntity
{

Die erwähnten Basisklassen stammen aus unserer Bibliothek baselibs-persistence, die mit folgender Maven-Dependency einbezogen werden kann:

<dependency>
  <groupId>de.gedoplan</groupId>
  <artifactId>baselibs-persistence</artifactId>
  <version>1.0.30</version>
</dependency>

An gleicher Stelle liegen auch die Sourcen dieser Klassen, wenn Sie dort mal hineinschauen möchten.

In https://github.com/GEDOPLAN/lombok-demo steht ein Showcase zum Anschauen und Ausprobieren bereit.

JPA mit dynamischer DB-Verbindung

In Java-EE-Anwendungen, die JPA zum DB-Zugriff nutzen, wird im Normalfall eine Datasource zur Spezifikation der Datenbankverbindung genutzt:

<persistence ...>
  <persistence-unit name="seminar">
    <jta-data-source>jdbc/seminar</jta-data-source>

Für eine so definierte Persistence Unit lässt man dann einen EntityManager injizieren – üblicherweise in einem CDI Producer:

public class EntityManagerProducer {
  @PersistenceContext(unitName = "seminar")
  EntityManager entityManager;

  @Produces
  public EntityManager getEntityManager() {
    return this.entityManager;

Das funktioniert so auch sehr gut, ist allerdings in Bezug auf die genutzte Datenbank recht statisch: Die Datasource – im Beispiel mit dem JNDI-Namen jdbc/seminar – muss zum Deployment-Zeitpunkt der Anwendung im Server konfiguriert sein. Zur Anwendungslaufzeit lässt sie sich nicht wechseln.

Um eine dynamische Zuordnung der DB zur Laufzeit zu ermöglichen, muss man die EntityManagerFactory per API initialieren und dabei die Connect-Parameter für die gewünschte Datenbank als Properties mitgeben:

<persistence ...>
  <persistence-unit name="showcase" transaction-type="JTA">
    <properties>
      <!-- Gemeinsame Parameter für die im Beispiel genutzten Datenbanken -->
      <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
      <property name="javax.persistence.jdbc.user" value="showcase" />
      <property name="javax.persistence.jdbc.password" value="showcase" />

      <!-- Diese Property wird eigentlich nicht benötigt, da sie beim Aufbau der EntityManagerFactory übergeben wird -->
      <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:" />
public class EntityManagerProducer {
  private EntityManagerFactory fetchEntityManagerFactory() {

    private ConcurrentHashMap<String, EntityManagerFactory> factoryMap = new ConcurrentHashMap<>();

    // Aktueller URL kommt aus anderem Service, z. B. "jdbc:h2:mem:showcase_1;DB_CLOSE_DELAY=-1"
    String url = ...; 

    // Falls nötig, Map-Eintrag für URL erstellen
    this.factoryMap.computeIfAbsent(url, u -> {
      Map<String, String> prop = new HashMap<>();

      // dabei URL als Property übergeben
      prop.put("javax.persistence.jdbc.url", url);

      // und DDL erlauben
      prop.put("eclipselink.ddl-generation", "create-or-extend-tables");
      prop.put("eclipselink.ddl-generation.output-mode", "database");
      prop.put("hibernate.hbm2ddl.auto", "update");

      return Persistence.createEntityManagerFactory("showcase", prop);
    });

    return this.factoryMap.get(url);
  }

...

Im Beispiel wird nur der Datenbank-URL dynamisch übergeben: Die Property javax.persistence.jdbc.url wird in Zeile 14 in ein Map-Obekt eingetragen und anschliessend zur Initialisierung der EntityManagerFactory verwendet. Im Beispiel wird ein ConcurrentHashMap genutzt, um die erzeugten Factories pro URL als Singletons zu führen.

Der Producer liefert nun einen EntityManager aus der so erzeugten EntityManagerFactory. Der Synchronisationstyp SynchronizationType.SYNCHRONIZED sorgt dafür, dass der EntityManager sich automatisch mit der JTA-Transaktion verbindet.
Da der EntityManager nun Application-managed ist, muss ein Disposer zum Schliessen vorgesehen werden:

...
  @Produces
  @RequestScoped
  EntityManager createEntityMnager()  {
    return fetchEntityManagerFactory().createEntityManager(SynchronizationType.SYNCHRONIZED);
  }

  void closeEntityManager(@Disposes EntityManager entityManager) {
    if (entityManager.isOpen()) {
      entityManager.close();

Auf diese Weise gelingt die Umschaltung der DB-Verbindung zur Laufzeit. Im Beispielprojekt sind die DB-URLs in einem separaten Service fest definiert. Sie könnten aber auch problemlos aus einer anderen, noch dynamischeren Quelle kommen.

Es zeigt sich hier einmal mehr, wie mächtig das Konzept der Producer in CDI ist: Die Dynamisierung der DBs geschieht alleine dort. Die konkreten DB-Zugriffe in der restlichen Anwendung bleiben unverändert.

Der sich nun möglicherweise einstellenden Euphorie muss allerdings ein „Aber“ entgegengestellt werden: Die gezeigte Lösung nutzt ein Feature aus, das in der JPA-Spezifikation nur schwach spezifiziert ist. Um einen EntityManager an JTA anbinden zu können, muss die Persistence Unit mit dem Parameter transaction-type="JTA" versehen sein. In dem Fall wird in der Spezifikation die Nutzung einer Datasource „erwartet“ – nicht etwa „verlangt“. Es ist sehr schade, dass die JPA-Spezifikation eine ganze Reihe solcher unterschiedlich interpretierbaren Sätze enthält.
In der Beispiellösung wird die DB-Verbindung über Properties angegeben, was Hibernate als JPA-Provider ohne Murren akzeptiert, während EclipseLink den Dienst verweigert. Insofern läuft die gezeigte Lösung nicht auf einem GlassFish (mit EclipseLink), wohl aber auf WildFly (mit Hibernate).

Den gezeigten Code finden Sie in einem kompletten Demoprojekt auf GitHub.

Java EE mit Java 8 verschönern!

J2EE war recht komplex und unübersichtlich, was viele durchaus zu Recht zu anderen Plattformen getrieben hat. Seit Java EE 5/6/7 ist das Thema aber deutlich verbessert: Der Fokus liegt nun eindeutig auf der Unterstützung der Entwickler und nicht mehr, wie zuvor, auf der für den Betrieb notwendigen Technik.

Durch Java 8 läßt sich nun noch eine weitere Verschönerung erreichen. Damit werden Java-EE-Anwendungen deutlich strukturierter!

Das folgende kleine Java-8-Programm tut den Trick:

package de.gedoplan.beantrial;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;

public class SourceCleaner
{

  public static void main(String[] args) throws IOException
  {
    for (String filename : args)
    {
      Files
        .lines(Paths.get(filename))
        .flatMapToInt(line -> line.codePoints())
        .mapToObj(i -> Character.valueOf((char) i))
        .collect(Collectors.groupingBy(x -> x, Collectors.summingInt(x -> 1)))
        .entrySet()
        .stream()
        .map(e -> new String(new char[e.getValue()]).replace('\0', e.getKey()))
        .forEach(System.out::println);
    }
  }
}

Aus einer unübersichtlichen Quelle wie der folgenden

package de.gedoplan.seminar.cdi.demo.intercept.interceptor;

import java.io.Serializable;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.apache.commons.logging.Log;

/**
 * Interceptor-Implementierung zu {@link TransactionRequired}.
 * 
 * @author dw
 */
@TransactionRequired
@Interceptor
public class TransactionRequiredInterceptor implements Serializable
{
  private static final long serialVersionUID = 1L;

  @Inject
  Log                       logger;

  @Inject
  UserTransaction           userTransaction;

  /**
   * Interceptor-Arbeitsmethode.
   * 
   * @param invocationContext InvocationContext
   * @return Returnwert
   * @throws Exception bei Fehlern
   */
  @AroundInvoke
  public Object manageTransaction(InvocationContext invocationContext) throws Exception
  {
    // Falls schon eine TX aktiv, Methode direkt aufrufen
    if (isTransactionActive())
    {
      return invocationContext.proceed();
    }

    // TX beginnen
    if (this.logger.isDebugEnabled())
    {
      this.logger.debug("Begin tx");
    }
    this.userTransaction.begin();

    try
    {
      // Methode aufrufen
      Object result = invocationContext.proceed();

      // TX committen
      if (this.logger.isDebugEnabled())
      {
        this.logger.debug("Commit tx");
      }
      this.userTransaction.commit();

      return result;
    }
    catch (Exception e) // CHECKSTYLE:IGNORE Allgemeine Exception ist hier OK
    {
      // TX zurückrollen
      if (this.logger.isDebugEnabled())
      {
        this.logger.debug("Rollback tx");
      }
      try
      {
        this.userTransaction.rollback();
      }
      catch (Throwable ignore)
      {
      }

      throw e;
    }

  }

  // Ist die TX aktiv?
  private boolean isTransactionActive() throws SystemException
  {
    return this.userTransaction.getStatus() == Status.STATUS_ACTIVE;
  }
}

wird die folgende, übersichliche und auch kürzere Form:

                                                                                                                                                                                                                                                                                                                                                                                                        
""""""
(((((((((((((((((((((
)))))))))))))))))))))
**************
,
--
...................................................
//////////////////
1
:
;;;;;;;;;;;;;;;;;;;;;;;;
====
?
@@@@@@@@@@
AAAAAAAA
B
CCCCCCCCCCC
DDDD
EEEEEEEEEEEEE
FF
G
H
IIIIIIIIIIIIIIIIII
KK
LLLL
MM
N
OOOO
RRRRRR
SSSSSSSSSS
TTTTTTTTTTTTTTTTTTTTTTT
UUUU
VV
XXXXX
Y
_
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbb
ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
ddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
fffffffff
ggggggggggggggggggggggggggggggggggggg
hhhhhhhhhhhhhhhhhhhhhhhhh
iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
jjjjjjjjjjjjjj
kkkkkkkkkk
llllllllllllllllllllllllllllllllllllllll
mmmmmmmmmmmmmmmmmmmmmmmmmmmmm
nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
ppppppppppppppppppppppppppppppppppppp
qqq
rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt
uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
vvvvvvvvvvvvvvvvvvvvvvv
wwwwwww
xxxxxxxxxxxxxxxxxxxxxxx
yyyy
zzzz
{{{{{{{{{{{{
ü
}}}}}}}}}}}}

Probieren Sie’s aus. Es funktioniert übrigens nicht nur für Java-Quellen, sondern auch für JavaScript, HTML, ja sogar für C#!