Relationen implementieren mit Hibernate

Mit hibernate verwende ich schon seit vielen Jahren ein verlässliches Framework, um Objektnetze in relationalen Datenbanken persistieren zu können. Auch wenn sich dieses Framework viel Mühe gibt, diese semantische Lüscke auszufüllen, gibt es doch immer wieder Momente, in denen die Unterschiede zwischen Objektnetzen einerseits sowie Datentupeln und Relationen andererseits aufblitzen. Dann kann es schon manchmal passieren, dass man aufgrund einer Wartungsaktivität eine geringfügige Änderung am Programmcode vornimmt und plötzlich treten Fehler in der Produktion (immer nur dort, nie in den Testumgebungen auf), die zu dauerhaftem Ärger führen können.

Eine dieser Stellen mit Problempotenzial ist die Abbildung der in der Datenbank vorhandenen Relationen im Objektmodell. Werden diese Relationen bidirektional implementiert, wird auch durch die Hibernate Dokumentation darauf hingewiesen, dass man beide Seiten der Relation synchron pflegen muss.

Betrachten wir einfach eine One-To-One Relation zwischene einer Master und einer Detail Instanz. Diese ist auf Seite des Masters schnell implementiert und kann dann einfach genutzt werden.

	private Detail detail;

	public Detail getDetail() {
	    return detail;
	}

	public void setDetail(Detail detail) {
    	this.detail = detail;
	}

In allen Service Implementierungen müssen dann immer korrekt beide Seiten der Beziehung manipuliert werden.

	Master master = ...;
	Detail detail = ...;
	if (master.getDetail() != null)
		master.getDetail().setMaster(null);
	master.setDetail(detail);
	if (detail != null)
		detail.setMaster(master);

Ansonsten tritt dort eines dieser schwer nachvollziehbaren Probleme auf: es wird nur eine Seite der Beziehung auf den neuen Wert geändert, die andere bleibt unverändert. Wird das geänderte Objekt dann in die Datenbank geschrieben, wurden damit implizit beide Seiten der Beziehung verändert. Immerhin ist dort der Fremdschlüssel als zusätzliche Information nur in einer der Tabellen enthalten.

Wird die Relation dann später aus der Datenbank zurückgelesen, werden die verwendeten Referenzen auf beiden Seiten der Relation mit den korrekten Werten belegt. Alles ist gut.

Manchmal aber ist es nicht gut, dann ist es schlechter… Ist nämlich eine der beteiligten Instanzen noch im Session- oder 2nd-Level-Cache enthalten, wird hier gegebenenfalls der alte, nicht veränderte Zustand aus dem Cache gelesen. In diesem Fall ist das im Speicher nur vorliegende Objektnetze fehlerhaft.

Hat man Glück und war sehr gewissenhaft, wird die Konsistenz der Daten zur Laufzeit verifiziert. Dann erscheinen in seltenen Fällen Fehlermeldungen, die man eigentlich nicht hat sehen wollen.

    Master master = ...;
    if (master.getDetail().getMaster() != master)
        throw new InconsistencyException();
    if (!master.getDetail().getMaster().equals(master))
        throw new InconsistencyException();

Meistens aber erkennt der Programmcode das Vorliegen solcher Defekte in den Objektnetzen gar nicht. Fehlerhafte Daten werden weiter verfälscht und die Todesspirale für die verwalteten Daten setzt ein.

Lieder gibt es keine Sicherheit, dass in wirklich 100% aller Fälle die Relation durch den Service korrekt gepflegt wird. Meistens sind die Integritätsprüfungen gar nicht oder nur an unpassenden Stellen realisiert.

Daher implementiere ich den oben gezeigten Code für die Pflege dieser Master/Detail Relation in der Regel innerhalb der Domänenobjekte. Dann gibt es ein Problem weniger, über das ich mir Sorgen machen könnte.

Transparente Proxies

In diesem Fall ist das Potenzial für einen weiteren Fehler gering (eigentlich nicht vorhanden). Wird aber anstelle der hier betrachteten One-To-One eine One-To-Many oder Many-To-One Relation genutzt, kann es auf der Many-Seite Probleme geben.

Ursache ist hier einfach das Hibernate Feature, dass für Objekte, die nicht unmittelbar aus der Datenbank gelesen werden konnten, erst einmal transparente Proxy Instanzen erzeugt werden. Diese bleiben aber für den Programmierer unsichtbar, selbst wenn zu einem späteren Zeitpunkt die konkrete Instanz aus der Datenbank gelesen wird. Dann wird, um die Objektidentität weiter aufrecht zu erhalten, für die betroffenen Instanzen weiter das transparente Proxy herausgegeben.

Das Problem beginnt aber, wenn man innerhalb der Domänenklassen sich selbst mit self referenziert. Dann nutzt man wirklich das materialisierte Objekt und nicht das transparente Proxy. Prüft der Programmcode dann, ob eine Instant bereits in einer Collection enthalten ist, liefert dieses Collection.contains() für gleiche Objekte gegebenenfalls fehlerhafterweise ein false. Werden dann das materialisierte Objekt und das transparente Proxy in die Collection eingefügt, gibt es bei der Synchronisation mit der Datenbank verständlicherweise aufgrund der Inkonsistenz einen Fehler.

Daher reiche ich in der Regel die Referenz auf das “lokale” Objekt in das Domänenobjekt hinein. Wenn ich dieses proxiedSelf in den nachfolgenden Schritten verwenden, ist die Relation in allen Fällen korrekt gepflegt.

	public static void assignDetail(Master proxiedSelf, Detail detail,
			User changedBy, Date changedAt) {
		if (proxiedSelf.detail == detail)
			return;

		// Verbindung zum alten Detail lösen
		if (proxiedSelf.detail != null)
			Detail.assignMaster(proxiedSelf.detail, null, changedBy, changedAt);

		if ((proxiedSelf.detail = detail) != null)
			Detail.assignMaster(detail, proxiedSelf, changedBy, changedAt);

		proxiedSelf.changedAt = changedAt;
		proxiedSelf.changedBy = changedBy;
	}

