Temporale Datenhaltung in der Praxis mit Java

Know-how  –  4 Kommentare

Unterschiedliche Prozesse in Anwendungen sorgen dafür, dass sich Daten ständig verändern. CRUD-Operationen (Create Read Update Delete) gehören wahrscheinlich zu den meist implementierten Features in Geschäftsanwendungen. Oft sind die aktuellen Daten für die Benutzer am interessantesten, da sie die aktuelle Situation eines Objektes darstellen. In bestimmten Bereichen spielt jedoch die Historie, die auch als temporale Datenhaltung bekannt ist, eine übergeordnete Rolle.

Als Beispiel für einen Fall, in dem die Historie von Objekten von Bedeutung ist, lässt sich das allgemeine Vertragsmanagement nennen: Die Nachvollziehbarkeit der Datenänderungen bei einzelnen Verträgen muss in dem Fall – teilweise sogar gesetzlich vorgeschrieben – gewährleistet werden. Der vorliegende Artikel soll anhand einer einfachen Domäne der Adressendatenverwaltung das Konzept der temporalen Datenhaltung demonstrieren. Anschließend wird es mit einem entsprechenden Framework in Java implementiert (siehe dazu den Exkurs "Basis-Frameworks für temporale Implementierungsbeispiele").

Die Behandlung des Zeitbezugs (Temporalität, Historisierung) von Daten stellt ein zentrales Thema in vielen Geschäftsanwendungen dar. Sind die Daten noch gültig? Wann haben sie sich gegebenenfalls geändert? Inwieweit muss das System auf Änderungen der Wirklichkeit reagieren und zu welchem Zeitpunkt sollte es das tun?

Allgemein bezeichnet temporale Datenhaltung eine Technik zum Speichern der zeitlichen Entwicklung der Daten in einer Datenbank. In vielen Geschäftsanwendungen ist ihr Einsatz nicht mehr wegzudenken:

  1. Rentenzahlungen: Datenänderungen der Rentner sind stets zu verwalten und zu historisieren.
  2. Allgemeines Vertragsmanagement: Verträge lassen sich zu bestimmten Bedingungen verknüpfen. Welche Bedingungen zu welchem Zeitpunkt gültig sind, muss stets eindeutig sein.
  3. Standes- und Einwohnermeldeamt: Geburtsdaten, Umzüge und Sterbedaten sind zu historisieren und zu protokollieren.

Grundlagen der Temporalität

Die Behandlung des Zeitbezugs kann in mehreren Stufen erfolgen:

Stufe 0 – Ohne Zeitbezug (keine Temporalität und keine Historie): In dem Fall lassen sich ausschließlich Fragen zum aktuellen Status beantworten.

Stufe 1 – Temporaler mit fachlichem Zeitbezug beziehungsweise mit aktuellem Zeitbezug: In dem Fall ist die Gültigkeit eines Objektes mitzuspeichern, sodass sich Fragen zum fachlichen Sachverhalt innerhalb eines Zeitintervalls beantworten lassen. Diese Stufe realisieren Entwickler in Geschäftsanwendungen oft mit zusätzlichen Feldern wie "fachlich gültig von" und "fachlich gültig bis".

Stufe 2 – Temporaler mit Bearbeitungs- beziehungsweise Transaktionszeitbezug: Das Programm speichert die Bearbeitungszeit eines Objektes mit, sodass sich Fragen zu Objektänderungen innerhalb eines Zeitintervalls beantworten lassen. Typisches Beispiel hierfür ist das Verwenden von Versionierungssystemen wie CVS oder SVN zum Protokollieren von Veränderungen im Quellcode. Auch in diesem Fall tauchen häufig Felder wie "Bearbeitungsgültigkeit von" und "Bearbeitungsgültigkeit bis" auf.

Stufe 3 – Mit fachlichem beziehungsweise aktuellem Zeitbezug und gleichzeitigem Bearbeitungszeitbezug, bekannt als bitemporaler Zeitbezug: Die Anwendung speichert beide Zeitbezüge gleichzeitig, sodass sich sowohl Fragen zur fachlichen Gültigkeit als auch zu technischen Änderungen des Objektes beantworten lassen.

