Temporale data – deel 2: het temporale pattern
By: gnoij, 7 December 2008Zoals al in het eerste deel te zien was is temporale data op te delen in temporale (alleen geldigheid) of bitemporale data (geldigheid met registratietijd). In dit tweede deel beschrijf ik het pattern voor temporale data.
De kern van dit pattern zijn de Java klassen TemporalObject en TemporalProperty. TemporalObject bevat de logica voor de geldigheid en TemporalProperty bevat de code om een de juiste versie van een property te kunnen bepalen.
TemporalObject
public abstract class TemporalObject { private Date ingangsdatum; private Date einddatum; protected TemporalObject(Date ingangsdatum) { super(); if (ingangsdatum == null) { throw new IllegalArgumentException("Ingangsdatum is verplicht"); } this.ingangsdatum = ingangsdatum; } public boolean isGeldigOp(Date peildatum) { boolean geldig = peildatum.after(this.ingangsdatum) || peildatum.equals(this.ingangsdatum); if (this.einddatum != null) { geldig = geldig && peildatum.before(this.einddatum); } return geldig; } public Date getIngangsdatum() { return this.ingangsdatum; } public Date getEinddatum() { return this.einddatum; } public void beeindig(Date einddatum) { this.einddatum = einddatum; } }
Deze klasse is de superklasse van een klasse met geldigheid. Deze klasse bevat de ingangsdatum en einddatum van een versie. Deze klasse bevat alleen een constructor met ingangsdatum, omdat deze verplicht is (anders kunnen we geen historie bijhouden). Verder kent deze klasse de methode om de te vragen of een object geldig is op een bepaalde datum en een methode om een instantie te kunnen beëindigen. Deze klasse is gebaseerd op het Effectivity Pattern van Martin Fowler.
TemporalProperty
public class TemporalProperty { protected List alleHistorischeVersies = new ArrayList(); public TemporalProperty() { super(); } public TemporalObject getActueleVersie() { return getVersieOp(new Date()); } public TemporalObject getVersieOp(final Date peildatum) { return (TemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() { public boolean evaluate(Object object) { return ((TemporalObject) object).isGeldigOp(peildatum); } }); } public void setActueleVersie(TemporalObject actueleVersie) { if (actueleVersie == null) { throw new IllegalArgumentException("actueleVersie is null"); } beeindigVorigeVersie(actueleVersie); this.alleHistorischeVersies.add(actueleVersie); } private void beeindigVorigeVersie(final TemporalObject actueleVersie) { TemporalObject teBeeindigenVersie = (TemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() { public boolean evaluate(Object object) { TemporalObject versie = (TemporalObject) object; return versie.isGeldigOp(actueleVersie.getIngangsdatum()); } }); if (teBeeindigenVersie != null) { teBeeindigenVersie.beeindig(actueleVersie.getIngangsdatum()); } } public void beeindig(Date einddatum) { TemporalObject teBeeindigenVersie = getActueleVersie(); if (teBeeindigenVersie != null) { teBeeindigenVersie.beeindig(einddatum); } } }
Deze klasse wordt gebruikt om alle historische versies van een property bij te houden. Deze klasse kent methoden om de actuele versie op te vragen, om de versie die op een bepaalde datum geldig is op te vragen en om een nieuwe actuele versie toe te voegen of te beëindigen. Als er een nieuwe actuele versie wordt toegevoegd, wordt de op dat moment geldende versie beëindigd. Deze klasse is gebaseerd op de TemporalProperty Pattern van Martin Fowler.
Let op: Fowler noemt deze klasse TemporalCollection. Deze naam gebruik ik voor de lijst properties met historie, die ik in deel 4 zal behandelen.
Voorbeeld
Om de werking van bovenstaande klassen uit te leggen gebruiken we het voorbeeld uit deel 1.
Kees werkte vanaf 1-1-2003 op de afdeling Inkoop. Vanaf 1-1-2006 werkt hij op de afdeling Verkoop. Deze overgang werd op 1-2-2006 geregistreerd in het systeem.
We hebben hier te maken met de klassen Werknemer en Afdeling. Omdat de afdeling bij meerdere werknemers voor kan komen gebruiken we een AfdelingRelatie klasse om de geldigheid van een afdeling bij een werknemer te registreren.
Het klasse model ziet er als volgt uit:
Werknemer
public class Werknemer { private TemporalProperty afdelingRelatie = new TemporalProperty(); private String naam; public Werknemer(String naam) { super(); this.naam = naam; } public String getNaam() { return this.naam; } public void setAfdeling(Afdeling afdeling, Date ingangsdatum) { this.afdelingRelatie.setActueleVersie(new AfdelingRelatie(afdeling, ingangsdatum)); } public Afdeling getAfdeling() { AfdelingRelatie relatie = getAfdelingRelatie(); if (relatie != null) { return relatie.getAfdeling(); } return null; } public Afdeling getAfdeling(Date peildatum) { AfdelingRelatie relatie = (AfdelingRelatie) this.afdelingRelatie.getVersieOp(peildatum); if (relatie != null) { return relatie.getAfdeling(); } return null; } public AfdelingRelatie getAfdelingRelatie() { return (AfdelingRelatie) this.afdelingRelatie.getActueleVersie(); } public AfdelingRelatie getAfdelingRelatie(Date peildatum) { return (AfdelingRelatie) this.afdelingRelatie.getVersieOp(peildatum); } public void uitDienst(Date datumUitdienst) { this.afdelingRelatie.beeindig(datumUitdienst); } }
De klasse Werknemer bevat een temporal property afdelingRelatie die alle historische versies van de afdelingen van de werknemer bevat. Verder bevat deze klasse de public methoden om de huidige afdeling op te vragen, de afdeling waar de werknemer werkte op een bepaalde datum op te vragen en om een nieuwe afdeling te koppelen aan de werknemer. Het opvragen en koppelen van de afdeling verloopt via de relatieklasse AfdelingRelatie, die de historie van een enkele versie bijhoudt. Als een werknemer uit dienst gaat, wordt de actuele versie van de relatie beëindigd.
AfdelingRelatie
public class AfdelingRelatie extends TemporalObject { private Afdeling afdeling; protected AfdelingRelatie(Afdeling afdeling, Date ingangsdatum) { super(ingangsdatum); this.afdeling = afdeling; } public Afdeling getAfdeling() { return this.afdeling; }
De klasse AfdelingRelatie is afgeleid van de klasse TemporalObject omdat deze klasse de geldigheid bevat van de relatie tussen de werknemer en de afdeling. Omdat een temporal object immutable is, kan er alleen maar een nieuwe versie via de constructor worden aangemaakt met een ingangsdatum die de datum voorstelt waarop de werknemer op de nieuwe afdeling komt te werken.
Afdeling
public class Afdeling { private String naam; public Afdeling(String naam) { super(); this.naam = naam; } public String getNaam() { return this.naam; } }
De klasse Afdeling is een eenvoudige klasse die voor ons voorbeeld alleen de naam van de afdeling bevat.
Het gebruik van de klassen en methoden volgt uit de volgende unit test.
public void testWerknemer() { Werknemer werknemer = new Werknemer("Kees"); Afdeling afdeling = new Afdeling("Inkoop"); werknemer.setAfdeling(afdeling, DateUtils.maakDate(2003, 1, 1)); afdeling = new Afdeling("Verkoop"); werknemer.setAfdeling(afdeling, DateUtils.maakDate(2006, 1, 1)); assertEquals("Kees", werknemer.getNaam()); assertEquals("Verkoop", werknemer.getAfdeling().getNaam()); assertEquals("Verkoop", werknemer.getAfdeling(DateUtils.maakDate(2006, 1, 15)).getNaam()); assertEquals("Inkoop", werknemer.getAfdeling(DateUtils.maakDate(2004, 1, 1)).getNaam()); werknemer.uitDienst(DateUtils.maakDate(2008,1,1)); assertNull(werknemer.getAfdeling()); }
In deze test maken we een werknemer Kees aan, die begint te werken op de afdeling Inkoop op 1 januari 2003. Vanaf 1 januari 2006 werkt Kees op de afdeling Verkoop en vanaf 1 januari 2008 is Kees uit dienst.
Hibernate mappings
Tenslotte wil ik nog de Hibernate mappings voor de klassen Werknemer en AfdelingRelatie tonen om te laten zien hoe dit in zijn werk gaat.
Werknemer.hbm.xml
<hibernate-mapping package="nl.ordina.temporal.example" default-access="field"> <class name="Werknemer" table="Werknemer"> ... <property name="naam" column="naam" /> <!-- de verwijzing naar de afdelingrelatie --> <component name="afdelingRelatie" class="nl.ordina.temporal.singletemporal.TemporalProperty" lazy="false"> <bag name="alleHistorischeVersies" cascade="all, delete-orphan" lazy="false"> <key column="werknemerId" /> <one-to-many class="AfdelingRelatie" /> </bag> </component> </class> </hibernate-mapping>
De temporal property afdelingRelatie wordt als een component opgenomen in de mapping van Werknemer. Hierbinnen wordt de lijst alleHistorischeRelaties gemapt als een bag.
AfdelingRelatie.hbm.xml
<hibernate-mapping package="nl.ordina.temporal.example" default-access="field"> <class name="AfdelingRelatie" table="AfdelingRelatie"> ... <!-- temporale informatie --> <property name="ingangsdatum" type="date" /> <property name="einddatum" type="date" /> <!-- de verwijzing naar de afdeling --> <many-to-one name="afdeling" lazy="false" class="Afdeling" column="afdelingId" cascade="none" /> </class> </hibernate-mapping>
De mapping van de AfdelingRelatie bevat de properties ingangsdatum en einddatum en een many-to-one relatie met de klasse Afdeling, die hier verder niet getoond wordt.
In het volgende deel zal het bitemporale pattern getoond worden. Hierbij zal het voorbeeld worden uitgebreid met registratiegegevens.

7 December 2008 om 3:59 pm
Zo op het eerste gezicht lijkt het me een patroon dat werkt. Ik vind het alleen wel jammer dat je met een abstract class werkt. Ik zou persoonlijk toch sneller met een associatie werken, want AfdelingRelatie is-a TemporalObject vind ik persoonlijk niet zo charmant.
In JPA 1.0 heb je alleen de beperking dat je in een @Embeddable geen associaties mag gebruiken. Dan moet je terugvallen op plain Hibernate of wachten op JPA 2.0.
Maar ik zou persoonlijk toch kijken naar een oplossing die niet met abstract classes werkt, want dan haal je jezelf mogelijk veel beperkingen op de hals.
8 December 2008 om 12:25 pm
Het voordeel van een abstracte klasse is dat je bij afleiding meteen alle functionaliteit voor temporale data tot je beschikking hebt. Maar ik snap de beperkingen ook wel, omdat je geen multiple inheritance hebt (bij bijvoorbeeld een generieke basisklasse (met je primary key oid) die dan ook de basisklasse moet zijn van TemporalObject.
Ik heb nog even gedacht aan een interface (met een methode getTemporalObject) die je dan moet implementeren, maar dan moet je in de implementatie wel TemporalObject als property toevoegen en deze teruggeven in de geimplementeerde methode. Ook de code voor TemporalProperty wordt dan iets anders omdat je dan de interface moet gebruiken om je TemporalObject te kunnen benaderen. En ook de mappings worden ingewikkelder omdat TemporalObject (met ingangs- en einddatum) dan als component gemapt moet worden.
Bovenstaande oplossing leek mij voor het doel (een introductie in temporale data) iets eenvoudiger.
11 December 2008 om 8:48 am
Het ziet er zuiver uit om in plaats van entiteiten via historie aan elkaar te koppelen dit per property te doen. Maar let op, dat wordt heeeeeel veel werk in een database waarin bv 5 enteiten historie t.o.v. elkaar hebben en elk bv 10 properties hebben.
Ik ben daarom stiekem best geinteresseerd hoe het er in de (ER) database uitziet en of er een ‘ORM hell’ ontstaat. Is AfdelingRelatie een koppeltabel geworden?
12 December 2008 om 5:32 pm
Update: Envers is nu onderdeel van JBoss geworden.
Met Envers kun je eenvoudig auditing/versioning van je entities doen. Let op, met versioning bedoel ik dus versionering en niet optimistic locking m.b.v. een version property.
Zie: http://www.jboss.org/envers/
16 December 2008 om 2:57 pm
We kennen binnen ons project twee soorten:
Een voorbeeld van de eerste soort is de relatie tussen Persoon en Adres. Deze wordt gemapt met een aparte klasse AdresRelatie, die de historie bevat van de relatie tussen de persoon en het adres. Deze AdresRelatie zal dus (analoog aan de AfdelingRelatie in de blog) gemapt worden als TemporalProperty.
Een voorbeeld van de tweede soort is de naamgeving van de persoon. Als een persoon huwt, kan er een wijziging in de naamgeving ontstaan (een vrouw kan bijvoorbeeld de achternaam van de man aannemen). De Naamgeving is integraal onderdeel van de Persoon (maar wel met aparte historie) en zal dus als TemporalProperty gemapt worden binnen de Persoon.
Wat de ORM-hell betreft, alleen voor de extra relatie klassen komt er een tabel bij in de database vergeleken met een versie zonder temporale data. De objecten houden namelijk zelf hun historie bij. Die historie betreft de relatie tussen de gemapte property en de klasse waarbinnen de property is gemapt. Alleen associaties worden als TemporalProperty gemapt, simpele properties (zoals strings, booleans etc.) kennen geen aparte historie.
22 December 2008 om 1:18 pm
[...] dit derde deel van de serie over temporale data breidt ik het temporale pattern uit het tweede deel uit met de registratietijd voor opvoer en afvoer. Hierdoor ontstaat het bitemporale [...]