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:
Database Manager connection
Es öffnet sich dieser Traum von einem Fenster. Die begrenzte Funktionalität ist hoffentlich selbsterklärend ;-).
Database Manager


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:
Build Path
Im Einzelnen: 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:


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.
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:
Datenbankinhalt



Stand 13.04.2010
Historie:
13.04.2010: Erstellt