Möglichkeiten der Umsetzung

Java-Entwickler kommen beim Thema der Temporalität schnell zum Einsatz von ORM-Frameworks (Objekt Relational Mapper). Schließlich verwenden sie Java-Objekte und wollen – soweit möglich – von der Datenbankschicht abstrahiert arbeiten können. Derzeit unterstützen gängige SQL-Datenbanken das Konzept der Bitemporalität (Stufe 3) nicht direkt. Daher ist sie explizit in den betroffenen Anwendungen zu realisieren. Bei der Implementierung der Bitemporalität existieren prinzipiell zwei Wege:

(1) Datenbankgetrieben:

  • Implementierung der Bitemporalität in der betroffenen Datenbank mit Hilfe von Triggern und Stored-Prozeduren: Die Variante ist auszuwählen, sofern nicht ausschließlich Java-Anwendungen in die Datenbank schreiben.
  • Falls Oracle im Einsatz ist, lässt sich Oracle Flashback einsetzen. Entwickler können es für Abfragen verwenden, um Informationen aus der Vergangenheit zu selektieren. Hier ist zu beachten, dass Flashback ausschließlich das Speichern der Transaktionszeit beziehungsweise Bearbeitungszeit unterstützt (Stufe 2). In Kombination mit einer eigenen Implementierung für die Stufe 1 (fachlicher Zeitbezug) lässt sich das Konzept der Bitemporalität (Stufe 3) umsetzen.
  • TimeDB lässt sich als temporale Anfragesprache für SQL-Datenbanken nutzen. Es ist mit Java implementiert, fungiert als dünne Schicht zwischen Java-Anwendungen und SQL-Datenbanken und erweitert die SQL-Befehle um temporale Befehle. Das Produkt bietet sowohl eine Benutzeroberfläche als auch Programmierschnittstellen (APIs) an. Da das Projekt seit 2005 nicht mehr aktiv ist, lässt sich der Einsatz aus heutiger Sicht nicht mehr empfehlen.

(2) Objektgetrieben mit Java:

  • Die Implementierung auf der Objekt-Ebene beziehungsweise mit ORM-Frameworks ist in der Java-Umgebung zu bevorzugen. Sämtliche Zugriffe auf die Daten passieren über ein ORM-Framework. Open-Source-Frameworks wie DAOFusion, Hibernate Envers und Spring Data JPA ermöglichen eine solche Behandlung der Temporalität (siehe Exkurs: Quelloffene Java-Frameworks für die temporale Datenhaltung).

Implementierungsbeispiele mit Spring und JPA mit Hibernate

Um die Unterschiede der einzelnen genannten temporalen Stufe besser verstehen zu können, lassen sich passende Fragen auf Basis einer einfachen Adressendatenverwaltungsdomäne ausformulieren und beantworten. Den Schwerpunkt des Artikels soll die objektgetriebene Variante bilden. Das einfache Beispiel zeigt zunächst folgendes Verhalten (siehe Abb. 1):

  • Eine Person (Entität Person) besitzt keine oder eine Adresse.
  • Eine Adresse (Entität Address) gehört stets zu einer Person.

Es gibt einen Person- und Adressen-Service, mit deren Hilfe sich Personen und Adressen anlegen, aktualisieren und finden lassen. Zudem sollen einige Hilfsfunktionen in den Service-Klassen
unterkommen. Um das Beispiel simpel zu halten, bleibt das Löschen der Entitäten außen vor.

Beispiel einer einfachen Adressendatenverwaltungsdomäne (Abb. 1)


Auf der Basis der Adressendatenverwaltungsdomäne lassen sich nun Beispielimplementierungen für die Stufe 0 bis 3 darstellen und untersuchen.

(1) Stufe 0: Ohne Zeitbezug (keine Temporalität und keine Historie)

In dem Fall kann man ausschließlich folgende Frage beantworten: Wo wohnt die Person "Müller" derzeit? Es gibt eine einzige Adresse der ausgewählten Person. Solch ein Verhalten ist in Geschäftsanwendungen wahrscheinlich am weitesten verbreitet und sicherlich am einfachsten umzusetzen. Mit Hilfe eines ORM-Mapper-Frameworks wie Hibernate (als JPA-Implementierung) lassen sich solche Funktionen übersichtlich implementieren.

