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