Diese Methode (ist aus guten Grund) als statische Methode implementiert, da so vermieden wird aus Versehen auf ein ungültiges self zuzugreifen. Zusätzlich wird hier auch die bei uns übliche Belegschreibung auf beiden Seiten der Relation vorgenommen, damit an den Daten immer erkennbar ist welcher Anwender zu welchem Zeitpunkt diese Datenkonstellation erzeugt hat.

Da diese Information oft aber auch schon im Objekt korrekt gepflegt vorliegt und ich tatsächlich gerne Tastenanschläge einspare, gibt es auch eine zusätzliche Convenience Methode, die einfach die bereits vorliegenden Werte für die Belegschreibung nutzt.

	public static void assignDetail(Master proxiedSelf, Detail detail) {
		Master.assignDetail(proxiedSelf, detail, proxiedSelf.changedBy,
				proxiedSelf.changedAt);
	}

Eclipse Template

Der hier vorliegende Programmcode muss aber nicht für jede Verwendung neu eingegeben werden. Stattdessen liegt dieser als Code-Template für die Eclipse IDE zur Verfügung. Ich habe ihn unter dem Stichwort OneToOne abgelegt und muss nach dem Aufruf mit Command-Space nur noch Klassen und Rollennamen ergänzen.

Hier das von mir verwendete OneToOne Template:

	/**
	 * Ermittelt die aktuelle {@link ${target_class}} Instanz der "${target_role}" Relation.
	 * 
	 * @return eine Referenz auf eine {@link ${target_class}} Instanz.
	 */
	public ${target_class} get${target_role}() {
		return ${role};
	}

	/**
	 * Pflegen einer One-To-One Relation "${target_role}", die auf {@link ${target_class}}
	 * Instanzen verweist.
	 * 
	 * Da diese Objektbeziehung auf Datenbankebene einer Relation entspricht,
	 * müssen beide Seiten dieser Relation im Objektmodell gepflegt werden, um
	 * Inkonsistenzen im Session- und 2nd-Level-Cache zu vermeiden. Da optional
	 * durch Hibernate Proxy Instanzen für die {@link ${class}} und {@link ${target_class}}
	 * 
	 * Teilnehmer der Relation genutzt werden, darf hier nicht einfach
	 * {@code self} genutzt werden um die {@code ${class}} Instanz auf dieser
	 * Beziehungsseite zu adressieren. Stattdessen muss die hier lokale Instanz
	 * über die hineingereichte Instanz "{@code proxiedSelf}" angesprochen
	 * werden.
	 * 
	 * Als Verursacher und Zeitpunkt der Änderung werden die in der
	 * {@link ${class}} Instanz vorliegenden " {@code changedBy}" und "
	 * {@code changedAt}" Werte genutzt.
	 * 
	 * @param proxiedSelf
	 *            die hier genutzte lokale {@link ${class}} Instanz.
	 * @param ${role}
	 *            die neu zu verwendende {@link ${target_class}} Instanz.
	 */
	public static void assign${target_role}(${class} proxiedSelf, ${target_class} ${role}) {
		${class}.assign${target_role}(proxiedSelf, ${role}, proxiedSelf.changedBy,
				proxiedSelf.changedAt);
	}

	/**
	 * Pflegen einer One-To-One Relation "${target_role}", die auf {@link ${target_class}}
	 * Instanzen verweist.
	 * 
	 * Da diese Objektbeziehung auf Datenbankebene einer Relation entspricht,
	 * müssen beide Seiten dieser Relation im Objektmodell gepflegt werden, um
	 * Inkonsistenzen im Session- und 2nd-Level-Cache zu vermeiden. Da optional
	 * durch Hibernate Proxy Instanzen für die {@link ${class}} und {@link ${target_class}}
	 * 
	 * Teilnehmer der Relation genutzt werden, darf hier nicht einfach
	 * {@code self} genutzt werden um die {@code ${class}} Instanz auf dieser
	 * Beziehungsseite zu adressieren. Stattdessen muss die hier lokale Instanz
	 * über die hineingereichte Instanz "{@code proxiedSelf}" angesprochen
	 * werden.
	 * 
	 * @param proxiedSelf
	 *            die hier genutzte lokale {@link ${class}} Instanz.
	 * @param ${role}
	 *            die neu zu verwendende {@link ${target_class}} Instanz.
	 * @param changedBy
	 *            eine {@link User} Instanz, die den jweils aktuell handelnden
	 *            Aktor bezeichnet
	 * @param changedAt
	 *            der aktuelle Zeitpunkt, zu dem die Relation gepflegt wird.
	 */
	public static void assign${target_role}(${class} proxiedSelf, ${target_class} ${role},
			User changedBy, Date changedAt) {
		if (proxiedSelf.${role} == ${role})
			return;

		// Verbindung zum alten ${target_class} lösen
		if (proxiedSelf.${role} != null)
			${target_class}.assign${reverse_role}(proxiedSelf.${role}, null, changedBy, changedAt);

		if ((proxiedSelf.${role} = ${role}) != null)
			${target_class}.assign${reverse_role}(${role}, proxiedSelf, changedBy, changedAt);

		proxiedSelf.changedAt = changedAt;
		proxiedSelf.changedBy = changedBy;
	}

Weitere Artikel: