Java für Fortgeschrittene Proseminar im Sommersemester 2009

Werbung
Java für Fortgeschrittene
Proseminar im Sommersemester 2009
Hibernate - Von Datenaustausch zu Datenbanken:
RDBMS und Objektpersistenz
Stephan Weiß
Technische Universität München
14.07.2009
Zusammenfassung
Diese Arbeit stellt eine Einführung in die Arbeit mit dem Persistenzframework Hibernate in
Verbindung mit der Programmiersprache Java dar. Sie zeigt einige Möglichkeiten auf mit
Hibernate Code zu sparen und Hibernate effizient zu nutzen.
1 Einführung
Bei fast allen Entwicklungsprojekten ist eine relationale Datenbank fester Bestandteil
des jeweiligen Systems: Logistik-Systeme, CRM-Systeme, Software für
Versicherungen und Banken, etc. – alle dieses Systeme bauen darauf auf Daten
persistent speichern zu können. Bei objektorientierten IT-Anwendungen stellt sich
nun die Herausforderung objektorientierte Strukturen (in Form von Klassen und
Beziehungen unter ihnen) auf eine relationale Struktur in die Datenbank abzubilden.
Hierfür werden sogenannte O/R-Mapper verwendet (Object-relational mapper), die
quasi als Vermittler zwischen der objektorientierten und relationalen Welt dienen. In
Java ist Hibernate, der wohl am weitest verbreiteste O/R-Mapper. Hibernate (engl.
„Winterschlaf halten“) ist ein Open-Source-Persistenz Framework, weit verbreitet und
spart Quellcode ein (um nur einige Vorteile zu benennen). Somit tritt der Code, der
sich mit den technischen Dingen des O/R-Mapping beschäftigt (z.B. Connections
verwalten) in den Hintergrund und der Entwickler kann sich mehr mit der
eigentlichen Anwendungsentwicklung beschäftigen. Zunächst wird in dieser Arbeit
ein Gesamtüberblick über Hibernate gegeben, bevor gezeigt wird wie es im
Speziellen genutzt wird.
1
2 Überblick
Hibernate ist also die Schnittstelle zwischen dem Java-Code des Entwicklers und der
relationalen Datenbank. Hibernate unterstützt hierbei (fast) alle relationalen
Datenbanken. Damit Hibernate Daten zwischen der Datenbank und den Objekten
austauschen kann, muss der Zusammenhang zwischen den Spalten der Tabellen und
den Attributen der Klassen hergestellt und Hibernate mitgeteilt werden. Dieses
Mapping kann von Annotations oder XML-Dateien übernommen werden. In dieser
Ausarbeitung wird mit den XML-Dateien gemappt, da so keine
Datenbankeinstellungen mit dem Java-Code vermischt werden. Über diese MappingDateien kann Hibernate dann die entsprechenden gemappten Objekte mit den Daten
aus der Datenbank füllen oder die Daten des Objekts in der Datenbank speichern. Um
Abfragen an die Datenbank abzusetzen, Löschungen oder Änderungen an den Daten
der Datenbank vorzunehmen, muss ein Session-Objekt erzeugt werden, das die
zentrale Schnittstelle zu Hibernate darstellt. Bei schreibenden Zugriffen muss
zusätzlich noch ein Transaction-Objekt mit Hilfe der Session erzeugt werden. Die
Session wird über eine SessionFactory geladen, die ein Pool von Sessions bereitstellt.
Die SessionFactory wird wiederum über ein Configuration-Objekt erzeugt, das die
Konfigurationen(also entweder die der XML-Dateien oder der Annotations) einliest.
Um Abfragen abzusetzen, wird über createQuery an der Session ein Query-Objekt
erzeugt, das durch die Sprache HQL (Hibernate Query Language) Datenbankabfragen
absetzen kann.
2
•
Configuration:
Diese
Klasse
kümmert
sich
um
alle
Konfigurationsmechanismen. Entweder kann über configure() die allgemeine
Konfigurationsdatei eingelesen werden oder im Code explizit die
Einstellungen gesetzt werden.
•
SessionFactory: Dieses Objekt muss über die Configuration-Klasse erzeugt
werden. Diese Klasse erzeugt Sessions. Üblicherweise gibt es nur ein Objekt
dieser Art pro Projekt.
•
Session: Dieses Objekt wird von der SessionFactory erzeugt. Dieses
Interface stellt nun die zentrale Schnittstelle zwischen dem eigenen Code
und Hibernate dar. Es stellt die Verbindung zur Datenbank her und stellt
Methoden für das Löschen, Lesen und Schreiben von gemappten Klassen in
die Datenbank zur Verfügung. Außerdem hält es einen Cache bereit, so dass
sich wiederholende Abfragen schneller ausgeführt werden können.
•
Transaction: Wenn Daten verändert werden sollen, muss zwangsläufig über
die Session ein Transaction-Objekt angelegt werden. Außerdem unterstützt
es die Transaktionsmethoden commit(alle Schreibzugriffe, die an die
Transaction gehangen wurden, werden an die Datenbank gesendet) und
rollback(alle Aktionen werden verworfen).
3
Konfiguration
Hibernate muss für das O/R-Mapping konfiguriert werden, so dass es weiß, welche
Klasse auf welche Tabelle abgebildet wird, welche Datenbank genutzt werden soll,
etc.. Für Hibernate gibt es etliche Konfigurationsmöglichkeiten: Annotations oder
XML-Mapping?, welche XML-Dateien verwende ich für was?,... Außerdem besteht
die Möglichkeit die Konfigurationen im Code vorzunehmen. Wichtig ist hier zu
wissen, dass die Einstellungen, die im Code vorgenommen werden, die Einstellungen
aus der XML-Datei überschreiben. Wir wollen uns aber zunächst daran halten, dass
wir pro Projekt eine allgemeine Konfigurationsdatei definieren, die auch allgemeine
Datenbankeinstellungen wie die URL, den Treiber oder den User angibt. Zusätzlich
definieren wir pro Klasse eine XML-Datei, die das eigentliche Mapping übernimmt.
In diesen Konfigurationsdateien wird dann festgelegt, welche Tabellen in
Zusammenhang mit dieser Klasse stehen und welche Attribute zu welcher Spalte
gehören.
In dieser Arbeit wird nun zunächst auf die Mapping-Dateien eingegangen und danach
gezeigt wie Hibernate im Code in Form von Abfragen, etc. genutzt werden kann.
3 Allgemeine Konfiguration
Im Folgenden wird ein einfaches Beispiel zum ersten Verständnis von Hibernate
betrachtet. Es besteht aus den Klassen Wald und Baum. Der Wald besteht dabei aus
mindestens zwei Bäumen.
In der Datenbank wird diese Beziehung dann über den Fremdschlüssel Wald_id in der
Tabelle Baum dargestellt:
4
Der Primärschlüssel id (für eindeutigen Zugriff notwendig) in der Tabelle Wald und
Baum ist eindeutig pro Tabelle und kennzeichnet jede Zeile der Tabelle. Wald_id ist
der Fremdschlüssel und stellt so die Verbindung zwischen Baum und Wald her,
indem in Wald_id die eindeutige id des Waldes steht. Jede Tabelle hat eine
zugehörige Entitätsklasse(=Klasse, deren Daten in der Datenbank gespeichert werden)
im Code. Das muss nicht immer so sein wie wir später noch sehen werden. Zunächst
wird die Entitätsklasse Baum erstellt. Sie besteht nur aus einer eindeutigen id und
dem String name. Außerdem weiß jedes Objekt Baum über das Attribut wald zu
welchem Wald-Objekt es gehört.
package de.wald.entity;
public class Baum {
private int id;
private String name;
private Wald wald;
public Wald getWald() {
return wald;
}
public void setWald(Wald wald) {
this.wald = wald;
}
protected Baum(String name){
super();
this.name=name;
}
Baum(){
super();
}
public int getId() {
5
return id;
}
private void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Die Klasse ist nur ein Datencontainer (das ist die Hauptfunktion von Entitätsklassen)
und besteht daher nur aus getter und setter-Methoden. Mit der Standardeinstellung
verwendet Hibernate genau diese getter und setter zur Befüllung der Objekte aus der
Datenbank (bei Abfragen) bzw. der Befüllung der Tabellen in der Datenbank aus den
Daten der Objekte (bei Schreibzugriffen). Dabei kann die Methode setId(int id)
private sein und Hibernate befüllt das Attribut id trotzdem. Der Grund hierfür ist, dass
Hibernate über Reflections auf die Methoden zugreift. Deshalb ist auch wichtig, dass
die Namenskonvention bei den Methoden nicht verletzt wird: getAttributname(),
setAttributname(Typ t). Außerdem sollte natürlich der SecurityManager den Zugriff
auf private Methoden über Reflection erlauben. Vorsicht ist nur beim Konstruktor
geboten. Seit Hibernate 3 arbeitet das Framework standardmäßig mit Proxies für das
Laden der Entitätsklassen. Daher ist es wichtig, dass die Entitätsklasse mindestens
einen Default-Konstruktor hat. Der Java-Code der Klasse Wald sieht so aus:
public class Wald {
private int id;
private Set<Baum> baumList=new HashSet<Baum>();
private String name;
protected Wald(String name) {
super();
this.name=name;
}
Wald(){
super();
}
public int getId() {
return id;
6
}
private void setId(int id) {
this.id = id;
}
public Set<Baum> getBaumList() {
return baumList;
}
public void setBaumList(Set<Baum> baumList) {
this.baumList = baumList;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
}
Wie weiß Hibernate aber nun wie diese Attribute zu befüllen sind und woher diese
Daten kommen? Dies geschieht über xml-Konfigurationsdateien (auch mit
Annotations im Code möglich). Wir verwenden wie üblich eine Konfigurationsdatei
pro Entitätsklasse und eine allgemeine für das gesamte Projekt. Als erstes
beschäftigen wir uns nun mit der allgemeinen Konfigurationsdatei, die direkt im
Classpath liegen muss und hibernate.cfg.xml heißt. In unserem Fall sieht sie so aus:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernateconfiguration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">
org.hsqldb.jdbcDriver
</property>
<property name="hibernate.connection.url">
jdbc:hsqldb:hsql://localhost
</property>
<property name="hibernate.connection.username">
test
7
</property>
<property name="hibernate.connection.password">
test
</property>
<property name="hibernate.connection.pool_size">
10
</property>
<property name="show_sql">false</property>
<property name="dialect">
org.hibernate.dialect.HSQLDialect
</property>
<property
name="hibernate.hbm2ddl.auto">update</property>
<!-- Mapping files -->
<mapping resource="de/wald/entity/Wald.hbm.xml"/>
<mapping resource="de/wald/entity/Baum.hbm.xml"/>
</session-factory>
</hibernate-configuration>
Der Header ist in Hibernate immer derselbe und wird daher in den folgenden
Beispielen vernachlässigt. Der interessante Teil steht im <session-factory>
Tag. Zunächst wird festgelegt um welche Art von Datenbank es sich handelt, indem
der richtige Treiber geladen wird. In unserem Fall handelt es sich um eine HSQLDB
(org.hsqldb.jdbcDriver). Danach wird die URL der Datenbank und
User/Passwort angegeben. Hibernate kann seinen eigenen Connection-Pool verwalten.
Jedes Mal wenn eine Connection benötigt wird, wird eine aus dem Pool geholt und
danach wieder zurückgelegt (Eine Connection kann auch mehrmals verwendet
werden). Über die pool_size-Property kann festegelegt werden wie groß dieser Pool
sein soll. Mit show_sql kann festgelegt werden, ob Hibernate die abgesetzten SQLBefehle mitloggen soll (auf der Konsole). Mit hibernate.hbm2ddl.auto
wird festgelegt wie die Datenbank genutzt wird. Dazu gibt es folgende
Möglichkeiten:
Quelle[3]
Wert
Update
Bedeutung
Vorhandene Tabellen werden benutzt und
ggf. angepasst
Abbruch mit Exception falls Überprüfung
des DB-Schemas zu einer Abweichung
führt.
Hibernate versucht die Tabellen neu
anzulegen
Validate
Create
8
Create-drop
Alle Tabellen werden gelöscht und neu
angelegt
Mit dem mapping-Tag werden dann die Konfigurationsdateien der Entitätsklassen bei
Hibernate registriert. In unserem Fall für Baum und Wald. Diese Entitätsklassen
müssen nun aber auch gemappt werden, da Hibernate wissen muss, welchen
Zusammenhang die Attribute der Objekte und die Spalten der Datenbanktabellen
haben. Diese Mapping-Dateien werden nun im folgenden Kapitel erläutert.
4 Mapping-Konfiguration
Nachdem wir uns jetzt bereits mit dem allgemeinen Konfigurationsfile beschäftigt
haben, widmen wir uns jetzt der Konfiguration für die Klasse Baum
(de/hibernate/entity/Baum.hbm.xml). Die Konvention für den Namen
der XML-Datei ist [Klassenname].hbm.xml
<hibernate-mapping package="de.wald.entity">
<class name="Baum">
<id name="id">
<generator class="increment"/>
</id>
<property name="name"/>
<many-to-one name="wald" column="Wald_id"
lazy="false"/>
</class>
</hibernate-mapping>
Mit dieser Konfiguration weiß Hibernate nun auch wie ein Objekt vom Typ Baum
bzw. die Tabelle Baum zu befüllen ist. Zunächst wird mit <hibernate-mapping
package="de.wald.entity">
definiert in welchem Package die
Konfigurationsklasse zu finden ist. Danach wird eingestellt in welcher Tabelle sich
die Klasse befindet (<class name...>). Wenn der Klassenname im Code dem
Tabellennamen der Datenbank gleicht, genügt <class name=“Baum“> - das
gleiche gilt für den Primärschlüssel und die Attribute. Andernfalls müsste das Tag um
das Attribut table=“[Tabellennname]“ bzw. column=“[Spaltenname]“ erweitert
werden, so dass Hibernate auch die Verbindung zur Datenbank hat. Danach wird der
Primärschlüssel definiert. Mit <generator class="increment"/> wird
festgelegt, auf welche Weise der Primärschlüssel generiert wird. Hier ein kleiner
Auszug der Möglichkeiten:
9
Quelle[2]
Wert
native
Bedeutung
Der ID-Algorithmus der Datenbank wird
verwendet
ID wird hochgezählt
Eindeutiger String wird generiert
ID muss manuell gesetzt werden
increment
uuid
assigned
Über <property name="name"/> wird dann die Spalte bzw. das Attribut name
registriert. Mit many-to-one wird eine Beziehung zwischen den Klassen Baum und
Wald hergestellt. Um dies zu verstehen, wird im Folgenden allgemein auf die
Darstellung von Beziehungen in Hibernate-Mappings eingegangen.
4.1 Beziehungen darstellen
Geschäftsobjekte und Wertobjekte
Ein Geschäftsobjekt/klasse ist eine Java-Klasse mit einer eigenen Identität, d.h. die
Java-Klasse besitzt ihre eigene Tabelle mit eigenem Primärschlüssel in der
Datenbank. Wertobjekte/klassen sind zwar auch Java-Klassen, die in der Datenbank
gespeichert werden, aber sie werden jeweils nur als Bestandteile von
Geschäftsobjekten in die Datenbank geschrieben – haben also keinen eigenen
Primärschlüssel in der Datenbank. Ein Beispiel wären hier die Klassen Ausflug und
Termin.
public class Termin {
private String tag;
private String uhrzeit;
private String datum;
...
}
public class Ausflug {
private Termin termin;
private String ziel;
private int id;
...
}
10
Die Klasse Ausflug hat eine eigene Tabelle Ausflug in der Datenbank. Man will jetzt
jedoch keine neue Tabelle für Termin machen, da die Attribute tag, uhrzeit und datum
direkt als Spalten in die Tabelle Ausflug eingetragen werden können. So hat man im
Java-Code zwar die Trennung zwischen Termin und Ausflug - in der Datenbank
jedoch nicht. Ausflug ist in diesem Fall also Geschäftsobjekt und Termin das
Wertobjekt, das einfach im Zuge der Veränderung von Ausflug in der Datenbank
auch in der Datenbank verändert wird. Wertobjekte benötigen daher auch keine
eigene Konfigurationsdatei. Sie werden mit der jeweiligen Konfiguration des
Geschäftsobjekts gemappt.
Beziehungen zwischen Wert- und Geschäftsobjekt
Bei dieser Beziehung gibt es nur zwei mögliche Typen: Das Geschäftsobjekt enthält
ein Wertobjekt (component) oder das Geschäftsobjekt enthält mehrere Wertobjekte
(composite).
Nehmen wir nochmals das Beispiel mit Ausflug und Termin zu Hilfe. Wir legen fest,
dass ein Ziel eindeutig bestimmt ist. Das bedeutet, die Datenbank enthält nicht
mehrfach dieselben Ziele. Ein Ausflug enthält einen Termin. Um nun Hibernate
mitzuteilen, dass Termin fester Bestandteil von Ausflug, also ein Wertobjekt von
Ausflug ist, muss die Konfigurationsdatei von Ausflug so aussehen:
<hibernate-mapping package="de.ausflug.entity">
<class name="Ausflug">
<id name="id">
<generator class="increment"/>
</id>
<property name="ziel"/>
<component name="termin">
<property name="tag"/>
<property name="uhrzeit"/>
<property name="datum"/>
</component>
</class>
</hibernate-mapping>
Den oberen Teil kennen wir ja bereits.
Interessant für uns ist nun das
<component>-Tag. Mit name wird festgelegt welchen Attributnamen das
Wertobjekt besitzt. Danach folgen noch im <component>-Tag die Attribute des
Wertobjekts. Die Klasse Termin ist durch dieses Mapping mit Ausflug verknüpft.
11
Objekte von Termin werden, wenn das zugehörige Ausflug-Objekt gespeichert wird,
auch in dieselbe Tabelle in der Datenbank geschrieben. Demnach könnte die Tabelle
Ausflug in diesem Fall zum Beispiel so aussehen:
id
ziel
tag
uhrzeit
datum
1
München
Dienstag
12:00
14.06.2009
2
Berlin
Montag
11:00
13.06.2009
Will man nun aber veranlassen, dass ein Ausflug an mehreren Terminen stattfindet,
muss man den <composite-element>-Tag benutzen. Der Ausflug enthält somit
eine Liste von Termin Objekten (als List-Objekt im Java-Code). In Hibernate können
verschiedene Collections dargestellt werden, was später noch erklärt wird. In der
Datenbank wird jetzt aber eine zweite Tabelle benötigt, um Redundanz zu vermeiden.
Wenn man die zweite Tabelle nicht aufsetzen würde, würde die Tabelle Ausflug etwa
so aussehen:
id
1
2
ziel
München
München
tag
Dienstag
Montag
uhrzeit
12:00
11:00
datum
14.06.2009
13.06.2009
In diesem Beispiel ist es noch relativ übersichtlich. Aber nehmen wir an das
Geschäftsobjekt enthält sehr viele Attribute. Dann würden alle diese Attribute pro
Wertobjekt redundant in der Datenbank erscheinen. Daher ist es sinnvoll eine zweite
Tabelle zu erstellen (in unserem Fall Termin). Diese Tabelle muss dann einen
Fremdschlüssel auf die Tabelle Ausflug haben. Die Konfigurationsdatei für Ausflug
würde dann so aussehen:
<hibernate-mapping package="de.ausflug.entity">
<class name="Ausflug">
<id name="id">
<generator class="increment"/>
</id>
<property name="ziel"/>
<list name="termine" table="Termin">
<key column="id"/>
<list-index column="list_id"/>
<composite-element class="Termin">
<property name="tag"/>
<property name="uhrzeit"/>
<property name="datum"/>
</composite-element>
</list>
</class>
</hibernate-mapping>
12
Zunächst wird über <list
name="termine"
table="Termin">
angegeben, wie das Attribut in der Klasse Ausflug heißt (hier: List<Termin>
termine). Dann wird über key der Fremdschlüssel festgelegt (hier steht dann die
Ausflug_id). Durch list-index bekommt jeder Eintrag der Liste eine laufenden
Nummer, so dass die Liste auch in der gleichen Reihenfolge nach dem reload aus der
Datenbank im Java Objekt Ausflug erscheint. Danach wird über <compositeelement class="Termin"> die Listen-Klasse festgelegt und darin die
einzelnen Attribute der Termin-Klasse aufgeschlüsselt. Die Tabellen Ausflug und
Termin sehen dann so aus:
Ausflug
id
1
2
ziel
München
Berlin
id
1
1
2
list_id
0
1
0
Termin
tag
Sonntag
Montag
Mittwoch
uhrzeit
18:00
10:00
09:00
datum
12.06.2009
13.06.2009
15.06.2009
Die Liste zum Ausflug München enthält somit zwei Termine und die zum Ausflug
Berlin nur einen.
Man hat in Hibernate aber auch die Möglichkeit mit anderen Collections zu arbeiten.
Hier eine kurze Übersicht:
Quelle[2]
Bag
Prinzipiell dasselbe wie List, jedoch mit der Eigenschaft, dass die
Reihenfolge der Listenelemente nicht mit verwaltet wird
(java.util.List):
<bag name="baumList" table="Baum">
<key column="Wald_id"/>
...
</bag>
List
List mit definierter Reihenfolge (java.util.List):
13
<list name=“baumList“ table=“Baum“>
<key column=“Wald_id“/>
<list-index column=“list_id“>
...
</list>
Map
Key/Value- Elemente werden gemappt (java.util.Map):
<map name=“baumList“ table=“Baum“>
<key column=“Wald_id“/>
<map-key column=“id“>
...
</map>
Set
Set ohne definierte Reihenfolge (java.util.Set):
<set name=“baumList“ table=“Baum“>
<key column=“Wald_id“/>
...
</set>
Außerdem gibt es noch SortedSet und SortedMap, auf die jetzt aber nicht näher
eingegangen wird.
Beziehungen zwischen Geschäftsobjekten
Zurück zu unserem Wald-Baum Beispiel. Hier haben wir zwei Geschäftsobjekte. Da
der Baum weiß zu welchem Wald er gehört, der Wald jedoch in einer anderen Tabelle
steckt, handelt es sich hierbei um eine Beziehung zwischen zwei Geschäftsobjekten
bzw. deren Tabellen. In Hibernate wird ausgehend vom Konzept der relationalen
Datenbanken grundsätzlich auch zwischen vier Beziehungstypen unterschieden:
one-to-one
Hier ist egal welche Klasse Konfigurations- bzw. Bezugsklasse ist.
Beide Klasse erhalten denselben Konfigurationseintrag, da sowohl
die eine als auch die andere Instanz der Klasse nur eine Instanz der
anderen Klasse enthalten darf. Beispiel: Ehemann-Ehefrau, da
jeder Ehemann nur eine Ehefrau hat und das gleiche auch für die
Ehefrau gilt.
many-to-one
Jede Instanz der Konfigurationsklasse enthält nur eine Instanz der
Bezugsklasse. Aber jede Instanz der Bezugsklasse kann mehrere
Instanzen der Konfigurationsklasse enthalten. Beispiel: Wald –
Baum (Baum=Konfigurationsklasse), da Baum nur ein Element
von Wald enthält.
14
one-to-many
Dies ist der umgekehrte Fall: Jede Instanz
der
Konfigurationsklasse kann mehrere Instanzen der Bezugsklasse
enthalten. Eine Instanz der Bezugsklasse ist aber genau einer
Instanz der Konfigurationsklasse zugeordnet. Beispiel: WaldBaum (Wald=Konfigurationsklasse)
many-to-many
Jede Instanz der Konfigurationsklasse kann mehrere Instanzen der
Bezugsklasse haben und umgekehrt. Beispiel: Auto-Person. Ein
Auto kann mehreren Personen gehören und eine Person kann auch
mehrere Autos haben.
Im
Fall
<many-to-one
name="wald"
column="Wald_id"
lazy="false"/> handelt es sich also um die many-to-one Beziehung. Wie
immer wird über name der Name des Attributs angegeben. Da diesmal aber der
relevante Spaltenname in der Datenbank vom Attributnamen in der Klasse abweicht,
wird über das Attribut column noch der Datenbank-Spaltenname angegeben. Die
Spalte in der Datenbank, auf die in der Konfiguration einer many-to-one Beziehung
Bezug genommen wird, ist der jeweilige Fremdschlüssel passend zur Bezugsklasse.
Mit lazy=“false“ wird festgelegt wie die Daten in diese Beziehung geladen werden.
Hibernate hat standardmäßig einen Lazy-Loading Mechanismus, so dass
Bezugsobjekte erst geladen werden, wenn sie tatsächlich per Aufruf gebraucht werden
(zuvor steht nur der Fremdschlüssel im Objekt). Lazy=“false“ schaltet diesen
Mechanismus ab, so dass sofort alle Daten geladen werden.
Nun beschäftigen wir uns mit dem Mapping für die Klasse Wald. Hier haben wir als
neue Herausforderung die Liste der Bäume, die in der Klasse Wald steckt, korrekt zu
mappen. Unsere Vorgabe anfangs war, dass ein Wald aus mindestens zwei Bäumen
bestehen muss. Ein Objekt vom Typ Wald beinhaltet somit mehrere Objekte des
Typs Baum.
<hibernate-mapping package="de.wald.entity">
<class name="Wald">
<id name="id">
<generator class="increment"/>
</id>
<property name="name"/>
<set name="baumList" table="Baum"
lazy="false">
<key column="Wald_id"/>
<one-to-many
class="de.wald.entity.Baum" />
</set>
15
</class>
</hibernate-mapping>
Der Anfang ist uns nun schon bekannt. Erst das Package angeben, dann die Klasse,
dann die Id und wie sie generiert wird, und schließlich dann noch das Attribut name
mappen. Neu ist das <set> - Tag. Hier wird eine Collection gemappt –nämlich ein
Set Objekt. Mit name wird hier wieder der Attributname in der Klasse Wald bekannt
gegeben und Table sagt Hibernate in welcher Tabelle danach zu suchen ist. Auch das
lazy Attribut hat hier wieder die selbe Bedeutung. Mit <key
column="Wald_id"/> wird der Fremdschlüssel zu Wald in der Tabelle Baum
festgelegt und schließlich wird mit one-to-many die Art der Beziehung festgelegt und
um welches Objekt es sich handelt. Hibernate nutzt dabei zum Befüllen der Tabelle
die zugehörigen getter und setter der Wald-Klasse:
public Set<Baum> getBaumList() {
return baumList;
}
public void setBaumList(Set<Baum> baumList) {
this.baumList = baumList;
}
Das sind die Möglichkeiten mit Beziehungen zwischen Klassen in Hibernate
umzugehen. Wie man eine Vererbungsbeziehung in Hibernate darstellt wird nun im
folgenden Kapitel erklärt.
4.2 Vererbung darstellen
Nehmen wir an, unser Wald-Baum Beispiel wird dahingehend verändert, dass nun
eine Klasse Eiche vorhanden ist, die von Baum erbt und Baum zwar seine Attribute
behält aber zu einer abstrakten Klasse wird:
package de.wald.entity;
public class Eiche extends Baum {
private String blaetterfarbe;
public String getBlaetterfarbe() {
return blaetterfarbe;
}
public void setBlaetterfarbe(String blaetterfarbe)
{
16
this.blaetterfarbe = blaetterfarbe;
}
}
package de.wald.entity;
public class Fichte extends Baum {
private String nadelhaerte;
public String getNadelhaerte () {
return nadelhaerte;
}
public void setNadelhaerte (String nadelhaerte) {
this. nadelhaerte = nadelhaerte;
}
}
Es gibt es drei Möglichkeiten Vererbung relational in einer Datenbank darzustellen:
•
•
•
Pro Klasse eine Tabelle
Pro konkrete Klasse eine Tabelle
Pro Klassenhierarchie eine Tabelle
Wir beschäftigen uns hier nur mit der zweiten Variante. Dennoch kann so eine
Entscheidung (wie bilde ich Vererbung relational ab?) nur im konkreten Projekt
getroffen werden. Es gibt hier keine Faustregel oder Ähnliches. Das heißt in unserem
Wald-Baum Beispiel gibt es für die Vererbungsbeziehung Baum-Eiche-Fichte nur
zwei Tabellen in der Datenbank, da Eiche und Fichte die konkreten Klassen sind und
Baum abstrakt ist. Dennoch benötigen wir für die Konfiguration dieser
Vererbungsbeziehung nur eine Mapping-Datei zur abstrakten Klasse Baum, die dann
so aussieht:
<hibernate-mapping package="de.wald.entity">
<class name="Baum">
<id name="id">
<generator class="increment"/>
</id>
<property name="name"/>
17
<many-to-one name="wald" column="Wald_id"
lazy="false"/>
<union-subclass name="Eiche" table="Eiche">
<property name="blaetterfarbe"/>
</union-subclass>
<union-subclass name="Fichte" table="Fichte">
<property name="nadelhaerte"/>
</union-subclass>
</class>
</hibernate-mapping>
Die Attribute, die direkt in der Klasse Baum definiert sind, werden wie zuvor durch
<property
name="name"/>
gemappt.
Mit
<union-subclass
name="Eiche" table="Eiche"> wird dann festgelegt, dass Eiche eine
konkrete Unterklasse von Baum ist und das zusätzliche Attribut blaetterfarbe hat.
Dasselbe gilt auch für die Fichte und ihr Attribut nadelhaerte. Die Tabellen würden
demnach so aussehen:
Eiche
id
1
2
4
name
Eiche1
Eiche2
Eiche3
wald_id
1
1
2
blaetterfarbe
Grün
Blau
Schwarz
id
3
5
name
Fichte1
Fichte2
wald_id
1
2
nadelhaerte
12
17
Fichte:
Wie man sieht, wird für beide Klassen nur ein Primärschlüssel verwaltet (id). Das
muss auch so sein, da der Primärschlüssel auf der Ebene des Baumes liegt und es
sonst zu doppelten Schlüsseln in der Klasse Baum kommen könnte.
Durch das Mapping ist nun der Grundstein gelegt um Abfragen auf die Datenbank
abzusetzen und Daten von Objekten persistent zu speichern. Das nächste Kapitel
erläutert daher wie Objekte gespeichert, geladen, gelöscht und verändert werden.
18
5 Sessions und Abfragen
5.1 Grundaktionen
Wie vorher unter 2. Erwähnt, wird eine Session (die zentrale Schnittstelle zu
Hibernate) nach folgendem Schema erzeugt:
Eine Configuration erzeugt eine SessionFactory, über die dann die Session instanziiert
werden kann. Mit der Session können nun beliebige Datenbankaktionen ausgeführt
werden: Speichern, Laden , Ändern, Löschen.
public void saveWald(Wald w){
Session s=DB.getSessionFactory().openSession();
Transaction t=s.beginTransaction();
s.saveOrUpdate(w);
t.commit();
//t.rollback();
s.close();
}
Die SessionFactory holen wir uns dabei so:
public static SessionFactory getSessionFactory(){
return new Configuraton
.configure().buildSessionFactory();
}
Mit saveWald wird nun also ein Objekt Wald in die Datenbank gespeichert. Zunächst
öffnet man eine Session mit Hilfe der SessionFactory. Dann muss bei schreibenden
Aktionen auf die Datenbank eine Transaktion begonnen werden. Mit saveOrUpdate
wird dann der Wald an der Session gespeichert, so dass das übergebene Objekt dann
beim nächsten commit der Transaktion in die Datenbank geschrieben wird.
SaveOrUpdate hat den Vorteil, dass der Entwickler sich nicht darum kümmern muss,
ob das Objekt jetzt neu ist oder einfach nur verändert wird. Hibernate sucht
automatisch danach, ob das Objekt vorhanden ist und schreibt dann entweder ein
neues Objekt in die Datenbank oder verändert das alte. Mit t.commit() wird
schließlich die Transaktion comitted (intern über session.flush()) und damit in die
19
Datenbank geschrieben). Am Ende sollte dann die Session geschlossen werden, da so
der Ressourcenverbrauch verringert wird. Statt t.commit() hat man auch die
Möglichkeit mit t.rollback() zu reagieren. Das bedeutet, dass die Transaktion
abgebrochen und verworfen wird und ein konsistenter Datenbankzustand herrscht.
Hier eine Übersicht der möglichen Aktionen an der Session:
void update(Object
o)
Hier muss das o-Objekt bereits vorhanden sein, da mit update
nur eine Veränderung eines bestehenden Datensatzes
vorgenommen wird. Sonst wird eine StaleStateException
geworfen.
void save(Object o)
Hier darf der Datensatz zum Objekt o noch nicht vorhanden
sein, da hier nur ein neues Objekt in die Datenbank
geschrieben wird.
void
saveOrUpdate(Object
o)
Object
get(Object.class,
Serializable id)
Ein neues Objekt wird in die Datenbank geschrieben oder ein
bereits bestehendes (in der Datenbank) modifiziert.
Object
load(Object.class,
Serializable id)
Das Objekt mit der übergebenen id wird aus der Datenbank
geladen. Falls das Objekt nicht existiert, wird eine
ObjectNotFoundException geworfen. Der Unterschied zu get
ist somit nur das Verhalten, falls das Objekt nicht existiert.
void delete(Object o)
Das Objekt o wird aus der Datenbank gelöscht. Falls es nicht
vorhanden ist, wird eine StaleStateException geworfen.
Das Objekt mit der übergebenen id wird aus der Datenbank
geladen. Falls das Objekt nicht existiert, liefert die Methode
null zurück.
Es brauchen nur die schreibenden Aktionen eine Transaktion. Somit genügt es für
load und get, wenn eine Session offen ist.
5.2 Zustandsmodell
Ein Objekt kann laut Hibernate drei verschiedene Zustände haben:
•
Transient: Ein neu erzeugtes Objekt oder ein gelöschtes Objekt aus der
Datenbank hat diesen Zustand. Das Objekt ist somit nur in der VirtualMachine jedoch nicht in der Datenbank vorhanden. Außerdem ist dieses
Objekt mit keiner Session verbunden.
20
•
Persistent: Objekt ist in der Datenbank, zumindest aber an der Session
gespeichert und wird mit dem nächsten session.flush() in der Datenbank
gespeichert. Alle Änderungen am mit der Session verbundenen Objekt haben
auch automatisch Auswirkung auf die Datenbank, da jeweils beim nächsten
flush() alle mit der Session verbundenen Objekte in die Datenbank
gespeichert werden.
•
Detached: Ein persistentes Objekt wird beim Schließen der Session von
dieser abgekoppelt. Das heißt, dass in der Datenbank zwar ein Eintrag
passend zum Objekt ist, eine Veränderung an dem Objekt jedoch nicht
automatisch in die Datenbank geschrieben wird. Dazu müsste das Objekt
wieder mit einer neuen Session verbunden werden z.B. über update().
Quelle[2]
21
5.3 HQL
Da die Abfragen oft komplizierter werden als nur über die Id ein Objekt zu laden
(z.B. where-Bedingung, Group By,etc.), gibt es in Hibernate die Abfragesprache
Hibernate Query Language (HQL), deren Abfragen in Hibernate durch einen
Algorithmus zu SQL Abfragen generiert werden. Die HQL (Quelle [1]) dient zum
Abfragen von Daten aus der Datenbank. Eine Query ist ein eigenes Objekt und kann
über Query q=session.createQuery(...) erstellt werden. Auf der Query kann dann z.B.
über die Methode q.list() die Abfrage ausgeführt werden und als List gespeichert
werden. Hier eine Übersicht über die Methoden, die die Queries ausführen und das
Ergebnis dieser Query zurückgeben:
list()
Liefert ein List Objekt zum jeweiligen Typen zurück
iterator()
Liefert einen Iterator über den jeweiligen Typen zurück
scroll()
Liefert ein ScrollableResult (Hibernate-Klasse) Objekt über den
jeweiligen Typen.
Wenn nur ein Ergebnis geliefert werden kann, wird durch diese
Abfrage entweder ein Ergebnis
• vom Typ Object, falls ein Ergebnis gefunden wurde
• mit dem Wert null, falls kein Ergebnis gefunden wurde
erstellt. Falls mehrere Ergebnisse gefunden wurden, wird eine
NonUniqueResultException geworfen.
uniqueResult()
Um also alle Wald Objekte zu bekommen, müsste unsere Abfrage so aussehen:
public List<Wald> findAllWald(){
Session s=DB.getSessionFactory().openSession();
List<Wald> waldList=(List<Wald>)s.createQuery("from Wald")
.list();
s.close();
return waldList;
}
Im Prinzip funktioniert die HQL wie SQL mit dem Unterschied, dass sich die Abfrage
auf die Klassennamen und Attributnamen bezieht und nicht auf die Datenbankinhalte.
Wenn ich also eine Tabelle1 auf Klasse1 gemappt habe (per XML-Datei) muss die
Abfrage from Klasse1 lauten und nicht wie bei SQL from Tabelle1. Die bekannten
SQL-Funktionen wie Count(...), Group By, etc. sind auch in HQL vorhanden und
werden auf die gleiche Weise verwendet. Der einzige Unterschied ist, dass HQL mit
den Klassen des Java-Codes arbeitet und SQL mit den Tabellen in der Datenbank.
Der Vorteil dabei ist, dass der Java-Code so frei von jeglichen Datenbankelementen
wie Tabellennamen oder Ähnlichem ist.
22
Wenn in der from-Klausel mehrere Typen angegeben werden, wird eine Liste aus
Object-Arrays, das die Länge der Anzahl der Typen hat, geladen. Das Object-Array
hat die Typen genau in der Reihenfolge gespeichert wie sie in der from-Klausel
angegeben wurden. Wenn man alle Wälder und dazugehörigen Bäume laden will,
sieht das so aus:
public List<Wald> findAllWaldBaeume(){
Session s=DB.getSessionFactory().openSession();
List<Object[]> waldList = s.createQuery("from Wald, Baum")
.list();
s.close();
return waldList;
}
Die Liste (hier: Liste von 2-dimensionalen Object-Arrays) hat dann z.B. folgende
Elemente zum Inhalt:
Object[0]
Wald1
Wald1
Wald2
Object[1]
Baum1
Baum2
Baum3
Zeile ist Ergebnis von Aufruf
waldList.get(0)
waldList.get(1)
waldList.get(2)
Natürlich ist es auch möglich where-Bedingungen anzugeben. Auch hier gilt wieder,
dass HQL mit den Klassen arbeitet und in der Bedingungen dann auch [Alias der
Klasse].Attributname angegeben werden muss:
public List<Object[]> findWaldBaeumeByWaldname(String name){
Session s=DB.getSessionFactory().openSession();
List<Object[]> waldList = s.createQuery("from Wald w,
Baum b where w.name=?").setString(0,name).list();
s.close();
return waldList;
}
Das ? stellt hier einen Platzhalter dar und kann über die set Methoden der Query (z.B.
setInteger(position des ?, Element)) befüllt werden. Die Position des ? wird
fortlaufend mit 0 beginnend aufgezählt (z.B. where w.name=? (Position:0) and w.id=?
(Position:1)...) . Der Vorteil dieser set-Methoden ist, dass sie jeweils wieder das
aktuelle Query-Objekt zurückgeben, so dass danach gleich list() oder wieder eine setMethode aufgerufen werden kann.
23
6 Demoprojekt
Das Demoprojekt ist eine Anwendung, die eine Plattform für Unternehmen anbietet
Produkte zu verwalten, zu bestellen (von anderen Unternehmen) und zu
produzieren(falls Produkte von anderen Unternehmen bestellt worden sind). Hierfür
gibt es vier Entitätsklassen, die mit xml-Dateien gemappt wurden: Unternehmen,
Ware, Bestellung, Bestellposition. Jede dieser Klassen hat eine zugehörige HomeKlasse (z.B. UnternehmenHome), die die zugehörigen Objekte per HQL aus den
Datenbanktabellen lädt, Objekte in der Datenbank speichert und hierfür jeweils die
nötigen Methoden für die verschiedenen Abfragen zur Verfügung stellt.
7 Fazit
Da Hibernate ein Open-Source-Produkt ist, ist dieses Framework auf der Kostenseite
nicht zu schlagen. Aus dieser Art der Vermarktung ergibt sich auch die hohe
Verbreitung von Hibernate. Dadurch ist es über die Jahre immer stabiler und
leistungsstärker geworden. Außerdem hat sich Hibernate in vielen anderen
Frameworks als das Persistenzframework etabliert., z.B. in Spring(Quelle [4]) oder
dem JBoss(Quelle [5]) Anders als bei manch anderen Frameworks muss man zur
Verwendung von Hibernate von keinen Klassen des Frameworks erben, was den
Entwickler einschränken würde, da Java keine Mehrfachvererbung unterstützt. Die
von Hibernate generierten SQL-Abfragen sind außerdem sehr effizient, so dass ein
Performanzproblem in einer Anwendung nicht auf Hibernate zurückzuführen ist.
Allerdings muss man, um eine optimale Performanz aus Hibernate zu holen, ein sehr
gutes Know-How des Frameworks haben, da Hibernate sehr viele
Konfigurationsmöglichkeiten hat, wobei aber viele schon eine Default-Einstellung
haben. Hibernate ist also ein sehr mächtiges Framework, mit dem sowohl ganze
Unternehmenssysteme arbeiten können, aber auch Kleinsysteme, die schnell fertig
gestellt werden sollen.
24
8 Quellen
[1] http://docs.jboss.org/hibernate/stable/core/reference/en/html/queryhql.html
[2] Einführung in Hibernate – Persistenz in Java-Systemen mit Hibernate 3; Beeger,
Hasse, Roock, Sanitz
[3] http://docs.jboss.org/hibernate/stable/core/reference/en/html/
[4] http://www.springsource.org/
[5] http://www.jboss.org/
25
Herunterladen