Beispiel: Daten speichern/laden mittels Hibernate
Das folgende Beispiel verwendet "Hibernate" als Entity Manager, der sich also komplett um die Datenbankzugriffe und um das Mapping von Datenbanktabelle
auf Javaklasse kümmert. Als Datenbank wird "HSQLDB" verwendet, die den Vorteil bietet, dass sie "In Process" laufen kann, also ohne Server.
Inhalt:
HSQLDB
Hibernate
Eclipse-Projekt
Hibernate-Config
Hibernate im Programm
Entities
Relationship
Hier gibt es die Sourcen (gesamtes Eclipse-Projekt): HibernateTesting.zip
HSQLDB
Installation
HSQLDB 1.8.1.2 von http://www.hsqldb.org/ herunterladen und irgendwohin entpacken. Fertisch ;-).
Da wir später aus Gründen der Einfachheit den "In Process Mode" verwenden (http://hsqldb.org/doc/guide/ch01.html#N101A8),
und in diesem Modus die Datenbank beim ersten Zugriff im Anwendungsverzeichnis erzeugt wird, sind keine weiteren Vorbereitungsarbeiten nötig.
Database Manager
Mittels des "HSQL Database Manager" können wir einen Blick in die Datenbank werfen (nachdem sie automatisch erzeugt wurde).
Startaufruf:
java -cp c:/temp/hsqldb_1.8.1.2/lib/hsqldb.jar org.hsqldb.util.DatabaseManager
Es erscheint ein Connect-Fenster. Hier werden folgende Daten eingegeben:
- Ich empfehle, dem Satz von Einstellungen einen "Setting Name" zu geben, damit man dieses beim nächsten Starten des Managers
geladen werden kann. Tut man dies nicht, sind die Einstellungen wesch.
- Beim Type wird "HSQL Database Engine In-Memory" gewählt.
- Beim Driver wird "org.hsqldb.jdbcDriver" eingetragen.
- Die URL hat die Form "jdbc:hsqldb:file:datenbankname". Beim Datenbanknamen kann man einen kompletten Pfad eintragen, es reicht aber auch der reine Dateiname,
wenn man den Database Manager aus dem Verzeichnis der Datenbank heraus startet.
- Username ist bei den automatisch generierten Datenbanken immer "SA", Passwort bleibt leer.
Es öffnet sich dieser Traum von einem Fenster. Die begrenzte Funktionalität ist hoffentlich selbsterklärend ;-).
Hibernate
Das Beispiel verwendet Hibernate 3.5.0 (http://www.hibernate.org/). Irgendwohin entpacken und das war es.
Zusätzlich wird SLF4J (Simple Logging Facade for Java) in Version 1.5.11 (http://www.slf4j.org/) benötigt,
da sonst der Anwendungsstart mit dieser Fehlermeldung fehlschlägt:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/impl/StaticLoggerBinder
at org.slf4j.LoggerFactory.getSingleton(LoggerFactory.java:230)
at org.slf4j.LoggerFactory.bind(LoggerFactory.java:121)
at org.slf4j.LoggerFactory.performInitialization(LoggerFactory.java:112)
at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:275)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:248)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:261)
at org.hibernate.cfg.Configuration.(Configuration.java:165)
at de.fhw.swtprojekt.hibernatetest.HibernateTestMain.main(HibernateTestMain.java:26)
Caused by: java.lang.ClassNotFoundException: org.slf4j.impl.StaticLoggerBinder
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 8 more
Hibernate selbst referenziert Version 1.5.8, aber das tricksen wir später aus.
Wir werden später die Variante "Simple" (enthalten in slf4j-simple-1.5.11.jar) verwenden, die alles mit Loglevel INFO und höher auf der Konsole (System.err) ausgibt.
Eclipse-Projekt
Das Eclipse-Projekt muss folgende JARs im Build Path haben:
Im Einzelnen:
- Treiber für HSQLDB: "PFAD_ZUR_HSQLDB\hsqldb\lib\hsqldb.jar"
- Hibernate-Dateien: Alle in "PFAD_ZU_HIBERNATE\hibernate-distribution-3.5.0-Final\lib\required" mit Ausnahme von slf4j-api-1.5.8.jar
- SL4J: "PFAD_ZU_SL4J\slf4j-1.5.11\slf4j-simple-1.5.11.jar" und außerdem "slf4j-api-1.5.11.jar". Letztere Datei ist eigentlich bereits in Hibernate enthalten,
allerdings dort in Version 1.5.8, und das gibt beim Programmstart folgende Fehlermeldung:
SLF4J: The requested version 1.5.11 by your slf4j binding is not compatible with [1.5.5, 1.5.6, 1.5.7, 1.5.8]
SLF4J: See http://www.slf4j.org/codes.html#version_mismatch for further details.
Damit alle aus einer Projektgruppe mit dem Projekt arbeiten können, würde ich empfehlen, die Dateien entweder direkt ins Projekt zu hängen (Unterverzeichnis "lib"
ist hier wohl Pseudostandard), oder mit Eclipse-Classpath-Variablen zu arbeiten ("Preferences" => "Java" => "Build Path" => "Classpath Variables").
Diese Classpath-Variablen müssen bei allen Projektmitarbeitern unter dem gleichen Namen vorhanden sein, können aber jeweils auf individuelle Pfade verweisen.
Hibernate-Config
Hibernate wird durch eine Datei "hibernate.cfg.xml" initialisiert, die im Eclipse-Projekt im Verzeichnis "src" liegen muss.
In meinem Beispiel sieht sie so aus:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="connection.url">jdbc:hsqldb:file:swtprojektdb</property>
<property name="connection.username">sa</property>
<property name="connection.password"></property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.HSQLDialect</property>
<property name="hbm2ddl.auto">update</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<mapping resource="de/fhw/swtprojekt/hibernatetest/Kuchen.hbm.xml" />
<mapping resource="de/fhw/swtprojekt/hibernatetest/Zutat.hbm.xml" />
</session-factory>
</hibernate-configuration>
Die Einstellungen im Einzelnen:
- "connection.driver_class", "connection.url", "connection.username", "connection.password" definieren die JDBC-Verbindung.
Hier verweise ich auf eine Datei-Datenbank namens "swtprojektdb", die im Projektverzeichnis liegt. Alternativ wären auch volle Pfadangaben möglich.
- Der "dialect" definiert für Hibernate, welche Datenbank angesprochen wird. Je nach Datenbank sind gewisse SQL-Elemente unterschiedlich.
- "hbm2ddl.auto" gibt an, wie Hibernate beim Anwendungsstart mit der Datenbank umgehen soll. Der Wert "update" gibt an, dass Datenmodelländerungen gemäß
Hibernate-Deklarationen in die Datenbank übernommen werden sollen. D.h. werden im Code Datenklassen zugefügt, werden sie beim nächsten Start automatisch
in der Datenbank angelegt. Ähnliches gilt für andere Änderungen, allerdings würde ich nicht garantieren, dass dies immer funktioniert.
Alternativen wären z.B. "create" => macht bei jedem Anwendungsstart die DB neu.
- "show_sql" schreibt die SQL-Befehle auf die Konsole => sehr nützlich fürs Debuggen.
- Auf die folgenden Mappings gehe ich später ein.
Hibernate im Programm
Der Programmrahmen für Hibernate-Zugriffe sieht so aus:
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
session.getTransaction().begin();
...auf der Session Operationen ausführen...
session.getTransaction().commit();
session.close();
Die erste Zeile des Beispiels liest die Konfiguration aus dem vorherigen Abschnitt ein.
Eine wichtige Besonderheit gibt es bei unserer In-Process-HSQLDB: diese verwirft ihre Daten normalerweise beim Anwendungsende.
Einzige Möglichkeit zum Speichern: ein SQL-Statement "SHUTDOWN" zu ihr schicken.
Bei Hibernate arbeitet man normalerweise nicht direkt mit SQL-Statements, aber auch dies ist möglich (indem ich es hier als Query vom Typ "Update" ausführe):
session.createSQLQuery("SHUTDOWN").executeUpdate();
Entities
Eine Datenklasse ist eine simple Java Bean mit Properties (also eine Kombination aus Membervariable und getter und setter). Zu ihr gehört außerdem eine Config-Datei,
die die Abbildung auf eine Datenbanktabelle beschreibt.
Meine Anwendung besteht aus zwei Entities, "Kuchen" und "Zutat". "Kuchen" sieht so aus (hier noch die vereinfachte Variante ohne Relationship):
package de.fhw.swtprojekt.hibernatetest;
public class Kuchen
{
private int iId;
private String sName;
public int getId()
{
return iId;
}
public void setId(int iId)
{
this.iId = iId;
}
public String getName()
{
return sName;
}
public void setName(String sName)
{
this.sName = sName;
}
@Override
public String toString()
{
return this.sName;
}
}
"toString" ist überladen, damit man in Konsolenausgaben schöne Anzeigen bekommt.
Die Deklaration als Hibernate-Klasse erfolgt in einer Datei "...hbm.xml" (ich würde empfehlen, sie immer genauso zu nennen wie die Datenklasse, und sie ins gleiche Verzeichnis
zu legen). Im Beispiel also "Kuchen.hbm.xml" im Package "de.fhw.swtprojekt.hibernatetest":
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="de.fhw.swtprojekt.hibernatetest.Kuchen" table="Kuchen">
<id name="id" column="kuchen_id">
<generator class="native" />
</id>
<property name="name" column="name" />
</class>
<query name="alle_kuchen">from Kuchen</query>
</hibernate-mapping>
Ich denke die Elemente erklären sich selbst.
- Jede "normale" Spalte wird über ein Element "property" deklariert. Die Property hat als "name" den Namen der Property (Regel: Namensanteil von "getXyz/setXyz",
ersten Buchstaben klein machen, also "xyz"). Der Spaltenname kann beliebig gewählt sein.
- ID-Spalten werden über das Element "id" deklariert. Hier kann man außerdem einen "generator" angeben, der dafür sorgt, dass beim Insert automatisch gültige IDs
vergeben werden. Der Wert "native" im Beispiel verwendet bei der HSQLDB eine "identity"-Funktion:
http://www.roseindia.net/hibernate/hibernateidgeneratorelement.shtml
- Falls man Datenbankabfragen verwendet, macht man dies am Besten, indem man eine "Named Query" deklariert. Wenn sie wie im Beispiel nur ein
"from ... where ..."-Fragment enthält, bezieht sie sich auf Instanzen der Klasse, in deren "...hbm.xml"-Datei man sich gerade befindet.
Im Code würde eine Instanz von "Kuchen" so simpel erzeugt:
session.getTransaction().begin();
Kuchen kuchen = new Kuchen ();
kuchen.setName("Käsekuchen");
session.save(kuchen);
session.getTransaction().commit();
Das SQL-Log dazu sieht so aus:
Hibernate: insert into Kuchen (kuchen_id, name) values (null, ?)
Hibernate: call identity()
Man beachte, dass alle Feldwerte als Parameter in die Query gepackt werden. Hierzu müsste das Logging ebenfalls einschaltbar sein.
Wenn es so funktioniert wie beim JBoss-Applikationsserver und "Enterprise Java Beans", dann sollte das über Log4J (also entsprechende SL4J-Implementierung verwenden!)
und folgenden Config-Datei-Eintrag möglich sein: ../../KomponentenArchitekturen2008/kuchen/index.html#logging.
Das Laden aller Kuchen anhand der oben deklarierten Named Query erfolgt so:
session.getTransaction().begin();
@SuppressWarnings("unchecked")
List<Kuchen> listKuchen = session.getNamedQuery("alle_kuchen").list();
for (Kuchen kuchen : listKuchen)
{
System.out.println("Geladener Kuchen: " + kuchen);
}
session.getTransaction().commit();
Man beachte, dass ich Eclipse austricksen musste, damit er mir keine Typkonvertierungswarnung bei der Liste ausspuckte ;-).
Hier sieht das SQL-Log so aus:
Hibernate: select kuchen0_.kuchen_id as kuchen1_0_, kuchen0_.name as name0_ from Kuchen kuchen0_
Relationship
Jetzt definieren wir eine Many-to-Many-Relationship zwischen Kuchen und Zutat, d.h. beim Kuchen wird hinterlegt, welche Zutaten in ihn gehören,
und bei der Zutat kann man ermitteln, in welche Kuchen sie kommt (eine "bidirektionale" Beziehung, im Gegensatz zur "unidirektionalen" Relation,
bei der die Datenmodell-Abbildung zwar gleich ist, aber nur eine Richtung per Javacode "navigierbar" ist).
In der Klasse "Kuchen" wird folgender Code zugefügt:
package de.fhw.swtprojekt.hibernatetest;
import java.util.HashSet;
import java.util.Set;
public class Kuchen
{
private Set<Zutat> zutaten = new HashSet<Zutat>();
...
public Set<Zutat> getZutaten()
{
return this.zutaten;
}
public void setZutaten(Set<Zutat> zutaten)
{
this.zutaten = zutaten;
}
}
Hinweis: Es wird ein "java.util.Set" statt einer "java.util.List" verwendet, da eine "List" die Besonderheit hat, dass Datensätze mehrfach vorkommen können.
Zumindest gemäß meiner JBoss/EJB-Erfahrung (dort wird ebenfalls intern Hibernate verwendet) kann eine "List" Fehlermeldungen spucken, weil Hibernate
bei einer längeren Relationskette meint, es könne das potentielle mehrfache Vorkommen von Daten nicht mehr abbilden.
In "Kuchen.hmb.xml" ist die Relation so deklariert:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="de.fhw.swtprojekt.hibernatetest.Kuchen" table="Kuchen">
...
<set name="zutaten" table="KUCHEN_ZUTAT" inverse="true" >
<key column="KUCHEN_ID"/>
<many-to-many column="ZUTAT_ID" class="de.fhw.swtprojekt.hibernatetest.Zutat"/>
</set>
</class>
...
</hibernate-mapping>
In der "Zutat" gibt es ebenfalls ein "Set" von Kuchen (analog zum Kuchen, deshalb kein Codebeispiel).
Allerdings gibt es in "Zutat.hbm.xml" einen wichtigen Unterschied:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="de.fhw.swtprojekt.hibernatetest.Zutat" table="Zutat">
...
<set name="kuchen" table="KUCHEN_ZUTAT">
<key column="ZUTAT_ID"/>
<many-to-many column="KUCHEN_ID" class="de.fhw.swtprojekt.hibernatetest.Kuchen"/>
</set>
</class>
</hibernate-mapping>
Hier fehlt das Attribut "inverse" (bzw. hat seinen Default "false"). Dadurch, dass man es wegläßt, erzeugt man eine bidirektionale Relationship.
Ich nehme an, dass diverse Besonderheiten, die für die Hibernate-Implementation der Enterprise Java Beans gelten, auch für das reine Hibernate zutreffen.
Deshalb sei für die Erklärung der Relationsattribute "lazy" (in EJB: "fetch-type") und "cascade" auf einen Jboss-Wiki-Eintrag verwiesen, den ich verbrochen habe:
https://www.jboss.org/community/wiki/EJB3relationships
Dort finden sich auch diverse Regeln für das programmatische Ändern einer Relationship (Erzeugen/Löschen von Verknüpfungen sowie Löschen von Elementen).
Im Testprogramm sieht das Erzeugen einer Kuchen-Zutat-Verknüpfung so aus:
session.getTransaction().begin();
kuchen1.getZutaten().add(zutat1);
zutat1.getKuchen().add(kuchen1);
session.save(kuchen1);
session.getTransaction().commit();
Wichtig ist hier, dass es nicht reicht, die Zutat dem Kuchen zuzufügen, sondern auch der umgekehrte Weg (Kuchen muss der Zutat zugefügt werden) muss
gegangen werden! Läßt man Schritt 2 weg, dann passiert beim Save des Kuchens rein garnichts!
Das SQL-Log zu diesem Codestück sieht so aus:
Hibernate: insert into KUCHEN_ZUTAT (ZUTAT_ID, KUCHEN_ID) values (?, ?)
Das Löschen habe ich zwar nicht ausprobiert, aber auch hier müssen vermutlich immer beide Seiten angepaßt werden, siehe oben verlinkter Wiki-Eintrag.
Das Ganze sieht in der Datenbank so aus:
Stand 13.04.2010
Historie:
13.04.2010: Erstellt