Home ORDIX AG             Dienstleistung             Trainingsshop    Kunden / Referenzen Aktuelles    Kontakt
Home  Pfeil  ORDIX News  Pfeil  2/2006  Pfeil  Java/XML
suche: 
Der Artikel richtet sich an Softwarearchitekten und Java-Entwickler, die sich mit Vererbung und Hibernate beschäftigen.

Glossar

Hibernate
Framework zur Speicherung von Daten. Als Backend-System dient eine Datenbank (siehe ORDIX News 04/2005).

O/R-Mapper
Object/Relational Mapper werden eingesetzt, um in objektorientierten Programmiersprachen Objekte auf (Datenbank-)Relationen zu mappen. Mit ihnen ist es möglich, diese Objekte in einer relationalen Datenbank abzuspeichern und wieder daraus zu erstellen. Dies geschieht weitestgehend automatisch, so dass sich der Entwickler nicht um die eigentliche Datenbankschnittstelle kümmern muss.
null
Beschreibt eine Datenbank Spalte die "nichts", also den Wert "null" annehmen kann. Nicht zu verwechseln mit dem numerischen Wert Null (0).
not-null
Gegenteil von null, d. h. es muss immer ein konkreter Wert abgespeichert werden.
Constraint
Beschreibt Bedingungen für den Inhalt von Tabellenspalten. Wird typischerweise verwendet, um eine Prüfung auf "not-null" oder eine korrekte Fremdschlüsselbeziehung durchzuführen. Da die Bedingungen in der Datenbank formuliert sind, verhindern sie das Einpflegen "ungültiger" Daten.
Normalisierung
Vermeidet das mehrfache Speichern von konkreten Datensätzen. Anstatt z. B. einen Ortsnamen mehrfach zu speichern, wird dieser nur einmalig unter einem Schlüssel abgelegt.
hbm.xml Dateien
Hibernate spezifische Konfiguration, die die Zuordnung zwischen Datenbanktabellen und Javaobjekten beschreibt.

Wenn der Vater mit dem Sohne...

Vererbung mit Hibernate

Dass Hibernate die Persistierung von Java-Objekten in einer relationalen Datenbank wie im (Winter-)Schlaf beherrscht, zeigte der einführende Artikel in der ORDIX News 4/2005. Dass Hibernate aber nicht nur einfache Objekte, sondern auch ganze Objekthierarchien abbilden kann, zeigen wir in diesem Artikel.

Die Klassen innerhalb einer objektorientierten Programmiersprache sind nicht nur die Summe ihrer Attribute. Die Objekte stehen auch in Beziehung zueinander, so dass es Basisklassen und davon abgeleitete Subklassen gibt.

Und so ist eine Klasse nicht nur durch ihre eigenen Attribute, sondern auch durch die der Elternklasse definiert. Hibernate unterstützt als vollwertiger O/R-Mapper natürlich auch bei der Abbildung dieser Beziehungen. Die drei folgenden Ansätze werden von Hibernate zur Verfügung gestellt.

Vererbungshierarchie
Abb. 1: Beispiel einer einfachen Vererbungshierarchie.

Für welchen Weg man sich entscheidet, hängt davon ab, wie komplex die abzubildenden Hierarchien sind. Des Weiteren bringen die verschiedenen Vorgehensweisen auch gewisse Einschränkungen mit sich, die man bei seiner Entscheidung in Betracht ziehen muss.

Für die weitere Vorstellung sei die in Abbildung 1 gezeigte Hierarchie von Zahlungsarten gegeben. Zunächst gibt es eine Basisklasse Zahlung (siehe Abbildung 2), die Zahlungen im Allgemeinen beschreibt.

public class Zahlung {
	protected int id;
	protected int betrag;

	protected int getId ( ) { return id; }
	protected void setId (int value) { id=value; }

	protected int getBetrag ( ) { return betrag; }
	protected void setBetrag (int value) { betrag=value; }
}
Abb. 2: Die Basisklasse Zahlung.

import Zahlung;
public class KreditkartenZahlung extends Zahlung {
	protected String kartennummer;
	protected String getKartennummer ( ) { return kartennummer; }
	protected void setKartennummer ( String value  ) {
		kartennummer = value; }
}
Abb. 3: Die Subklasse KreditkartenZahlung.

Da ein Betrag in allen Zahlungsformen vorhanden ist, ist das Attribut Betrag innerhalb der Basisklasse zu finden. Ferner gibt es noch zwei Subklassen KreditkartenZahlung (siehe Abbildung 3) und BarZahlung (siehe Abbildung 4). In einer objektorientierten Welt erscheint dies trivial. In der Welt relationaler Datenbanken ist es grundsätzlich nicht vorgesehen.

import Zahlung;
public class BarZahlung extends Zahlung {
	protected String einzahler;
	protected String getEinzahler ( ) { return einzahler; }
	protected void setEinzahler ( String value ) { einzahler = value; }
}
Abb. 4: Die Subklasse BarZahlung.

Jeder auf seine Art

In diesem Zusammenhang sind daher drei beispielhafte Szenarien interessant:


Wichtig ist dabei das polymorphe Verhalten einer Zahlung, wenn wir auf sie zugreifen. D. h. wenn eine Zahlung eine Kreditkartenzahlung ist, möchten wir auch direkt den Zugriff auf die Kreditkartenart haben, während bei einer Barzahlung aufgrund des Geldwäschegesetzes ab einer gewissen Höhe der Einzahler von Bedeutung ist.

In den oben beschriebenen Szenarien bedeutet das, dass beim Erzeugen der Verknüpfung, der Liste und der Ergebnismenge polymorphe Objekte verwendet werden. D. h. es handelt sich dabei zwar grundsätzlich immer um Zahlungen, aber entsprechend der noch zu beschreibenden Vererbungsdefinition legt Hibernate Instanzen der entsprechenden Subklassen an.

In einem Auftrag kann also hinter der Zahlung eine Kreditkarten- oder eine Barzahlung stecken. In der Liste von Zahlungen einer Rechnung und in der Ergebnismenge der Suche können dann sogar Objekte aller möglichen Ausprägungen kombiniert sein: Es finden sich in der Liste bzw. in der Ergebnismenge Objekte vom Typ KreditkartenZahlung genauso wieder wie Objekte vom Typ BarZahlung.

Eine für Alle

Die einfachste Art, eine Vererbungshierarchie abzubilden, ist der Ansatz der "Tabelle pro Klassenhierarchie". Hierbei werden alle Attribute (gemeinsame, wie individuelle) innerhalb der Hierarchie in einer einzigen Tabelle gespeichert.

Wie Abbildung 5 zeigt, erfolgt die Unterscheidung der Subklassen an Hand einer Discriminator-Spalte (engl. "Unterscheider"). Die Subclass-Elemente definieren dann den für sie spezifischen Wert des Discriminators.

<class name="Zahlung" table="ZAHLUNG">
	<id name="id" type="long" column="ZAHLUNGS_ID">
		<generator class="native"/>
	</id> 
	<discriminator column="ZAHLUNGS_TYP" type="string"/> 
	<property name="betrag" column="BETRAG"/> 
	... 
	<subclass name="KreditkartenZahlung" 
		discriminator-value="KREDIT">
		<property name="kartennummer" column="KARTENNUMMER"/>
		... 
	</subclass>
	<subclass name="BarZahlung" discriminator-value="BAR">
 	... 
	</subclass> 
</class>
Abb. 5: Auszug aus der Zahlung.hbm.xml bei dem gewählten Ansatz "Tabelle pro Klassenhierarchie".

Auch wenn dies wenig administrativen Aufwand innerhalb der Datenbank bedeutet, sollte man doch einiges beachten. So steht dieses Modell im Gegensatz zur Normalisierung, welche in relationalen Datenbanken angestrebt wird.

Statt dessen werden hierbei unter Umständen sogar bewusst Daten redundant gehalten. Des Weiteren erzwingt dieser Ansatz, dass alle Attribute der Subklassen "nullable" sind.

Eine Prüfung der Datenkonsistenz für die Pflichtfelder durch einen entsprechenden "not null"-Constraint auf Datenbankebene ist also nicht möglich. Ein Vorteil ist die Tatsache, dass beim Einlesen von Daten aus der Persistenzschicht keine Tabellen miteinander verknüpft werden müssen.

Fair geteilt

Auch wenn der zuvor gezeigte Ansatz sehr einfach zu implementieren ist, hat er doch einige Nachteile. Insbesondere die Verletzung des Normalisierungsgrundsatzes ist ein gutes Gegenargument.

Um dem Anspruch einer korrekten Normalisierung auf Datenbankseite gerecht zu werden, kann Hibernate auch individuelle Tabellen pro Subklasse einsetzen. Das Datenbankmodell entspricht dabei dann den javaseitigen Klassen.

