blog.smart-java.nl
Ordina J-Technologies – Java Blog



Temporale data – deel 3: het bitemporale pattern

By: gnoij, 22 December 2008

In 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 pattern.

BiTemporalObject

public abstract class BiTemporalObject implements Cloneable {
 
    private Date ingangsdatum;
 
    private Date einddatum;
 
    private Date opvoerTijdstip;
 
    private Date afvoerTijdstip;
 
    protected BiTemporalObject(Date ingangsdatum) {
        super();
 
        if (ingangsdatum == null) {
            throw new IllegalArgumentException("Ingangsdatum is null");
        }
 
        this.ingangsdatum = ingangsdatum;
 
        this.opvoerTijdstip = new Date();
    }
 
    public boolean isGeldigOp(Date registratieTijdstip, Date peilDatum) {
        return isGeregistreerdOp(registratieTijdstip) && isGeldigOp(peilDatum);
    }
 
    private boolean isGeregistreerdOp(Date registratieTijdstip) {
        return !isAfgevoerd(registratieTijdstip)
                && (registratieTijdstip.after(this.opvoerTijdstip) || registratieTijdstip.equals(this.opvoerTijdstip));
    }
 
    private boolean isAfgevoerd(Date registratieTijdstip) {
        return this.afvoerTijdstip != null && (registratieTijdstip.after(this.afvoerTijdstip) || registratieTijdstip.equals(this.afvoerTijdstip));
    }
 
    private 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;
    }
 
    protected BiTemporalObject kopieer(Date wijzigingsdatum, Date registratieTijdstip) {
        // maak een kopie met gevulde einddatum
        BiTemporalObject versie = (BiTemporalObject) super.kopieer();
        versie.opvoerTijdstip = registratieTijdstip;
        versie.afvoerTijdstip = null;
        versie.einddatum = wijzigingsdatum;
 
        // voer de oude versie af
        this.afvoerTijdstip = registratieTijdstip;
 
        return versie;
    }
 
    public Date getIngangsdatum() {
        return this.ingangsdatum;
    }
 
    public Date getEinddatum() {
        return this.einddatum;
    }
 
    public Date getOpvoerTijdstip() {
        return this.opvoerTijdstip;
    }
 
    public Date getAfvoerTijdstip() {
        return this.afvoerTijdstip;
    }

In deze klasse vallen (ten opzichte van TemporalObject) een aantal dingen op. Allereerst dat deze klasse clonable is, verder dat een object pas geldig is als de registratietijd binnen de opvoer en afvoer registratie is en dat er een kopieer methode is, die een kopie (clone) van het object maakt met de huidige opvoertijd en einddatum. Het oorspronkelijke object wordt dan afgevoerd door het vullen van de afvoer registratietijd. Bij creatie van een BiTemporalObject wordt automatisch het opvoertijdstip gevuld met de systeemdatum.

BiTemporalProperty

public class BiTemporalProperty {
 
    protected List alleHistorischeVersies = new ArrayList();
 
    public BiTemporalProperty() {
        super();
    }
 
    public BiTemporalObject getActueleVersie() {
        return getVersieOp(new Date(), new Date());
    }
 
    public BiTemporalObject getVersieOp(final Date registratieTijdstip, final Date peilDatum) {
        return (BiTemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                return ((BiTemporalObject) object).isGeldigOp(registratieTijdstip, peilDatum);
            }
        });
    }
 
    public void setActueleVersie(BiTemporalObject actueleVersie) {
        if (actueleVersie == null) {
            throw new IllegalArgumentException("actueleVersie is null");
        }
 
        beeindigVorigeVersie(actueleVersie);
        this.alleHistorischeVersies.add(actueleVersie);
    }
 
    private void beeindigVorigeVersie(final BiTemporalObject actueleVersie) {
        BiTemporalObject afTeVoerenVersie = (BiTemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                BiTemporalObject versie = (BiTemporalObject) object;
                return versie.isGeldigOp(actueleVersie.getOpvoerTijdstip(), actueleVersie.getIngangsdatum());
            }
        });
 
        if (afTeVoerenVersie != null) {
            BiTemporalObject kopieVersie = afTeVoerenVersie.kopieer(actueleVersie.getIngangsdatum(), actueleVersie.getOpvoerTijdstip());
            this.alleHistorischeVersies.add(kopieVersie);
        }
    }
 
    public void beeindig(Date einddatum) {
        BiTemporalObject teBeeindigenVersie = getActueleVersie();
        if (teBeeindigenVersie != null) {
            BiTemporalObject kopieVersie = teBeeindigenVersie.kopieer(einddatum, new Date());
            this.alleHistorischeVersies.add(kopieVersie);
        }
    }
}

Het verschil van de BiTemporalProperty met de TemporalProperty is dat er nu twee tijdstippen worden meegegeven om de juiste versie te bepalen. Naast de peildatum is dat ook het registratietijdstip. Als er een actuele versie wordt toegevoegd of beëindigd, wordt de op dat moment geldende versie gekopieerd en afgevoerd. De gekopieerde versie wordt beëindigd. Hierdoor zal er altijd bij beëindiging of wijziging een extra record ontstaan. Dit wordt verklaard in de concepten voor bitemporale data in deel 1.

Een voorbeeld

We gebruiken hier hetzelfde voorbeeld als uit deel 2.

Werknemer

public class Werknemer {
 
    private BiTemporalProperty afdelingRelatie = new BiTemporalProperty();
 
    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 registratieTijdstip, Date peildatum) {
        AfdelingRelatie relatie = (AfdelingRelatie) this.afdelingRelatie.getVersieOp(registratieTijdstip, peildatum);
        if (relatie != null) {
            return relatie.getAfdeling();
        }
        return null;
    }
 
    public AfdelingRelatie getAfdelingRelatie() {
        return (AfdelingRelatie) this.afdelingRelatie.getActueleVersie();
    }
 
    public AfdelingRelatie getAfdelingRelatie(Date registratieTijdstip, Date peildatum) {
        return (AfdelingRelatie) this.afdelingRelatie.getVersieOp(registratieTijdstip, peildatum);
    }
 
    public void uitDienst(Date datumUitdienst) {
        this.afdelingRelatie.beeindig(datumUitdienst);
    }
}

Ten opzichte van de klasse Werknemer uit deel 2 hebben de methoden getAfdeling en getAfdelingRelatie een extra parameter registratieTijdstip.

AfdelingRelatie

public class AfdelingRelatie extends BiTemporalObject {
 
    private Afdeling afdeling;
 
    protected AfdelingRelatie(Afdeling afdeling, Date ingangsdatum) {
        super(ingangsdatum);
 
        this.afdeling = afdeling;
    }
 
    public Afdeling getAfdeling() {
        return this.afdeling;
    }
}

Deze klasse AfdelingRelatie is afgeleid van de klasse BiTemporalObject. Verder is deze analoog aan de klasse uit deel 2.

De klasse Afdeling is analoog aan de klasse Afdeling uit deel 2.

De test ziet er als volgt uit:

    public void testWerknemer() {
        Werknemer werknemer = new Werknemer("Kees");
 
        // simuleer de opvoer tijd 1-1-2003
        BiTemporalObject.TEST_OPVOER_TIJDSTIP = DateUtils.maakDate(2003, 1, 1);
 
        Afdeling inkoop = new Afdeling("Inkoop");
        werknemer.setAfdeling(inkoop, DateUtils.maakDate(2003, 1, 1));
 
        // simuleer de opvoer tijd 1-2-2006
        BiTemporalObject.TEST_OPVOER_TIJDSTIP = DateUtils.maakDate(2006, 2, 1);
 
        Afdeling verkoop = new Afdeling("Verkoop");
        werknemer.setAfdeling(verkoop, DateUtils.maakDate(2006, 1, 1));
 
        assertEquals("Kees", werknemer.getNaam());
        assertEquals("Verkoop", werknemer.getAfdeling().getNaam());
        assertEquals("Verkoop", werknemer.getAfdeling(DateUtils.vandaag(), DateUtils.maakDate(2006, 1, 15)).getNaam());
        assertEquals("Inkoop", werknemer.getAfdeling(DateUtils.maakDate(2006, 1, 15), DateUtils.maakDate(2006, 1, 15)).getNaam());
 
        // simuleer de opvoer tijd 1-2-2008
        BiTemporalObject.TEST_OPVOER_TIJDSTIP = DateUtils.maakDate(2008, 2, 1);
 
        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. Dit wordt op dezelfde datum geregistreerd. Vanaf 1 januari 2006 werkt Kees op de afdeling Verkoop, wat pas op 1 februari wordt geregistreerd en vanaf 1 januari 2008 is Kees uit dienst. Dit wordt een maand later pas geregistreerd.

Omdat de registratietijd automatisch wordt bepaald heb ik (uitsluitend voor testdoeleinden) de klasse BiTemporalObject uitgebreid met een public static member TEST_OPVOER_TIJDSTIP, om de registratietijd uit het voorbeeld te kunnen simuleren. Hierdoor verandert de constructor van BiTemporalObject in

    protected BiTemporalObject(Date ingangsdatum) {
        super();
 
        if (ingangsdatum == null) {
            throw new IllegalArgumentException("Ingangsdatum is null");
        }
 
        this.ingangsdatum = ingangsdatum;
 
        // voor testdoeleinden
        if (TEST_OPVOER_TIJDSTIP == null) {
            this.opvoerTijdstip = new Date();
        } else {
            this.opvoerTijdstip = TEST_OPVOER_TIJDSTIP;
        }
    }

Hibernate mappings

De hibernate mapping van de klasse Werknemer en Afdeling zijn analoog aan die van de klasse Werknemer uit deel 2.
De mapping van de klasse AfdelingRelatie is uitgebreid met de opvoer- en afvoer registratietijd.

AfdelingRelatie

<hibernate-mapping package="nl.ordina.bitemporal.example"
	default-access="field">
	<class name="AfdelingRelatie" table="AfdelingRelatie">
		<id name="id" column="id">
			<generator class="identity"></generator>
		</id>
		<version name="versie" />
 
		<!-- temporale informatie -->
		<property name="ingangsdatum" type="date" />
		<property name="einddatum" type="date" />
		<property name="opvoerTijdstip" type="date" />
		<property name="afvoerTijdstip" type="date" />
 
		<!-- de verwijzing naar de afdeling -->
		<many-to-one name="afdeling" lazy="false" class="Afdeling"
			column="afdelingId" cascade="none" />
	</class>
</hibernate-mapping>

Het volgende deel zal het bitemporale pattern uitbreiden met historische collections.

Eén reactie op “Temporale data – deel 3: het bitemporale pattern”

  1. Ordina J-Technologies » Blog archief » Temporale data – deel 4: bitemporale collections zegt:

    [...] 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 [...]

Laat een reactie achter