Temporale Datenhaltung in der Praxis mit Java

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.