Temporale data – deel 4: bitemporale collections
By: gnoij, 7 December 2009Het is alweer enige tijd geleden dat ik heb geschreven over temporale patterns. In dit deel wil ik het bitemporale pattern uitbreiden met bitemporale collections. Werd met de bitemporale property nog een enkele property historisch gemaakt, in dit deel maken we een lijst historisch. Dat wil zeggen dat er meerdere elementen van een historische lijst tegelijkertijd geldig kunnen zijn.
De klasse BiTemporalObject uit het derde deel blijft hetzelfde. En naast de BiTemporalProperty voor enkelvoudige historische relaties krijgen we nu ook een klasse BiTemporalCollection voor meervoudige historische relaties.
BiTemporalCollection
public class BiTemporalCollection { protected List alleHistorischeVersies = new ArrayList(); public BiTemporalCollection() { super(); } public List getActueleVersies() { return getVersieOp(new Date(), new Date()); } public List getVersieOp(final Date registratieTijdstip, final Date peilDatum) { return (List) CollectionUtils.select(this.alleHistorischeVersies, new Predicate() { public boolean evaluate(Object object) { return ((BiTemporalObject) object).isGeldigOp(registratieTijdstip, peilDatum); } }); } public void voegActueleVersieToe(BiTemporalObject actueleVersie) { if (actueleVersie == null) { throw new IllegalArgumentException("actueleVersie is null"); } this.alleHistorischeVersies.add(actueleVersie); } public void wijzigActueleVersie(BiTemporalObject nieuweVersie, BiTemporalObject oudeVersie) { if (nieuweVersie == null) { throw new IllegalArgumentException("nieuweVersie is null"); } if (oudeVersie == null) { throw new IllegalArgumentException("oudeVersie is null"); } beeindigVorigeVersie(nieuweVersie, oudeVersie); this.alleHistorischeVersies.add(nieuweVersie); } private void beeindigVorigeVersie(final BiTemporalObject nieuweVersie, final BiTemporalObject oudeVersie) { BiTemporalObject afTeVoerenVersie = zoekActueleVersie(oudeVersie, nieuweVersie.getIngangsdatum(), nieuweVersie.getOpvoerTijdstip()); if (afTeVoerenVersie == null) { throw new IllegalStateException("Geen oude actuele versie gevonden!"); } beeindigVersie(afTeVoerenVersie, nieuweVersie.getIngangsdatum(), nieuweVersie.getOpvoerTijdstip()); } public void beeindig(final BiTemporalObject actueleVersie, final Date einddatum) { BiTemporalObject afTeVoerenVersie = zoekActueleVersie(actueleVersie, einddatum, new Date()); if (afTeVoerenVersie == null) { throw new IllegalStateException("Geen oude actuele versie gevonden!"); } beeindigVersie(afTeVoerenVersie, einddatum, new Date()); } public void beeindig(final Date einddatum) { List afTeVoerenVersies = (List) CollectionUtils.select(this.alleHistorischeVersies, new Predicate() { public boolean evaluate(Object object) { BiTemporalObject versie = (BiTemporalObject) object; return versie.isGeldigOp(new Date(), einddatum); } }); CollectionUtils.forAllDo(afTeVoerenVersies, new Closure() { public void execute(Object input) { beeindigVersie((BiTemporalObject) input, einddatum, new Date()); } }); } private void beeindigVersie(BiTemporalObject versie, Date einddatum, Date registratieTijdstip) { BiTemporalObject kopieVersie = versie.kopieer(einddatum, registratieTijdstip); this.alleHistorischeVersies.add(kopieVersie); } private BiTemporalObject zoekActueleVersie(final BiTemporalObject actueleVersie, final Date peildatum, final Date registratieTijdstip) { return (BiTemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() { public boolean evaluate(Object object) { BiTemporalObject versie = (BiTemporalObject) object; return versie.equals(actueleVersie) && versie.isGeldigOp(registratieTijdstip, peildatum); } }); }
De wijzigingen ten opzichte van de BiTemporalProperty uit het vorige deel zijn dat er nu niet één maar meer versies tegelijkertijd geldig kunnen zijn (getActueleVersies) en dat we versies kunnen toevoegen (voegActueleVersieToe) en kunnen wijzigen (wijzigActueleVersie). Bij deze laatste methode we de te wijzigen versie meegeven, zodat deze beëindigd kan worden en de nieuwe versie toegevoegd wordt. Ook kunnen we alle versies van de collection beëindigen (beeindig) of een enkele versie (die dan weer aan de methode meegegeven moet worden).
Een voorbeeld
In dit voorbeeld wordt het voorbeeld uit deel 3 uitgebreid met een manager (zelf ook een werknemer), die meerdere werknemers onder zich heeft.
Manager
public class Manager extends Werknemer { private static final long serialVersionUID = 1L; private BiTemporalCollection werknemers = new BiTemporalCollection(); public Manager() { super(); } public Manager(String naam) { super(naam); } public void voegWerknemerToe(Werknemer werknemer, Date ingangsdatum) { this.werknemers.voegActueleVersieToe(new WerknemerRelatie(werknemer, ingangsdatum)); } public List getTeManagenWerknemers() { List relaties = getWerknemerRelaties(); CollectionUtils.transform(relaties, new Transformer() { public Object transform(Object object) { return ((WerknemerRelatie)object).getWerknemer(); } }) ; return relaties; } public List getTeManagenWerknemers(Date registratieTijdstip, Date peildatum) { List relaties = getWerknemerRelaties(registratieTijdstip, peildatum); CollectionUtils.transform(relaties, new Transformer() { public Object transform(Object object) { return ((WerknemerRelatie)object).getWerknemer(); } }) ; return relaties; } public List getWerknemerRelaties() { return this.werknemers.getActueleVersies(); } public List getWerknemerRelaties(Date registratieTijdstip, Date peildatum) { return this.werknemers.getVersieOp(registratieTijdstip, peildatum); } public void wijzigWerknemer(Werknemer nieuweWerknemer, Werknemer oudeWerknemer, Date wijzigingsdatum) { WerknemerRelatie nieuweRelatie = new WerknemerRelatie(nieuweWerknemer, wijzigingsdatum); WerknemerRelatie oudeRelatie = bepaalWerknemerRelatie(oudeWerknemer); this.werknemers.wijzigActueleVersie(nieuweRelatie, oudeRelatie); } private WerknemerRelatie bepaalWerknemerRelatie(final Werknemer werknemer) { return (WerknemerRelatie)CollectionUtils.find(this.werknemers.getActueleVersies(), new Predicate() { public boolean evaluate(Object obj) { return ((WerknemerRelatie)obj).getWerknemer().equals(werknemer); } }); } public void stopManagenVanWerknemer(Werknemer werknemer, Date einddatum) { WerknemerRelatie relatie = bepaalWerknemerRelatie(werknemer); this.werknemers.beeindig(relatie, einddatum); } public void stopManagen(Date einddatum) { this.werknemers.beeindig(einddatum); } }
Deze klasse representeert de manager. Als een manager een werknemer gaat managen, wordt de werknemer toegevoegd aan de lijst van te managen werknemers (voegWerknemerToe). Als een werknemer wordt vervangen door een andere werknemer wordt de methode wijzigWerknemer aangeroepen, waardoor de relatie met de oude werknemer wordt beëindigd en de nieuwe werknemer wordt begonnen. Als een werknemer uit dienst gaat of naar een andere afdeling, stopt de manager met het managen van deze werknemer (stopManagenVanWerknemer), waardoor de historische relatie van de manager met de werknemer wordt beëindigd. Als de manager stopt met managen (stopManagen), wordt de relatie met alle gemanagede werknemers beëindigd.
De historische relatie van de manager met de werknemer wordt gerepresenteerd door de klasse WerknemerRelatie.
WerknemerRelatie
public class WerknemerRelatie extends BiTemporalObject { private static final long serialVersionUID = 1L; private Werknemer werknemer; protected WerknemerRelatie() { super(); } protected WerknemerRelatie(Werknemer werknemer, Date ingangsdatum) { super(ingangsdatum); this.werknemer= werknemer; } public Werknemer getWerknemer() { return this.werknemer ; } }
De test ziet er als volgt uit:
public void testManager() { Manager jan = new Manager("Jan"); // simuleer de opvoer tijd 1-1-2003 Date aanvangsdatum = DateUtils.maakDate(2003, 1, 1); BiTemporalObject.TEST_OPVOER_TIJDSTIP = aanvangsdatum; Afdeling inkoop = new Afdeling("Inkoop"); jan.setAfdeling(inkoop, aanvangsdatum); Werknemer kees = new Werknemer("Kees"); kees.setAfdeling(inkoop, aanvangsdatum); jan.voegWerknemerToe(kees, aanvangsdatum); Werknemer piet = new Werknemer("Piet"); piet.setAfdeling(inkoop, aanvangsdatum); jan.voegWerknemerToe(piet, aanvangsdatum); assertEquals(2, jan.getTeManagenWerknemers().size()); assertTrue(jan.getTeManagenWerknemers().contains(kees)); assertTrue(jan.getTeManagenWerknemers().contains(piet)); Date wijzigingsdatum = DateUtils.maakDate(2006, 1, 1); BiTemporalObject.TEST_OPVOER_TIJDSTIP = wijzigingsdatum; Werknemer johan = new Werknemer("Johan"); johan.setAfdeling(inkoop, wijzigingsdatum); // Johan vervangt Piet, die uit dienst gaat jan.wijzigWerknemer(johan, piet, wijzigingsdatum); piet.uitDienst(wijzigingsdatum); assertEquals(2, jan.getTeManagenWerknemers().size()); assertTrue(jan.getTeManagenWerknemers().contains(johan)); assertFalse(jan.getTeManagenWerknemers().contains(piet)); // jan managete gisteren piet nog wel Date gisteren = DateUtils.maakDate(2005, 12, 31); assertTrue(jan.getTeManagenWerknemers(new Date(), gisteren).contains(piet)); Date einddatum = DateUtils.maakDate(2009, 1, 1); BiTemporalObject.TEST_OPVOER_TIJDSTIP = wijzigingsdatum; jan.stopManagenVanWerknemer(kees, einddatum); kees.uitDienst(einddatum); assertEquals(1, jan.getTeManagenWerknemers().size()); assertFalse(jan.getTeManagenWerknemers().contains(kees)); jan.stopManagen(einddatum); assertTrue(jan.getTeManagenWerknemers().isEmpty()); }
In deze test maken we een manager Jan aan, die werkt op de afdeling Inkoop op 1 januari 2003. Vanaf deze dag begint hij ook met het managen van twee werknemers Kees en Piet. Vanaf 1 januari 2006 vervangt een nieuwe werknemer Johan Piet, die uit dienst gaat. Op 1 januari 2009 stopt Jan met het managen van al zijn werknemers.
Hibernate mappings
De hibernate mapping van de klasse WerknemerRelatie is analoog aan de mapping van de AfdelingRelatie uit deel 3.
De mapping van de werknemer is uitgebreid met de subclass Manager, die de werknemers als BiTemporalCollection property mapt.
Werknemer.hbm.xml
<hibernate-mapping package="nl.ordina.bitemporal.example" default-access="field"> <class name="Werknemer" table="Werknemer" discriminator-value="Werknemer"> <id name="id" column="id"> <generator class="identity"></generator> </id> <discriminator column="type" /> <version name="versie" /> <!-- properties --> <property name="naam" column="naam" /> <!-- de verwijzing naar de afdelingrelatie --> <component name="afdelingRelatie" class="nl.ordina.temporal.bitemporal.BiTemporalProperty" lazy="false"> <bag name="alleHistorischeVersies" cascade="all, delete-orphan" > <key column="werknemerId" /> <one-to-many class="AfdelingRelatie" /> </bag> </component> <subclass name="Manager" discriminator-value="Manager"> <!-- de verwijzing naar de werknemerrelatie --> <component name="werknemers" class="nl.ordina.temporal.bitemporal.BiTemporalCollection" lazy="false"> <bag name="alleHistorischeVersies" cascade="all, delete-orphan" > <key column="managerId" /> <one-to-many class="WerknemerRelatie" /> </bag> </component> </subclass> </class> </hibernate-mapping>
LET OP: In bovenstaand artikel wordt gesproken over BiTemporalCollection. Dit is niet dezelfde als de BiTemporalCollection van Fowler , wat eigenlijk de BiTemporalProperty uit deel 3 is.
