Temporale Datenhaltung in der Praxis mit Java

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;
}
...