Gemeinsame Attribute der Vaterklasse liegen in der ihr zugeordneten Tabelle, die individuellen Eigenschaften der Subklassen liegen in jeweils eigenen Tabellen.

Es werden insgesamt <Anzahl-Subklassen+1> Tabellen benötigt. Bei der Instanziierung lädt Hibernate dann die Daten aus der gemeinsamen wie auch aus der jeweils individuellen Tabelle.

Abbildung 6 zeigt ein solches Beispiel. Innerhalb der "joined-subclass"-Elemente gibt es jeweils einen Verweis auf die Tabellen der Subklassen. Das Key-Element der Subklassen verweist jeweils auf die Spalte in der die Vaterklasse ihre ID ablegt.

<class name="Zahlung" table="ZAHLUNG">
	<id name="id" type="long" column="ZAHLUNGS_ID">
		<generator class="native"/>
	</id>
	<property name="betrag" column="BETRAG"/>
	... 
	<joined-subclass name="KreditkartenZahlung" 
	   table="KREDITKARTENZAHLUNG">
		<key column="ZAHLUNGS_ID"/> 
		<property name="kartennummer" column="KARTENNUMMER"/> 
		... 
	</joined-subclass> 
	<joined-subclass name="BarZahlung" table="BARZAHLUNG"> 
		<key column="ZAHLUNGS_ID"/> 
		... 
	</joined-subclass> 
</class>
Abb. 6: Auszug aus der Zahlung.hbm.xml bei dem gewählten Ansatz "Tabelle pro Subklasse".

Innerhalb der Datenbank haben die Einträge für Subklassen jeweils immer den gleichen Primärschlüssel wie die Einträge für die Vaterklasse. Es handelt sich um 1:1-Relationen.

Jeder bekommt Alles

Der Ansatz "Tabelle pro konkreter Klasse" stellt die Umkehr des eingangs vorgestellten Ansatzes "Tabelle pro Klassenhierarchie" dar. Hierbei werden so viele Tabellen benötigt, wie es konkrete (Sub-)Klassen gibt (siehe Abbildung 7).

<name="Zahlung"> 
	<name="id" type="long" column="ZAHLUNGS_ID"> 
		<generator class="sequence"/> 
	</id> 
	<property name="betrag" column="BETRAG"/> 
	... 
	<union-subclass name="KREDITKARTENZAHLUNG" 
	table="KREDITKARTENZAHLUNG"> 
		<property name="kartennummer" column="KARTENNUMMER"/> 
		... 
	<union-subclass> 
	<name="BarZahlung" table="BARZAHLUNG">
	... 
	</union-subclass> 
</class>
Abb. 7: Auszug aus der Zahlung.hbm.xml bei dem gewählten Ansatz "Tabelle pro konkreter Klasse".

Die vererbten Attribute werden jeweils mit in der Tabelle der Subklasse gespeichert. Wenn die Vaterklasse nicht abstrakt ist, und es folglich von ihr auch eigenständige Instanzen geben kann, wird für diese ebenfalls eine Tabelle benötigt.

Ebenso wie der erste Ansatz erzeugt auch dieser Ansatz Redundanzen auf der Datenbankseite und widerspricht somit dem Normalisierungsgedanken. Eine Einschränkung ist, dass die Spaltennamen der gemeinsamen Attribute in allen Subklassentabellen identisch sein müssen. Ein weiterer Nachteil dieses Ansatzes ist, dass Hibernate in diesem Fall nur eingeschränkte polymorphe Abfragen unterstützt.

Wer sich nach dem Sinn eines solchen Mappings fragt, sollte immer an die unterschiedlichen Einsatzmöglichkeiten von Hibernate denken. Die meisten Projekte fangen nicht auf der Grünen Wiese an. Und wer noch nie - aus welchen Gründen auch immer, insbesondere n seinem vorherigen Leben als nicht-objektorientierer Entwickler - beim Datenbankdesign die Normalisierungsregeln verletzt hat, der werfe den ersten Stein.

Den weiteren Möglichkeiten von Hibernate werden wir uns in einer der kommenden Ausgaben der ORDIX News widmen. Wer seine Neugier eher stillen möchte, wird im ORDIX Seminarprogramm fündig. Dort gibt es ab sofort auch ein Seminar zum Thema Hibernate.

Michael Heß (info@ordix.de).