Das Beispiel non-temporal stellt den Code der Adressendatenverwaltungsdomäne ohne Zeitbezug zur Verfügung. Aus dem Domänenmodell (siehe Abbildung 1) kann der Generator Simple Java von KissMDA die Java-Schnittstellen generieren. Java-Implementierungsklassen realisieren Letztere anschließend. In dem Zusammenhang lässt sich die Klasse PersonImpl.java als Beispiel heranziehen, die die generierte Schnittstelle Person.java implementiert. Zu beachten ist die 1:1-Beziehung zu der Schnittstelle Address.java:

@Entity
@Table(name = "person")
public class PersonImpl implements Person, Serializable {
...
@OneToOne(targetEntity = AddressImpl.class)
private Address address;
...
}

Der Rest der Implementierungen mit der Klasse PersonRepository.java und der Service-Klasse PersonServiceImpl.java stellt bereits bekannte DAO- beziehungsweise Repository- und Service-Muster bei der Entwicklung mit Spring Framework und JPA dar.

Zum Testen der Anwendung steht eine Integrationstest-Klasse PersonTest.java zur Verfügung. In der Klasse sind einige Szenarien zu den obengenannten Fragen ausformuliert, die sich anschließend auf die Richtigkeit der Ergebnisse prüfen lassen:

...
public class PersonTest {
@Test
public void testCreateNonTemporalAddresses() {
...
// Erzeuge die erste Adresse
Address createdAddress1 = addressService.createAddressWithPerson(
firstAddress, createdPerson);
...
// Finde die richtige Person mit der aktuellen Adresse
Person updatedPerson = personService.findPersonById(
createdPerson.getId());
...
// Stimmt die Adresse mit der ersten Adresse?
Address firstCheckedAddress = updatedPerson.getAddress();
assertEquals(firstAddress.getCity(), firstCheckedAddress.getCity());
...
}
...
}

Zeit- und Fachbezug

(2) Stufe 1: Mit fachlichem beziehungsweise aktuellem Zeitbezug

Folgende Frage lässt sich auf dieser Stufe beantworten: Wo hat die Person "Müller" am 1. September 2012 tatsächlich gewohnt? Das entsprechende fachliche Gültigkeitsdatum (Feld validFrom) ist hierbei in der betroffenen Entität Address zu definieren und zu verwalten. Zudem ist die Beziehung zwischen den Klassen Person und Address als 1 zu n zu entwerfen, da mehrere Adressen mit unterschiedlicher fachlicher Gültigkeit zu einer Person existieren können. In Abbildung 2 ist der Sachverhalt grafisch dargestellt.

Beispiel um aktuellen beziehungsweise fachlichen Zeitbezug ergänzt (Abb. 2)


Das Beispiel actual-temporal zeigt eine naive Implementierung einer Adressenverwaltung mit einer fachlichen Gültigkeit der Adresse (Feld validFrom). Wie im ersten Beispiel bildet ausschließlich JPA die Entitäten ab. Die folgende JPA-Klasse PersonImpl.java implementiert die Schnittstelle Person:

@Configurable(preConstruction = true)
@Entity
@Table(name = "person")
public class PersonImpl implements Person, Serializable {
...
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
@Cascade(value = { CascadeType.ALL })
private final Collection<AddressImpl> addresses =
new ArrayList<AddressImpl>();

@Transient
@Inject
@Named("addressRepository")
private AddressRepository addressRepository;
...
@Override
public Address address(Date valid) {
...
}
...
}

Die Beziehung zwischen Personen und Adressen ist nun als 1 zu n zu entwerfen. Damit soll es möglich sein, mehrere Adressen mit unterschiedlicher Gültigkeit zu verwalten. Hierbei ist jedoch zu beachten, dass die Gültigkeit eindeutig sein muss. Es darf nicht mehrere Adressen im gleichen Gültigkeitszeitraum geben. Dies wird oft als fachliche Objektintegrität bezeichnet. Die Methode address(Date valid) in der Klasse PersonImpl.java liefert genau eine Adresse, die sich im vorgegebenen Gültigkeitsdatum befindet.

Im Beispiel ist das von Spring Framework AspectJ Annotation @Configurable gestellte Domain Driven Design im Einsatz. Die Entität Person beinhaltet tatsächlich nicht nur Eigenschaften wie firstname und lastname sondern auch Verhalten wie address(Date valid). Die genannte Methode nutzt die Klasse AddressRepository.java, um auf die richtige Adresse per JPQL (JPA Query Language) zugreifen zu können:

... 
public Address address(Date valid) {
Address currentAddressOnDate = getSingleAddress(valid);
return currentAddressOnDate;
}

Address getSingleAddress(Date validDate) {
Collection<AddressImpl> currentAddresses = addressRepository
.findByPersonIdAndValidity(this.id, validDate);
if (currentAddresses.size() == 0) {
return null;
} else if (currentAddresses.size() == 1) {
Address currentAddress = currentAddresses.iterator().next();
return currentAddress;
} else {
Address latestAddress = Collections.max(currentAddresses);
return latestAddress;
}
}
...

Zum Testen der Anwendung steht die Klasse PersonTest.java zur Verfügung. Sie zeigt beispielhaft die Verwendung der Service- und Entitätsklassen aus der Adressenverwaltungsdomäne:

...
public class PersonTest {
@Test
public void testCreateActualTemporalAddresses() {
...
// Die Adresse ist von 9.9.1999 gültig
Calendar cal = Calendar.getInstance();
cal.set(1999, Calendar.SEPTEMBER, 9);
firstAddress.setValidFrom(cal.getTime());

// Erzeuge die erste Adresse
Address createdAddress1 = addressService.createAddressWithPerson(
firstAddress, createdPerson);
Person updatedPerson = personService.findPersonById(createdPerson
.getId());

// Überprüfe, welche Adresse am 9.8.2008 gültig ist ==> firstAddress
...
cal.set(2008, Calendar.AUGUST, 9);
firstCheckedAddress = updatedPerson.address(cal.getTime());
assertEquals(firstAddress.getCity(), firstCheckedAddress.getCity());
...
}
...
}

(3) Stufe 2: Mit Bearbeitungszeitbezug

Folgende Frage zum Wohnsitz der Person Müller kann gestellt werden: Was wussten wir am 1. Oktober 2012 über die Wohnstatt der Person "Müller"? In der betroffenen Anwendung sind die transaktionalen Änderungen und Bearbeitungszeiten der Adressen gespeichert. Damit wird die Historisierung der Adressen sichergestellt. Änderungen der einzelnen Eigenschaften wie Straße (street), Stadt (city) und PLZ (code) hält das System fest.

In Abbildung 3 wird das Klassenmodell der Adressendatenverwaltungsdomäne dargestellt. In dem Fall ist zu beachten, dass die Klasse Person, wie im ersten Beispiel bei der Stufe 0, keine oder eine Adresse besitzt. Idealerweise soll das Speichern bei Änderungen der Klasse Address transparent stattfinden, sodass keine Änderung der Geschäftsentitäten nötig ist.

Beispiel um Bearbeitungs- beziehungsweise Transaktionszeitbezug erweitert (Abb. 3)


Das Beispielprojekt record-temporal-envers setzt Stufe 2 mit dem Open-Source-Framework Hibernate Envers um. Mit ihm ist die Historisierung der Entität Address transparent. Die Klasse PersonImpl.java entspricht dem Beispiel in der Stufe 0. Im Gegensatz hierzu ist die Klasse
AddressImpl.java mit einer Envers-Annotation @Audited zu markieren, sodass Envers die Historisierung der Adressenobjekte übernehmen kann. Im Beispielprojekt muss die Entität Person nicht historisiert werden, daher ist der Eintrag targetAuditMode auf RelationTargetAuditMode.NOT_AUDITED gesetzt:

... 
@Entity
@Table(name = "address")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
public class AddressImpl implements Address, Serializable {
...
}

Aus den JPA-Klassen lassen sich Datenbanktabellen generieren. Die Struktur der Datenbank ist in Abbildung 4 dargestellt. In der Tabelle ADDRESS_AUD hält die Anwendung die historisierten Adressendaten vor (Abbildung 4 auf der rechten Seite). Darüber hinaus kommen einige Informationstabellen wie REVCHANGES (Name der historisierten Entitäten, im Beispiel AddressImpl) und REVINFO (Revisionsnummer und Revisionszeit) dazu, die allgemeine Eckdaten über die Historisierung der Entitäten beinhalten.

Datenbankstruktur und -inhalt mit Hibernate Envers (Abb. 4)


Die Verwendung von Envers ist für die Java-Entwickler transparent. Jedes Erstellen eines Objekts mit JPA-Entitymanager wird automatisch historisiert. Der Entwickler speichert das Adresse-Objekt wie gewohnt:

... 
public Address save(Address address) {
em.persist(address);
return address;
}
...

Um die historisierten Daten abzufragen, kommt die Anfragesprache von Envers zum Einsatz. Sie ist der Hibernate-Criteria-Anfragesprache sehr ähnlich. Die Envers-Dokumentation gibt einen Überblick und stellt einige Beispiele zur Verfügung. Mit folgender Anfrage lassen sich beispielsweise sämtliche Adressen mit einer bestimmten Revisionsnummer finden:

... 
public Collection<Address> findAuditedAdressesWithRevision(
Integer revisionNumber) {
// Finde sämtliche Adressen, die sich in einer bestimmten
// Revisionsnummer befinden
Collection<Address> auditedAddresses =
getAuditReader().createQuery()
.forEntitiesAtRevision(AddressImpl.class, revisionNumber)
.getResultList();
return auditedAddresses;
}
...

Bitemporaler Zeitbezug

(4) Stufe 3: Mit aktuellem Zeit- und gleichzeitigem Bearbeitungszeitbezug (bekannt als bitemporaler Zeitbezug)

In dem Zustand lassen sich umfangreiche Fragen beantworten: Was wussten wir am 1. Oktober 2012 über den tatsächlichen Wohnsitz der Person "Müller" am 1. September 2012? Das Beispiel in Abbildung 5 zeigt den gleichen Sachverhalt jedoch mit Betrachtung der Bitemporalität. Die Entität Person besitzt in dem Fall eine 1:n-Beziehung zur Entität Address.java, da eine Person technisch mehrere Adressen besitzen kann. Die wiederum besitzen sowohl mehrere fachliche als auch transaktionale Gültigkeiten, die sich in der Beziehung zur Entität BitemporalAddress.java darstellen lassen.

Die Methode address() liefert die Klasse WrappedBitemporalProperty, die als Wrapper für eine bitemporale Eigenschaft aus dem Framework DAOFusion dienen soll. Mit der Klasse WrappedBitemporalProperty lassen sich bitemporale Zugriffe auf die Entitäten regeln. Zusätzlich zur Adressenentität ist die Methode alive() ebenfalls bitemporal gehalten. Sie stellt dar, ob die Person noch am Leben ist.

Beispiel mit bitemporalem Zeitbezug (Abb. 5)


Das Beispielprojekt bitemporal-daofusion zeigt eine Implementierung des genannten Sachverhalts mit dem Framework DAOFusion. In der Klasse PersonImpl.java lässt sich die Entität Person realisieren:

@Entity
@Table(name = "person")
public class PersonImpl extends MutablePersistentEntity implements Person {
...
@OneToMany(fetch = FetchType.LAZY)
@Cascade(value = { CascadeType.ALL, CascadeType.DELETE_ORPHAN })
@JoinColumn(name = "person")
private final Collection<BitemporalAddressImpl> bitemporalAddresses =
new LinkedList<BitemporalAddressImpl>();
...
@OneToMany(fetch = FetchType.LAZY)
@Cascade(value = { CascadeType.ALL, CascadeType.DELETE_ORPHAN })
@JoinColumn(name = "person")
private final Collection<BitemporalBooleanImpl> bitemporalAlives =
new LinkedList<BitemporalBooleanImpl>();
...

Von besonderer Bedeutung sind die zwei Eigenschaften bitemporalAddresses und bitemporalAlives, die für die bitemporale Beziehung zuständig sind. Sie zeigen jeweils die Sammlung (als Java-Collection definiert) der Klassen BitemporalAddressImpl und BitemporalBooleanImpl, die sich wie folgt umsetzen lassen:

@Entity
@Table(name = "bitemp_addr")
public class BitemporalAddressImpl extends BitemporalWrapper<Address>
implements BitemporalAddress {

@ManyToOne(targetEntity = AddressImpl.class,
cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private Address value;
...

@Entity
@Table(name = "bitemp_boolean")
public class BitemporalBooleanImpl extends BitemporalWrapper<Boolean> {
...

Beide Klassen erweitern die aus dem DAOFusion-Framework stammende Klasse BitemporalWrapper um die Typen Address für die Methode address() und Boolean für die Methode alive(). Für die Klasse BitemporalAddressImpl (Entität BitemporalAddress aus Abbildung 5) ist zusätzlich eine Beziehung n zu 1 zur Adressenentität umzusetzen.

Auf Objekte zugreifen

Die Implementierung der Methoden address() und alive() in der Klasse PersonImpl setzt den Einsatz des Wrappers WrappedBitemporalProperty für die jeweiligen bitemporalen Entitäten BitemporalAddressImpl und BitemporalBooleanImpl voraus. Der Zugriff auf die bitemporalen Objekte geschieht anschließend ausschließlich über diesen Wrapper:

@Entity
@Table(name = "person")
public class PersonImpl extends MutablePersistentEntity implements Person {
...
@Override
public WrappedBitemporalProperty<Address, BitemporalAddressImpl> address() {
return new WrappedBitemporalProperty<Address, BitemporalAddressImpl>(
bitemporalAddresses, new WrappedValueAccessor<Address,
BitemporalAddressImpl>() {

@Override
public BitemporalAddressImpl wrapValue(Address value,
Interval validityInterval) {
return new BitemporalAddressImpl(value, validityInterval);
}
});
}

@Override
public WrappedBitemporalProperty<Boolean, BitemporalBooleanImpl> alive() {
return new WrappedBitemporalProperty<Boolean, BitemporalBooleanImpl>(
bitemporalAlives, new WrappedValueAccessor<Boolean,
BitemporalBooleanImpl>() {

@Override
public BitemporalBooleanImpl wrapValue(Boolean value,
Interval validityInterval) {
return new BitemporalBooleanImpl(value, validityInterval);
}
});
}
...

Die JPA-Klassen dienen dazu, die Tabellen ADDRESS, PERSON, BITEMP_BOOLEAN und BITEMP_ADDRESS zu generieren (Abb. 6). In den bitemporalen Tabellen sind Informationen wie RECORDFROM, RECORDTO (Bearbeitungszeitbezug) sowie VALIDFROM, VALIDTO (aktueller Zeitbezug) gespeichert.

Datenbankstruktur und -inhalt mit DAOFusion (Abb. 6)


Entitäten bitemporal verwenden

Für die bitemporale Nutzung der Entitäten steht, wie oben erwähnt, die Wrapperklasse WrappedBitemporalProperty zur Verfügung. Sie erweitert wiederum die Klasse BitemporalProperty,
die den API-Nutzern Methoden zum Zugriff und Anfragen der bitemporalen Objekte wie set(), get(), now() und hasValueOn() zur Verfügung stellt. Im Quellcode der Klasse BitemporalProperty ist die Nutzung der APIs ausführlich dokumentiert.

Die Klasse ScenarioTest.java setzt bekannte bitemporale Szenarien um. Das Erzeugen der ersten Adresse führt die Service-Klasse AddressServiceImpl mit der Methode createAddressWithPerson() durch. In das Zeitintervall-Objekt ist der aktuelle Zeitbezug (fachlicher Zeitbezug) einzutragen. Im Beispiel stellt er die Zeit dar, ab wann die entsprechende Person an welcher Adresse gemeldet ist.

...
@Transactional(propagation = Propagation.REQUIRED)
public Address createAddressWithPerson(Address address, Person person,
Interval interval) {
// Persistiere das Adressenobjekt
Address addressCreated = addressRepository.save(address);
Person personFound = personService.findPersonById(person.getId());
// Erzeuge die bitemporalen Eigenschaften des Adressenobjektes
personFound.address().set(addressCreated, interval);
return addressCreated;
}
...

Bevor sich die Service-Methode aufrufen lässt, ist der Bearbeitungszeitbezug festzusetzen. Im Beispiel stellt er die Zeit dar, wann eine Person in der Anwendung die Anmeldung bezüglich ihres Wohnorts durchführt:

...
addressService.setTimeReference(TimeUtils.day(4, 4, 1975));
...

Analog funktioniert die Methode alive(). Mit den folgenden Zugriffen stirbt die Person "John Doe", wobei diese Information ebenfalls bitemporal gehalten wird.

...
johnDoe = personService.findPersonByIdWithAlives(johnDoe.getId());
johnDoe.alive().set(false);
johnDoe = personService.updatePerson(johnDoe);
...

Um anschließend die Anfrage an die bitemporalen Adressen via BitemporalProperty durchzuführen, lassen sich folgende Zugriffe mit beispielsweise on() und now() verwenden:

...
Address addressValue4 = (Address) johnDoe.address().on(
TimeUtils.day(26, 8, 1994), TimeUtils.day(27, 12, 1994));
assertEquals(addressCheck2.getCity(), addressValue4.getCity());

Address addressValue5 = (Address) johnDoe.address().now();
...

Ließ sich der Test erfolgreich durchführen, stehen neue Adressendatensätze in der entsprechenden Tabelle ADDRESS zur Verfügung. Die Datensätze sind in Abbildung 7 dargestellt.

Datenbankinhalt ADDRESS nach dem Ausführen des Tests (Abb. 7)


Zu den neuen Adressendatensätzen werden ebenfalls neue bitemporale Datensätze in der Tabelle BITEMP_ADDRESS angelegt [Abb. 8]. Die Beziehung zur Tabelle ADDRESS legt das Feld VALUE_ID fest: Zur Adresse "Smallville" gehören zwei bitemporale Datensätze mit ID 0 und 1. Durch den Umzug zur Adresse "Bigtown" aktualisiert die Anwendung den Bearbeitungszeitbezug des Datensatzes mit ID 0 (RECORDTO mit 27.12.1994 abgeschlossen). Anschließend erzeugt sie einen neuen Datensatz mit ID 1, um den aktuellen beziehungsweise fachlichen Zeitbezug (Umzugtermin, VALIDTO mit 26.08.1994) abzuschließen.

Zur Adresse "Bigtown" gehört ein bitemporaler Datensatz mit ID 2. Für die neue Adresse legt man am Ende einen Datensatz mit offenem Bearbeitungszeitbezug (RECORDTO mit 14.09.3197) und offenem aktuellen Zeitbezug (VALIDTO mit 14.09.3197) an.

Fazit

Im Prinzip werden aus einem neuen Adressendatensatz zwei zusätzliche bitemporale Datensätze erstellt und der bitemporale Ursprungsdatensatz gleichzeitig verändert ("Aus Einem mach' Drei"-Prinzip). Dieser Prozess ist für jedes angelegte Adressenobjekt zu wiederholen.

Datenbankinhalt BITEMP_ADDRESS nach Ausführung des Tests (Abb. 8)


Ein wichtiges Thema ist die (bi)temporale Objektintegrität. Wie bereits in Stufe 1 erwähnt, muss sich das Framework in dem Fall ebenfalls darum kümmern, dass zu einem Zeitpunkt stets ein eindeutiges Objekt (in dem Fall Adresse) existiert. Ausführliche Informationen zur temporalen Integrität lassen sich an anderer Stelle nachlesen.

Fazit

Das Konzept der Temporalität gehört zum festen Bestandteil jeder Geschäftsanwendung. Daten sind dort oft zu historisieren, um die Nachvollziehbarkeit eines Geschäftsobjektes zu ermöglichen. Je nach Situation müssen Architekt oder Entwickler eine Stufe von 0 (keine Temporalität) bis 3 (Bitemporalität) festlegen. Die Wahl ist sehr wichtig, denn eine falsche Entscheidung kann zu einer aufwändigen Implementierung und gleichzeitig zu einer schlechten Systemperformanz im Betrieb führen. Schließlich muss die darunterliegende Datenbank alle zusätzlichen Daten, die durch die Temporalität entstanden sind, verwalten.

Aus Erfahrungen des Autors reicht Stufe 1 oder 2 für viele Anwendungsfälle aus. Die Entscheidung für den Einsatz der Stufe 3 (Bitemporalität) für ausgewählte Geschäftsobjekte muss stets durchdacht und gut mit der Fachseite diskutiert sein. Falls Stufe 1, 2 oder 3 notwendig ist, lässt sich auf Open-Source-Frameworks wie DAOFusion, Hibernate Envers und Spring Data JPA zurückgreifen. Eigene Implementierungen und ein entsprechender API-Entwurf sind meistens nicht sinnvoll und verursachen später mehr Kosten bei der Wartung der Anwendungen.

Dr. Lofi Dewanto
arbeitet als Softwarearchitekt und -entwickler bei der Deutschen Post Renten Service. Er engagiert sich besonders für "javanische" Open-Source-Software sowie modellgetriebene Softwareentwicklung mit UML.

Exkurse

Exkurs: Basis-Frameworks für temporale Implementierungsbeispiele

Als Basis-Frameworks werden folgende Open-Source Java-Projekte verwendet:

  • KissMDA: Mit dem Framework lassen sich die in UML modellierten Klassen direkt als Java-Schnittstellen generieren. Eine aktuelle Dokumentation ist so stets gewährleistet.
  • Spring Framework: Spring fungiert als Basis sowohl für Context- und Dependency-Injection, als auch für die Integrationsplattform (Transaktionen, Objektspeicherung und Integrationstest).
  • Hibernate: Das ORM-Mapper-Framework kommt für die Implementierung der JPA-Spezifikation zum Einsatz.
  • Maven: Mit dem Build- und Abhängigkeitsverwaltungswerkzeug werden sämtliche Beispielprojekte gebaut.

Auf GitHub stehen sämtliche Quellcodes der Beispiele zur Verfügung.

Exkurs: Quelloffene Java-Frameworks für die temporale Datenhaltung

  • Entwurfsmuster für die Temporalität: Die Beiträge von Martin Fowler dienen als Basis für die Umsetzung der objektorientierten Temporalität.
  • Hibernate Envers: Envers erweitert Hibernate um Auditing und Versionierung der persistenten Java-Objekte. Ab Version 3.5 liefert Hibernate Envers direkt mit, sodass das Modul nicht mehr separat zu installieren ist. Hibernate Envers unterstützt den Mechanismus der Temporalität mit Bearbeitungs- beziehungsweise Transaktionszeitbezug (Stufe 2). Bitemporalität unterstützt das Framework nicht direkt, weshalb der Entwickler sie selbst implementieren muss.
  • Spring Data JPA: Ein einfaches Implementieren des Repository-Musters beziehungsweise des Data Access Object (DAO) ermöglicht das Projekt Spring Data JPA. Es erweitert JPA darüber hinaus um eine Auditing- und Versionierungsfunktion. Spring Data JPA unterstützt - wie Hibernate Envers - Stufe 2 der temporalen Datenhaltung, allerdings keine Bitemporalität. Eine Integration von Hibernate Envers in Spring Data JPA für die temporale Datenhaltung bietet das Projekt Spring Data Envers.
  • DAOFusion: Die Implementierung der Bitemporalität (Stufe 3) in DAOFusion nimmt das Konzept des bitemporalen Frameworks in Java von Erwin Vervaet (Schöpfer von Spring Web Flow) zur Grundlage und fügt einige Erweiterungen hinzu. Es umfasst vergleichsweise benutzerfreundliche Schnittstellen (APIs) und bitemporale Integration für JPA.