- Diplomarbeit - Entwicklung der Softwarearchitektur zum Projekt SAS III Implementierung der Datenbankzugriffe im Projekt SAS III unter Verwendung des OR-Mappers Hibernate Verfasser: Johannes Artner Reitern 8 A-3672 Maria Taferl Gutachter: Mag. Otto Reichel Bearbeitungszeitraum: September 2009 - Mai 2010 Erklärung Ich versichere hiermit, dass ich die vorliegende Diplomarbeit mit dem Thema Implementierung der Datenbankzugriffe im Projekt SAS III ” unter Verwendung des OR-Mappers Hibernate“ selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel benutzt habe. i Danksagungen An dieser Stelle möchte ich mich bei allen Personen bedanken, die mich bei der Erstellung dieser Diplomarbeit unterstützt haben. Ein großer Dank gilt meinen Eltern, die es mir ermöglichten, diese Schulausbildung zu erhalten und mich dabei so gut es ging unterstützten. Bedanken möchte ich mich auch bei meinem Betreuungslehrer, Herrn Prof. Mag. Otto Reichel, sowie dem gesamten Projektteam für die gute Zusammenarbeit. ii Zusammenfassung Projekt SAS III In diesem Projekt wird die serverseitige Software SAS (Schuladministrations Software) auf Web 2.0 umgestellt, um somit eine dynamische Java-Webapplikation zu entwickeln. Um dieses Ziel zu erreichen, werden die Technologien ICEFaces, Facelets und Hibernate evaluiert und verwendet. Übersicht über die Diplomarbeit: Ziel der Diplomarbeit ist es, die Anbindung an die Datenbank im Projekt SAS III unter der Verwendung des OR-Mappers Hibernate praktisch umzusetzen. Die verwendete Technologie wird unter Berücksichtigung der Einsatzmöglichkeiten in SAS III neben der praktischen Umsetzung auch theoretisch analysiert. iii Abstract Project SAS III The aim of this project is to convert the server-application SAS (School administration Software) to Web 2.0, to get a dynamic Java web application. To achieve this goal, the technologies ICEfaces, Facelets and Hibernate are evaluated and used. Overview on the diploma thesis: The aim of this dissertation is to maintain the access to the database using the OR-Mapper Hibernate. The used technology is also analyzed theoretically, considering to the field of application of SAS III. iv Inhaltsverzeichnis Vorwort i Erklärung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i Danksagungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii Abstract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv Inhaltsverzeichnis v 1 Motivation 1 1.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Objekt-Relationaler-Bruch . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2.2 Granularität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2.3 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2.4 Identität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2.5 Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2.6 Kardinalitäten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2.7 Datennavigation . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 v 1.3 Lösungsansätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Hibernate 5 7 2.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.2 Einsatzgründe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.3 Java Persistence API (JPA) . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.4 Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.4.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.4.2 Hibernate Core . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.4.3 Hibernate Annotations . . . . . . . . . . . . . . . . . . . . . . . . 10 2.4.4 Hibernate EntityManager . . . . . . . . . . . . . . . . . . . . . . 10 2.4.5 Hibernate Shards . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.4.6 Hibernate Validator . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.4.7 Hibernate Search . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.4.8 Hibernate Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3 Technologiebeschreibung Hibernate 13 3.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2 OR-Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2.2 Mapping mit XML . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.2.3 Mapping mit Annotations . . . . . . . . . . . . . . . . . . . . . . 20 3.2.4 Mapping mit XDoclet . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.3 Entitäten/Werttypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.4 Component Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 vi 3.5 Identität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.5.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.5.2 Synthetischer Primärschlüssel . . . . . . . . . . . . . . . . . . . 24 3.5.3 Fachlicher Primärschlüssel . . . . . . . . . . . . . . . . . . . . . 26 3.6 Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.7 Beziehungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.7.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.7.2 Kardinalitäten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.7.3 many-to-one / one-to-many . . . . . . . . . . . . . . . . . . . . . 36 3.7.4 one-to-one . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.7.5 many-to-many . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.8 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.8.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.8.2 Tabelle pro Klassenhierarchie (SINGLE TABLE) . . . . . . . . . 42 3.8.3 Tabelle pro Subklasse (JOINED) . . . . . . . . . . . . . . . . . . 44 3.8.4 Tabelle pro konkreter Klasse (TABLE PER CLASS) . . . . . . . 46 3.9 Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 3.9.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 3.9.2 hibernate.properties . . . . . . . . . . . . . . . . . . . . . . . . . 48 3.9.3 hibernate.cfg.xml . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 3.9.4 Zusätzliche Methoden . . . . . . . . . . . . . . . . . . . . . . . . 50 3.9.5 Konfiguration beim Mapping mit Annotations . . . . . . . . . . . 51 3.10 Sessions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 vii 3.10.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 3.10.2 Session-Level-Cache . . . . . . . . . . . . . . . . . . . . . . . . 54 3.10.3 Lebenszyklus von Entitäten . . . . . . . . . . . . . . . . . . . . . 54 3.11 Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.11.1 Connection-Pool . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.11.2 JNDI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.12 Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.12.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.12.2 Konkurrierende Datenbankzugriffe . . . . . . . . . . . . . . . . . 59 3.12.3 Transaktionsisolation . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.12.4 Sperrstrategien . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.13 Datenbankabfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.13.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.13.2 Hibernate Qery Language (HQL) . . . . . . . . . . . . . . . . . . 66 3.13.3 Criteria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 3.13.4 SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 3.14 Fetching und Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 3.14.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 3.14.2 Lazy Loading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 3.14.3 Eager Loading . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 3.14.4 Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 3.15 Entwicklungsstrategien . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 3.15.1 Top down . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 viii 3.15.2 Bottom up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 3.15.3 Middle-Out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 3.15.4 Meet-in-the-Middle . . . . . . . . . . . . . . . . . . . . . . . . . . 81 4 Pattern 83 4.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 4.2 Session per Request . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 4.3 Session per Conversation . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.4 Open Session in View . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 5 Umsetzung im Projekt SAS III 89 5.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 5.2 Aufbau der Applikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 5.2.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 5.2.2 POJO und Mapping-Informationen . . . . . . . . . . . . . . . . . 90 5.2.3 Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 5.2.4 SessionFactory . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 5.3 Views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 5.3.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 5.3.2 Navigationsbäume . . . . . . . . . . . . . . . . . . . . . . . . . . 96 5.3.3 Mapping Generator . . . . . . . . . . . . . . . . . . . . . . . . . 98 5.4 Sessions, Transaktionen und Lazy Loading . . . . . . . . . . . . . . . . 107 5.5 Sperren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 5.6 Component-Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 5.7 Criteria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 ix 5.8 Identität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 6 Konklusion/Ausblick 115 Anhang 117 Abbildungsverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Tabellenverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 Verzeichnis der Listings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 x Kapitel 1 Motivation 1.1 Übersicht Beinahe jede Anwendung benötigt persistente Daten. Diese Daten werden so gut wie immer in relationalen Datenbanken verwaltet. Das relationale Datenbankmodell wurde 1970 entwickelt und ist heute der de facto Standard. Dieses Konzept findet in kommerziellen, wie auch open-source Implementierungen Verwendung. Es ist flexibel, robust und über viele Jahre erprobt. Zur Verwaltung solcher Datenbanken dienen RDBMS1 . Mithilfe von SQL2 -Statements können Daten manipuliert bzw. ausgelesen werden. Mit der Entwicklung objektorientierter Programmiersprachen fand ein Bruch zwischen Anwendung und relationaler Datenbank statt. Dieser Strukturbruch wird als ObjektRelationaler-Bruch bezeichnet. 1.2 Objekt-Relationaler-Bruch 1.2.1 Übersicht Bei der Abbildung von Objekten auf relationale Datenbanken stehen sich zwei grundsätzlich unterschiedliche Modelle gegenüber. 1 2 Relationale Datenbank Management Systeme Structured Query Language 1.2 Objekt-Relationaler-Bruch 2 Objektorientierte Programmiersprachen verfügen über ein hierarchisches System von Objekten und Beziehungen zueinander. Relationale Datenbanken hingegen besitzen eine flache Struktur von Tabellen und Beziehungen. Der Objekt-Relationale-Bruch tritt erst bei komplexen Objektstrukturen auf. Ein einfaches Objekt kann als Zeile einer Tabelle gesehen werden. Die wesentlichen Unterschiede betreffen: • Granularität • Vererbung • Identität • Datentypen • Kardinalitäten • Datennavigation 1.2.2 Granularität Der Begriff Granularität bezieht sich auf die Anzahl der Unterscheidungsstufen von Elementen. In diesem Zusammenhang betrifft dies die Möglichkeit, eigene Datentypen zu verwenden. Java ist fein granular. Klassen bzw. Objekte werden als Datentypen betrachtet. Die Eigenschaften dieses Datentyps werden durch Instanzvariablen realisiert. Es können beliebige Datentypen angelegt werden. Hierbei nimmt auch die has-a“-Beziehung eine wesentliche Rolle ” ein. Objekte können Referenzen auf andere Objekte beinhalten, welche wiederum andere Objekte kapseln, ... . Relationale Datenbanken hingegen sind grob granular. Zwar gibt es in einigen Datenbanksystemen die Möglichkeit, eigene Datentypen zu realisieren, jedoch wird dies aufgrund von Kompatibilitätsproblemen und häufig auftretenden Fehlern abgelehnt. Eine Tabelle ist kein Datentyp. 1.2 Objekt-Relationaler-Bruch 1.2.3 3 Vererbung Ein wesentliches Element jeder objektorientierten Programmiersprache ist die Vererbung. Subklassen erben die Eigenschaften der Superklassen. Prinzipiell kann dieser Umstand mittels Sichten, sogenannten Views, auf Datenbankseite gelöst werden. Doch objektorientierte Programmiersprachen bieten ein weiteres Feature: Den Polymorphismus. Erst zur Laufzeit wird ersichtlich, welcher Objekttyp hinter einer Referenz steckt. Der Laufzeittyp muss zwar in der Vererbungslinie liegen, jedoch kann zur Compilezeit keine Aussage über den konkreten Typ getroffen werden. Auf Datenbankseite muss dies jedoch feststehen. Schließlich referenziert ein Foreign-Key einen Datensatz einer bestimmten Tabelle. 1.2.4 Identität Objekte können nach zwei Kriterien verglichen werden: • Objektgleichheit Beide Referenzen verweisen auf dasselbe Objekt. Dies wird durch den == Operator geprüft. • Wertgleichheit Zwei Objekte kapseln die gleichen Werte. D.h. Die Instanzen der Objekte haben den gleichen Inhalt. Dies wird durch die Methode equals() geprüft. Auf Datenbankseite wird die Identität mittels Primärschlüssel (engl. Primary Key) realisiert. Dieser ist innerhalb einer Tabelle eindeutig. Weder der == Operator, noch die Methode equals() entsprechen diesem Konzept. 1.2.5 Datentypen Java und Datenbanken stellen unterschiedliche Datentypen bereit. Grundsätzlich wird dieses Problem bereits von der JDBC3 -Schnittstelle gelöst. Datentypen der Datenbank werden in sinnvolle Java-Typen konvertiert (z.B. String - VARCHAR). 3 Java Database Connectivity 1.2 Objekt-Relationaler-Bruch 4 Jedoch bleibt ein gewisser Restaufwand. Auf Datenbankseite gibt es Zeichenbeschränkungen. Aus Performancegründen ist es sinnvoll, diesen Datenbankmechanismus auch auszunützen. Ansonsten würde beispielsweise der Java Datentyp String umgelegt auf die Datenbank die maximale Zeichenanzahl eines Varchars belegen. 1.2.6 Kardinalitäten In Java werden Beziehungen mittels Objektreferenzen dargestellt. Eine Referenz stellt eine Beziehung in genau einer Richtung dar. Sie ist unidirektional. Will man, dass eine Beziehung in beiden Richtungen besteht, muss auf beiden Seiten eine Objektreferenz angelegt werden. Auf Datenbankseite werden Fremdschlüssel (engl. Foreign Keys) verwendet. Beziehungen bestehen in beiden Richtungen. Sie sind bidirektional. Bei einer m:n-Beziehung wird in relationalen Datenbanken eine Kreuztabelle angelegt. Im Domainenmodell von Java existiert diese Kreuztabelle nicht. Mehrwertige Beziehungen werden mithilfe von Collections implementiert. 1 2 3 4 5 6 7 public class Schueler{ private Set<Erziehungsberechtigter> erziehungsberechtigte; } public class Erziehungsberechtigter{ private Set<Schueler> kinder; } Listing 1.1: m:n-Beziehung in Java 1.2.7 Datennavigation Ein weiterer Effekt der unterschiedlichen Struktur betrifft die Datennavigation. Will man in Java auf ein bestimmtes Element zugreifen, iteriert man das Objektnetzwerk bis zum gewünschten Element durch. aktDirektion.getOrt().getOrtPlz(); 1.3 Lösungsansätze 5 Auf Datenbankseite muss man entweder Joins oder Subselects verwenden, um die Postleitzahl der Direktion zu bekommen. SELECT ort.ort_plz FROM Direktion AS dir JOIN Ort AS ort ON ort.ort_id = dir.dir_ort_id WHERE dir.dir_schulkennzahl = ’302467’; 1.3 Lösungsansätze Es gibt nachstehende Möglichkeiten, um den Problemen des Objekt-RelationalenBruchs zu begegnen. • Die ursprüngliche Form ist, SQL-Statements selbst zu formulieren und so den Objekt-Relationalen-Bruch händisch“ zu umgehen. Ergebnisse von Anfragen müssen ” über Resultsets in die objektorientierte Struktur übertragen werden. Hierbei geht jedoch viel Zeit verloren und das Risiko des Scheiterns eines Softwareprojektes steigt. • Ein weiterer Ansatz ist, objektorientierte Datenbanken zu verwenden. Objektrelationale Datenbanken, wie Postgres oder Oracle, erweitern das relationale Modell um Konzepte objektorientierter Programmiersprachen. Diese Datenbankkonzepte finden derzeit jedoch noch wenig Verwendung. Aktuell bergen sie Probleme und sind nicht ausgereift. Des Weiteren lösen diese Datenbanken den Objekt-Relationalen-Bruch nicht vollständig. • Die dritte Möglichkeit ist die Verwendung eines OR-Mappers4 . Ein OR-Mapper stellt eine zusätzliche Schicht zwischen Anwendung und Datenbank dar. Neben dem Umgehen des Objekt-Relationalen-Bruchs gibt es weitere Gründe für den Einsatz eines OR-Mappers: 4 Objekt-Relationaler-Mapper 1.3 Lösungsansätze 6 • Performance Gute OR-Mapper optimieren die SQL-Abfragen hinsichtlich Performance. • Weniger Code SQL-Statements werden vom OR-Mapper erzeugt und müssen daher nicht mehr händisch“ in den Quellcode geschrieben werden. ” Entwickler/innen müssen sich nicht darum kümmern, wie die Daten aus der Datenbank geladen bzw. gespeichert werden. Dabei spricht man von der Transparenten Persistenz. Programmierer/innen können sich auf die Applikationslogik konzentrieren. Dadurch wird die Entwicklung effizienter. • Wartbarkeit Ein weiterer Effekt des reduzierten Codes ist, dass sich neue Entwickler/innen schneller in ein Projekt einarbeiten können. • Datenbankabstraktion Die SQL-Statements werden zur Laufzeit je nach Datenbankdialekt formuliert. Dadurch ist die Applikation einfach auf andere Datenbanken portierbar. Die Abhängigkeit zu einer Datenbankimplementierung sinkt, was sich positiv auf das Projektrisiko auswirkt. Eine OR-Mapper-Implementierung ist Hibernate. Kapitel 2 Hibernate 2.1 Übersicht Vor allem in der Java-Welt hat sich Hibernate als Standard OR-Mapper etabliert. Für das .NET Framework gibt es die äuquivalente Technologie NHibernate. Hibernate ist Bestandteil der JBoss-Gruppe, welche wiederum zu RedHat gehört. Die erste Hibernate-Version wurde im Jahr 2002 veröffentlicht. Die Hibernate-Bibliotheken stehen auf www.sourceforge.net, der weltgrößten OpenSource-Webseite zur Verfügung. Dort zählt das Hibernate-Projekt weit über 5 Millionen Downloads. 2.2 Einsatzgründe Gründe für den Einsatz von Hibernate sind: • Open Source – Es kann kostenfrei genutzt werden. Jedoch bleibt zu beachten, dass Schulungen bzw. Weiterbildungen sehr wohl hohe Kosten verursachen können. – Der Quelltext kann eingesehen werden. Dadurch ist es möglich, Detailfragen zu klären, wenn die Dokumentation 2.3 Java Persistence API (JPA) 8 nicht ausreicht. Unternehmen, welche langfristig auf diesen OR-Mapper setzen, können die Weiterentwicklung auch selbst übernehmen, sollte diese eingestellt werden. • Hibernate ist weit verbreitet Dadurch ist es einfach, qualifiziertes Personal zu finden bzw. zu ersetzen. Des Weiteren gibt es im Internet eine große Community, welche sich Problemen und Fragen annimmt. • Hibernate bewährt sich seit Jahren in unzähligen Projekten Dadurch ist es gut getestet und das Risiko ist sehr gering, dass es für das eigene Projekt nicht geeignet ist. • Einfaches Programmiermodell Die Einarbeitungszeit ist gering. • Transparenz Anders als bei anderen OR-Mappern werden Datenbankstrukturen und die Verwendung von SQL nicht vom Anwendungsentwickler verborgen. Dadurch können Fehler rasch gefunden bzw. gelöst werden. • Flexibel Hibernate kann sowohl bei bestehender Datenbank, dem Reverse Engineering, wie auch zum Generieren eines Datenbankschemas, dem Forward Engineering, eingesetzt werden. 2.3 Java Persistence API (JPA) JPA ist Teil der EJB1 3.0 Spezifikation. EJB beschreibt die serverseitige Architektur von Komponenten in Java EE2 . JPA definiert die Objekt-Relationale Abbildung. Sie besteht aus folgenden Komponenten: • Persistenzobjekt Hierbei handelt es ich um persistierbare Klassen, welche auf Tabellen abgebildet werden können. 1 2 Enterprise JavaBeans Java Enterprise Edition 2.4 Komponenten 9 • Metadaten Es handelt sich um Informationen der Abbildung von persitierbaren Klassen auf die Datenbank und umgekehrt. Diese Informationen werden mittels Annotations in die Persistenzklasse integriert. • JPQL Eine standardisierte objekorientierte Abfragesprache, deren Syntax an SQL angelehnt ist. Die EJB 3.0 Spezifikation wurde 2003 eingeführt. Wichtige Hersteller von OR-Mappern, darunter auch Hibernate, nahmen Einfluss auf diesen neuen Standard. Besonderer Wert wurde auf die Portabilität von Code gelegt. 2.4 Komponenten 2.4.1 Übersicht Hibernate Core stellt die Kernfunktionalität zur Verfügung. Auf dieser Plattform setzen nachstehende Erweiterungen auf: • Annotations • EntityManager • Shards • Validator • Search • Tools 2.4.2 Hibernate Core Im Wesentlichen liefert der Hibernate Core folgende Features: • SQL-Code-Generation Hibernate generiert die SQL-Statements je nach SQL-Dialekt zur Laufzeit. Als 2.4 Komponenten 10 SoftwareentwicklerIn greift man auf Objekte zu und läd diese durch vordefinierte Zugriffsmethoden (z.B. save, update, ..) aus der Datenbank. • Objektorientierung Unterstützt die Objekt-Orientierten-Konzepte und das Collections-API. • Objekt-Orientiertes-HQL3 / Einfaches SQL Sollte es nötig sein, können SQL-Befehle auch selbst formuliert werden. Hierbei ist es möglich, die Statements im jeweiligen SQL-Dialekt oder mittels HQL zu verfassen. • Metadaten in XML-Dateien Die benötigten Informationen über die Abbildung der Entitäten auf die Datenbank und umgekehrt stehen in XML-Dateien. 2.4.3 Hibernate Annotations Jeder OR-Mapper, und so auch Hibernate, benötigt Metadaten. Früher war es nur möglich, diese Informationen mittels XML-Dateien anzugeben. Seit dem JDK4 5.0 gibt es die Möglichkeit, Metadaten im Java-Quelltext mittels Annotations einzubinden. Dadurch steht die gesamte notwendige Information in einer Datei. Hibernate verwendet JPA-Annotations, stellt aber auch eigene bereit, um alle Feinheiten des Mappings zu ermöglichen. Ab der Hibernate-Version 3.5 sind Hibernate Annotations Teil des Hibernate Core. 2.4.4 Hibernate EntityManager Diese Komponente umhüllt den Hibernate Core, um vollständige Kompatibilität mit der JPA-Spezifikation zu gewährleisten. Insbesondere bedeutet dies die Verwendung von JPQL und der Einsatz von JPAAnnotations. Ab der Hibernate-Version 3.5 ist der Hibernate EntityManager Teil des Hibernate Core. 3 4 Hibernate Query Language Java Development Kit 2.4 Komponenten 2.4.5 11 Hibernate Shards Manchmal ist es notwendig, Datensätze einer Tabelle an unterschiedlichen Orten zu speichern. Gründe dafür sind vor allem Datensicherheit und Zugriffszeiten. Die Entwicklung von Software wird dadurch komplex. Hibernate Shards vereinfacht die Implementierung einer Software-Lösung bei Verwendung von verteilten Datenbanksystemen. 2.4.6 Hibernate Validator In den Metadaten können Integritätsbestimmungen angegeben werden. Somit können Eingaben des Benutzers vor dem Speichervorgang geprüft werden. Beim Forward-Engineering von Hibernate ist es möglich, diese Integritätsregeln in das Datenbankschema zu integrieren. 2.4.7 Hibernate Search Häufig ist es notwendig, eine Suchfunktion bereitzustellen. Das Suchen in normalen Textdateien stellt keine große Herausforderung dar. Schwierig wird es jedoch, wenn nach Daten gesucht wird, welche in relationalen Datenbanken stehen. Die in der Java-Welt am weitesten verbreitete Bibliothek für Volltextsuche liefert die Apache Software Foundation mit Lucene. Lucene ist stabil, weit verbreitet und sehr gut dokumentiert. Beispielsweise Wikipedia verwendet Lucene zur Volltextsuche. Hibernate Search verwendet intern Lucene. In den Hibernate-Metadaten kann ein Index für die Suche erstellt werden. 2.4.8 Hibernate Tools Hierbei handelt es sich um Plugins für Entwicklungsumgebungen und Apache Ant5 Tasks. Angeboten werden Unterstützungstools zur Entwicklung von Hibernate-Software. Dies 5 Apache Ant (engl. Ameise) ist neben anderen Verwendungsmöglichkeiten, ein Werkzeug zur automatisierten Erstellung von Quelltexten. 2.4 Komponenten 12 betrifft Code-Vervollständigung, das Reverse-/Forward-Engineering und viele mehr. Die Entwicklungsumgebung Netbeans 6.7.1, in dem das Projekt SAS III implementiert wird, stellt diese Hilfen eingeschränkt ebenfalls zur Verfügung. Kapitel 3 Technologiebeschreibung Hibernate 3.1 Übersicht Hibernate bildet eine zusätzliche Schicht zwischen Anwendung und Datenbank und legt die Objektstrukturen auf die relationale Datenbank um. Hibernate verwendet die in Abbildung 3.1 ersichtlichen Komponenten: • Entitäten/POJO1 s Persistierbare Geschäftsobjekte. • Mappings Mapping Informationen können in Form von Annotations und XDoclet integriert, oder in externen XML2 -Dateien stehen. • Configuration Hiermit wird das Verhalten von Hibernate und die Datenbankeinstellungen konfiguriert. • SessionFactory Wird durch ein Configuration-Objekt erzeugt und liefert Objekte vom Typ Session. Normalerweise gibt es genau eine SessionFactory pro Applikation. • Session Mithilfe von Sessions wird auf die Datenbank zugegriffen. 1 2 Plain Old Java Object Extensible Markup Language 3.1 Übersicht 14 – Transaction Um Datenbankbestände zu verändern dienen Transaktionen.´ – Query Hiermit können Datenbankabfragen formuliert werden. Abbildung 3.1: Hibernate Ueberblick Zur Illustrierung wird für die folgenden Kapitel ein einfaches Beispiel aus dem Projekt SAS III verwendet: Der Gegenstand. Die Tabelle gegenstand enthält folgende Spalten: 3.2 OR-Mapping 15 • geg_id Primärschlüssel, serial • geg_kurzbez Gegenstandskurzbezeichnung, character varying(10), NOT NULL • geg_upiskurzbez Gegenstandskurzbezeichnung in UPIS, character varying(10) • geg_langbez Gegenstandslangbezeichnung, character varying(100), NOT NULL • geg_gar_kurzbez Gegenstandsartkurzbezeichnung, character(1), NOT NULL, Foreign-Key auf die Tabelle Gegenstandsart • geg_notenattr_art Art des verwendeten Notenattributes, integer • geg_msp_id Sprache in der der Gegenstand unterrichtet wird, integer, NOT NULL, ForeignKey auf die Tabelle Muttersprache • geg_fremdsprbilddok Kürzel für Fremdsprache aus Bildungsdokumentation, integer 3.2 OR-Mapping 3.2.1 Übersicht Um die Objekt-Relationale-Abbildung zu beschreiben, gibt es folgende drei Möglichkeiten. • Mapping mit XML • Mapping mit Annotations • Mapping mit XDoclet Welche Variante des Mappings man verwendet, ist grundsätzlich egal. Die drei Varianten können auch parallel zueinander verwendet werden. 3.2 OR-Mapping 3.2.2 16 Mapping mit XML Übersicht Mapping mit Hilfe von XML-Dateien ist die ursprüngliche Form von Hibernate, die Objekt-Relationale-Abbildung zu beschreiben. Hierbei stehen Mapping-Informationen und Geschäftsklasse in unterschiedlichen Dateien. Das ist die Persistenzklasse (POJO) und jeweils eine zusätzliche XML-MappingDatei, in denen die Metadaten zur Persistenzklasse stehen. Abbildung 3.2: POJO - Mapping POJO Ein POJO stellt eine Entität der Applikationslogik dar. Man spricht von einem persistenten Objekt. Der Zustand eines POJOs muss jedoch nicht persistent sein. Weitere Informationen dazu gibt es im Kapitel 3.10.3. In der Variante des Mappings mithilfe von zusätzlichen XML-Dateien beinhalten die POJO-Klassen in der einfachsten Form lediglich die Instanzvariablen, entsprechende Getter- und Setter Methoden und einen parameterlosen Konstruktor. Fest vorgeschrieben ist nur der parameterlose Konstruktor. Es ist konfigurierbar, ob Hibernate direkt, oder mittels Getter/Setter auf die Instanzvariablen zugreift. Standardmäßig werden die Getter/Setter verwendet. 3.2 OR-Mapping 17 1 public class Gegenstand{ 2 3 private int gegId; 4 private String gegKurzbez; 5 private String gegUpiskurzbez; 6 private String gegLangbez; 7 private char gegGarKurzbez; 8 private Integer gegNotenattrArt; 9 private int gegMspId; 10 private Integer gegFremdsprbilddok; 11 12 public Gegenstand() { 13 14 } 15 16 public int getGegId() { 17 return this.gegId; 18 } 19 20 public void setGegId(int gegId) { 21 this.gegId = gegId; 22 } 23 //... 24 //Weitere Getter und Setter 25 //... 26 } Listing 3.1: Klasse Gegenstand Werden Getter/Setter verwendet, müssen diese dem JavaBean-Standard folgen. Grundsätzlich ist es nicht erforderlich, Getter und Setter bzw. Konstruktoren öffentlich zu machen. Hibernate greift mittels Reflection darauf zu. Mapping-Datei Der Inhalt dieser Datei beschreibt die Abbildung der einzelnen Klassen (POJOs) auf die Datenbank und umgekehrt. Speziell geht es hierbei um die Zuordnung der einzelnen Attribute der POJOs zu den Spalten der Tabelle. Es ist zwar möglich, den Inhalt der Mapping-Datei auf ein Minumum zu beschränken. Sinnvoll ist es jedoch, viele Informationen in die Mapping-Datei zu integrieren. Sonst leidet die Performance der Applikation. Schließlich müssen dann die genauen Infor- 3.2 OR-Mapping 18 mationen zu den einzelnen Member der Klasse zur Laufzeit geholt werden. Grundsätzlich ist es möglich, mehr als eine Klasse pro Mapping-Datei zu beschreiben. Aufgrund der Übersichtlichkeit ist von dieser Vorgangsweise jedoch abzuraten. In der Java-Welt haben sich zwei Standards etabliert. Zum einen liegen die MappingDateien im selben Paket wie das POJO. Zum anderen entspricht der Name der MappingDatei dem der Klasse und besitzt zusätzlich die Endung .hbm.. In diesem Fall ist der vollständige Dateiname deshalb Gegenstand.hbm.xml.. 1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping ←DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate- ←mapping-3.0.dtd"> 3 4 <hibernate-mapping> 5 <class name="Gegenstand" table="gegenstand" schema="public"> 6 <id name="gegId" type="int"> 7 <column name="geg_id"/> 8 <generator class="assigned"/> 9 </id> 10 <property name="gegKurzbez" type="string"> 11 <column length="10" name="geg_kurzbez" not-null="true" ←unique="true" /> 12 </property> 13 <property name="gegUpiskurzbez" type="string"> 14 <column length="10" name="geg_upiskurzbez"> 15 </column> 16 </property> 17 <property name="gegLangbez" type="string"> 18 <column length="100" name="geg_langbez" not-null="true" /> 19 </property> 20 <property name="gegGarKurzbez" type="char"> 21 <column length="1" name="geg_gar_kurzbez" not-null="true"/> 22 </property> 23 <property name="gegNotenattrArt" type="java.lang.Integer"> 24 <column name="geg_notenattr_art" /> 25 </property> 26 <property name="gegMspId" type="int"> 27 <column name="geg_msp_id" not-null="true" /> 28 </property> 29 <property name="gegFremdsprbilddok" type="java.lang.Integer"> 30 <column name="geg_fremdsprbilddok" /> 3.2 OR-Mapping 19 31 </property> 32 </class> 33 </hibernate-mapping> Listing 3.2: Gegenstand Mapping File Wie in obigem Listing zu sehen, besteht eine XML-Mapping-Datei, unter anderen, aus folgenden Komponenten: • DOCLET Am Anfang der XML-Datei wird die DTD3 angegeben. Eine DTD beschreibt die Struktur eines XML-Dokuments. Hibernate sucht diese Struktur zunächst im Classpath. Wird diese nicht gefunden, wird versucht, sie über den mittels des <!DOCTYPE>-Tags angegeben Link zu finden. • hibernate-mapping Die Mapping-Informationen beginnen mit diesem Element. Grundsätzlich ist es möglich, mehr als eine Klasse darin zu beschreiben. Doch wie oben erwähnt, ist davon abzuraten. Im <hibernate-mapping>-Tag können Standardeinstellungen von Hibernate geändert werden. Diese Einstellungen gelten für alle Kindelemente, sofern sie darin nicht überschrieben werden. • class Dieses Element ordnet die Klasse einer Tabelle zu. Der name-Parameter gibt den Namen der Klasse an und der table-Parameter definiert die verwendete Tabelle. Der schema-Parameter ist optional. Sollte es in der Datenbank mehrere Schemata geben, könnte man hier den Namen angeben. Wird dieses Attribut weggelassen, wird die Tabelle in der gesamte Datenbank gesucht. • id Dieser Tag beschreibt die Identität der Entität. Eine Entität muss eine Identität besitzen. Der name-Parameter gibt hierbei den Namen der Instanzvariable und type den Datentyp im POJO an. Der <id>-Tag hat folgende Kindelemente: – column Beschreibung der Datenbankspalte. – generator Dieses Element regelt, wie der Primärschlüssel vergeben werden soll. In unserem Beispiel erfolgt die Vergabe nach der Strategie assigned. 3 Document Type Definition 3.2 OR-Mapping 20 Mehr Informationen dazu folgen im Kapitel 3.5. • property und column Diese Elemente ordnen die Member von Klassen den Tabellen und umgekehrt zu. Auch hier gilt wieder: Umso mehr Informationen, desto schneller kann Hibernate arbeiten. Aus diesem Grund sollten auch optionale Parameter, wie beispielsweise type, angegeben werden. Hier können auch Datenintegritätsregeln angegeben werden. In diesem Beispiel werden die Parameter not-null und length gesetzt. 3.2.3 Mapping mit Annotations Seit Java 5 gibt es die Möglichkeit, Metadaten direkt im Quellcode bereitzustellen. Dies ist mithilfe von Annotations möglich. Hibernate erschien 2002. Java 5 im Jahr 2004. Dadurch kam dieses Feature erst später hinzu. Vor diesem Zeitpunkt war das Hinterlegen von Metadaten für Hibernate hauptsächlich in seperaten XML-Dateien vorgesehen. Mapping mit Annotations hat den Vorteil, dass die Informationen zu einer Klasse zentral in der Klassendatei abgelegt werden. Somit ist die Zuordnung von Instanzvariablen zu Spalten und umgekehrt einfacher. Alle Konzepte des XML-Mappings können auch beim Mapping mit Annotations angewendet werden. In dieser Diplomarbeit wird vor allem auf das Mapping mit XML eingegangen. Es werden aber immer wieder Ausblicke auf das Mapping mit Annotations gegeben. Annotations Eine Annotation (engl. Vermerk, Kommentar) beginnt mit einem @. Danach wird der Name der Annotation angegeben. Parameter werden nach dem Namen in runden Klammern angegeben (z.B. @Table(name = "tabelle1")). Hat eine Annotation nur einen Parameter, so muss der Name nicht angegeben werden. Es reicht, einfach den Wert anzugeben. Bei parameterlosen Annotations ist das Setzen von Klammern optional (z.B. @Entity). Java stellt sieben vordefinierte Annotations zur Verfügung. Dies sind: @Deprecated, @Override, @SuppressWarnings, @Documented, @Inherited, @Retention, 3.3 Entitäten/Werttypen 21 @Target. Es besteht die Möglichkeit, selbst Annotations zu definieren. Hibernate verwendet die in JPA definierten Annotations. JPA stellt bereits viele Annotations zur Verfügung. Um alle Feinheiten des Mappings zu ermöglichen, definiert Hibernate jedoch selbst weitere Annotations. Die Annotation @Enitity kennzeichnet eine Entitäts-Klasse. 3.2.4 Mapping mit XDoclet Bei dieser Variante werden die Mapping-Informationen mit XDoclet in den Quellcode der Klassen integriert und durch geeignete Hibernate-Module zur Laufzeit XML-MappingDateien erzeugt. Annotations machen diese Technik jedoch überflüssig. Daraus resultiert auch die geringe Bedeutung dieser Variante. 3.3 Entitäten/Werttypen Hibernate nimmt eine wesentliche Unterscheidung von Elemten vor. Diese wird hier für alle nachfolgenden Kapitel beschrieben. Es gibt Entitäts- und Wert-Typen. Entitäten (engl. Entities) sind Geschäftsobjekte. Also gemappte POJOs auf Java-Seite bzw. eine Zeile einer Tabelle auf Datenbankseite. Entitäten besitzen einen Primärschlüssel und durchlaufen einen Lebenszyklus (siehe Kapitel 3.10.3). Werttypen hingegeben besitzen keinen Primärschlüssel und somit keine Identität. Sie sind Teil einer Entität. Werttypen sind somit Spalten einer Tabelle bzw. Instanzvariablen von gemappten Klassen. Nicht gemappte Klassen können als Werttypen verwendet werden. Diese Unterscheidung ist sinnvoll und wichtig. Wie im Kapitel Objekt-RelationalerBruch beschrieben, ist Java im Gegensatz zu relationalen Datenbanken fein-granular. Mithilfe von Wertobjekten kann dieses Konzept auf die Datenbank übertragen werden. 3.4 Component Mapping 3.4 22 Component Mapping Werttypen können in eine Entität eingebettet werden. Zur Illustrierung wird ein einfaches Beispiel verwendet: Ein Kunde hat eine Addresse. Es ergeben sich zwei Klassen: Kunde und Addresse. Die Klasse Kunde besitzt ein Addresse-Objekt. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Entity @Table(name = "kunde") class Kunde implements Serializable { private int k_id; private String vorname; private String zuname; @Embedded private Addresse adresse; //...Konstruktoren, Getter/Setter } @Embeddable class Addresse implements Serializable { private int plz; private String ort; private String strasse; //...Konstruktoren, Getter/Setter } Listing 3.3: Annotation Component-Mapping Die Klasse Kunde kapselt ein Addresse-Objekt. Dieses wird mittels der @EmbeddedAnnotation für Hibernate eingebunden. Wird eine Klasse als Entität deklariert, so geschieht dies mithilfe der Annotation @Entity. Die Annotation @Emmbeddable definiert eine Werttyp-Klasse. In der Datenbank ergibt sich durch dieses Mapping die Tabelle kunde. Diese besitzt als Spalten jetzt die Instanzvariablen beider Klassen. Session s = new AnnotationConfiguration().configure(). buildSessionFactory().openSession(); Transaction tx = s.beginTransaction(); s.save(new Kunde("Max", "Mustermann", 3.4 Component Mapping 23 new Addresse(1010, "Wien", "Stephansplatz 1"))); tx.commit(); Nach Ausführung der obenstehenden Codezeilen ergibt sich folgender Datenbestand: k_id 1 ort plz strasse Wien 1010 Stephansplatz 1 vorname zuname Max Mustermann Beim Mapping mit XML sieht die Mapping-Datei für die Klasse Kunde folgendermaßen aus: 1 <hibernate-mapping> 2 <class name="Kunde" schema="public" table="kunde"> 3 <id name="id" type="integer"> 4 <column name="k_id"/> 5 <generator class="assigned"/> 6 </id> 7 <property name="vorname" type="string"> 8 <column name="vorname"/> 9 </property> 10 11 <property name="zuname" type="string"> 12 <column name="zuname"/> 13 </property> 14 15 <component name="adresse" class="Addresse"> 16 <property name="ort"/> 17 <property name="plz"/> 18 <property name="strasse"/> 19 </component> 20 </class> 21 </hibernate-mapping> Listing 3.4: XML Component-Mapping 3.5 Identität 3.5 24 Identität 3.5.1 Übersicht Jede Entität muss zur Unterscheidbarkeit eine Identität besitzen. Die Identität wird durch den Primärschlüssel geregelt. In der Variante des Mappings mit XML wird die Identität einer Entität über das <id>Element beschrieben. Beim Annotation-Mapping wird der Primärschlüssel mittels der @Id-Annotation gekennzeichnet. Im Großteil aller Fälle wird als Primärschlüssel ein Zahlenwert in Form von int, long oder short verwendet. Der Inhalt des Datensatzes hat keinen Einfluss auf den Primärschlüssel. Man spricht von einem synthetischen Primärschlüssel. Der andere Weg ist der fachliche Primärschlüssel. Dieser setzt sich aus Eigenschaften des Datensatzes zusammen. 3.5.2 Synthetischer Primärschlüssel Mittels des Tags <generator> kann die Erzeugungsstrategie festgelegt werden. <generator class="sequence"> <param name="sequence">gegenstand_geg_id_seq</param> </generator> Die Angabe des Sequenznamens ist optional. Beim Annotation-Mapping wird die Erzeugungsstrategie folgend festgelegt: @Id @GeneratedValue(generator = "seq") @GenericGenerator(name = "seq", strategy = "sequence") Die @GeneratedValue-Annotation ist in JPA definiert. Um alle von Hibernate definierten Strategien nutzen zu können dient die @GenericGenerator-Annotation. 3.5 Identität 25 Hibernate liefert nachstehende Erzeugungsstrategien: • increment Diese Strategie ist nur dann geeignet, wenn genau ein Prozess auf die Datenbank zugreift. Er liefert einen Zahlenwert vom Typ short, long oder int. Der Generator besitzt eine Instanzvariable, welche bei jedem Speichervorgang hochgezählt wird. Lediglich zur Initialisierung bei der ersten Verwendung muss ein Wert aus der Datenbank gelesen werden. Vorteil dieser Strategie ist die gute Performance. • sequence, identity, hilo und native Sequence und identity arbeiten ähnlich. Durch einen Datenbankmechanismus wird beim Speichern ein neuer Primärschlüssel generiert. Anschließend wird das ID-Attribut des Objekts gesetzt. Nachteil dieser beiden Versionen ist, das bei jedem Speichervorgang ein zusätzlicher Datenbankzugriff erforderlich wird. Die Datenbankmechanismen sind nicht Teil des SQL-Standard. Identity wird jedoch von den Datenbanken DB2, MySQL, MS SQL Server, Sybase und HypersonicSQL unterstützt. Sequence wird beispielsweise von Postgres und Oracle unterstützt. Hilo arbeitet ähnlich wie increment. Kapselt jedoch keine Instanzvariable. Stattdessen wird eine zusätzliche Schlüsseltabelle verwendet. Darüber hinaus wird im Speicher ein eigener Wert gehalten. Dadurch muss nicht bei jedem Einfügen auf die zusätzliche Tabelle zugegriffen werden. Diese Strategie kann auch bei Mehrbenutzersystemen eingesetzt werden. Des Weiteren ist sie relativ schnell, da nur selten auf die zusätzliche Tabelle zugegriffen werden muss. Native verwendet je nach Fähigkeiten des Datenbanksystems eine der drei beschriebenen Erzeugungsstrategien. • seqhilo Geht wie hilo vor. Der Unterschied besteht darin, dass statt einer zusätzlichen Schlüsseltabelle eine Datenbank-Sequenz verwendet wird. • assigned Bei dieser Strategie wird der Primärschlüssel händisch“ vergeben. Wie jedes ” andere Attribut muss auch der Primärschlüssel in der Programmlogik gesetzt werden. 3.5 Identität 26 • select Dieser Generator arbeitet mit Datenbanktriggern. Beim Einfügen eines Datensatzes werden Trigger aktiv, welche dem eingefügten Datensatz einen Primärschlüssel zuweisen. Anschließend wird dieser Wert von Hibernate ausgelesen. Dazu verlangt Hibernate jedoch eine zusätzliche UNIQUESpalte, um den eben erst eingefügten Datensatz wiederzufinden. Diese Strategie macht nur dann Sinn, wenn man aufgrund von Datenbankvorgaben zu dieser Vorgehensweise gezwungen ist. • foreign Dieser Generator ist nur in Verbindung mit einer 1:1 oder besser <one-to-one>bzw. @OneToOne-Beziehung anwendbar. Hierbei wird beiden an der Beziehung beteiligten Entitäten der selbe Primärschlüssel vergeben. Mehr Informationen dazu finden Sie im Kapitel 3.7. • uuid - Universally Unique Identifier Dieser Algorithmus liefert einen 32 Zeichen langen String, welcher abhänging von der Uhrzeit und IP-Adresse generiert wird. Der Vorteil dieser Strategie liegt in der Erzeugungsgeschwindigkeit. Immerhin kommt diese Strategie ohne Datenbankzugriffe aus. Auf lange Sicht wird das System jedoch verlangsamt, da es sich hierbei um einen String-Primärschlüssel handelt. Alle Fremdschlüssel auf diese Tabelle müssen ebenfalls einen String speichern, was einen Overhead darstellt. • guid - Globally Unique Identifier Arbeitet ähnlich wie uuid. Der Unterschied besteht darin, dass der eindeutige String mithilfe von Datenbankmechanismen generiert wird. Diese Mechanismen unterstützt nur der MS SQL Server und MySQL. Dadurch ist die Portabilität eingeschränkt. Noch dazu ist ein zusätzlicher Datenbankzugriff nötig. Deshalb gibt es eigentlich keinen Grund, diese Strategie zu verwenden. Die oben beschriebenen Strategien decken die meisten Möglichkeiten bereits ab. Sollte es jedoch notwendig sein, können auch eigene Generatorstrategien implementiert werden. 3.5.3 Fachlicher Primärschlüssel Wie bereits erwähnt, setzt sich ein fachlicher Primärschlüssel aus Eigenschaften des Datensatzes zusammen. 3.5 Identität 27 Grundsätzlich ist diese Form abzulehnen, da es vorkommen kann, dass zwei Datensätze denselben Primärschlüssel liefern. Des Weiteren können bei der Veränderung von Daten Inkosistenzen auftreten. Es gibt jedoch Fälle, in denen diese Fehler logisch auszuschließen ist, und ein fachlicher Primärschlüssel somit durchaus Sinn macht. Immerhin wird dadurch eine Spalte in der Tabelle und Aufwand bei der Erzeugung des synthetischen Primärschlüssels gespart. Im Gegenstand-Beispiel ist dies in Kombination der Gegenstandskurz- und -langbezeichnung der Fall. Hat ein Gegenstand das gleiche Kürzel und die gleiche Langbezeichnung handelt es sich um den selben Gegenstand. Fachliche Primärschlüssel werden in Hibernate als Composite-ID bezeichnet. Composite-ID Alle Spalten, welche nicht Teil des Primärschlüssels sind, werden wie üblich in der Geschäftsklasse gehalten. Zusätzlich kapselt die Geschäftsklasse das Identitätsobjekt. Dieses Objekt kapselt alle Spalten, welche Teil des Primärschlüssels sind. Zur Unterscheidbarkeit müssen die Methoden equals() und hashcode() in der Identitätsklasse implementiert werden. Zudem muss sie das Interface java.io.Serializable implementieren. Im oben beschriebenen Beispiel bilden die Spalten geg_kurzbez und geg_langbez die Identitätsklasse. Diese Klasse wird mit dem Namen GegenstandID benannt. 1 public class GegenstandID implements java.io.Serializable { 2 private String gegKurzbez; 3 private String gegLangbez; 4 5 public GegenstandID() { 6 } 7 8 //Getter und Setter ... 9 10 @Override 11 public boolean equals(Object obj) { 12 if (obj == null) { 13 return false; 14 } 15 if (getClass() != obj.getClass()) { 16 return false; 3.5 Identität 17 18 19 28 } final GegenstandID other = (GegenstandID) obj; if ((this.gegKurzbez == null) ? (other.gegKurzbez != null) ←: !this.gegKurzbez.equals(other.gegKurzbez)) { return false; } if ((this.gegLangbez == null) ? (other.gegLangbez != null) ←: !this.gegLangbez.equals(other.gegLangbez)) { return false; } return true; 20 21 22 23 24 25 26 27 28 29 30 31 } 32 33 34 35 } @Override public int hashCode() { int hash = 3; hash = 17 * hash + (this.gegKurzbez != null ? this. ←gegKurzbez.hashCode() : 0); hash = 17 * hash + (this.gegLangbez != null ? this. ←gegLangbez.hashCode() : 0); return hash; } Listing 3.5: Zusaetzliche Identifikationsklasse beim fachlichen Primaerschluessel Wie oben beschrieben, kapselt die Geschäftsklasse dieses Identitätsobjekt. Somit wird eine Instanzvariable vom Typ GegenstandID hinzugefügt. Der synthetische Primärschlüssel gegId wird nicht mehr benötigt. Ansonsten folgt diese Klasse den normalen Regeln einer Entitäts-Klasse. 1 public class Gegenstand { 2 3 private GegenstandID id; 4 private String gegUpiskurzbez; 5 private Character gegGarKurzbez; 6 private Integer gegNotenattrArt; 7 private Integer gegMspId; 8 private Integer gegFremdsprbilddok; 9 10 public Gegenstand() { 11 } 12 13 //... 14 //Getter und Setter 3.5 Identität 15 16 17 } 29 //... Listing 3.6: Persistenzklasse beim Fachlichen Primaerschluessel Zum Mappen des fachlichen Primärschlüssels wird beim XML-Mapping der <composite-id>-Tag verwendet. Dieser kann beliebig viele <key-property>-Tags aufnehmen. Das <key-property>-Element beschreibt eine Spalte des fachlichen Primärschlüssels. Es werden hier alle Instanzvariablen der Identitätsklasse beschrieben bzw. angegeben. 1 <composite-id name="id"> 2 <key-property name="gegKurzbez" column="geg_kurzbez"/> 3 <key-property name="gegLangbez" column="geg_langbez"/> 4 </composite-id> Listing 3.7: Fachlicher Primaerschluessel in der Mapping-Datei Beim Annotation-Mapping erfolgt das Mappen eines zusammengesetzten Primärschlüssels nach demselben Konzept. Hier wird die ID-Klasse mit der @Embeddable-Annotation definiert. In der Geschäftsklasse Gegenstand wird diese ID-Klasse mittels @EmbeddedId gekennzeichnet. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Entity @Table(name = "gegenstand") public class Gegenstand { private private private private private String gegUpiskurzbez; Character gegGarKurzbez; Integer gegNotenattrArt; Integer gegMspId; Integer gegFremdsprbilddok; @EmbeddedId private GegenstandID id; //... } @Embeddable public class GegenstandID implements java.io.Serializable { private String gegKurzbez; 3.6 Collections 20 21 22 23 24 25 26 } 30 private String gegLangbez; public GegenstandID() { } //... Listing 3.8: Annotation-Mapping des fachlichen Primaerschluessels 3.6 Collections Hibernate unterstützt alle in Abbildung 3.3 ersichtlichen Collections. Des Weiteren werden auch Java-Arrays unterstützt. Abbildung 3.3: Vererbungshierarchie von Collections und Maps 3.6 Collections 31 Um mehrwertige Datenbankbeziehungen auf die Java-Welt umzusetzen, werden Collections verwendet. Dies wird im nachfolgenden Kapitel 3.7 gezeigt. Die Verwendung von Collections beschränkt sich jedoch nicht auf die Abbildung von Assoziationen und deren Entitäten. Es können auch Collections von Wertobjekten definiert werden. JPA definiert kein Collection-Mapping von Wertobjekten. Die Hibernate-spezifische Annotation ist @CollectionOfElements. Es ist zu beachten, dass eine Collection in der Javaklasse mit dem Interface-Typen deklariert werden muss. List<Muttersprache> muttersprachen = new Vector<Muttersprache>(); Es gibt nachstehende Collection-Mappings. • <list> Hier wird eine java.util.List verwendet. Konkret handelt es sich daher um ArrayList, Vector oder LinkedList. In einer List wird die Einfügereihenfolge beibehalten. Um dies zu gewährleisten wird eine zusätzliche Indexspalte in der Tabelle benötigt. Diese Spalte wird über das Element <list-index> bzw. der @IndexColumn-Annotation angegeben. Dabei muss es sich um eine Spalte vom Typ Integer handeln. Häufig ist die Einfügereihenfolge jedoch egal. Dort sind andere Varianten vorzuziehen, um den Overhead einer Zusatzspalte zu vermeiden. • <bag> Hierbei handelt es sich genau wie bei <list> um eine java.util.List. Einziger Unterschied besteht darin, dass die Reihenfolge egal ist. Somit wird auch keine zusätzliche Indexspalte benötigt. • <set> Es handelt sich um eine Implementierung von java.util.Set. Im Gegensatz zu List kann ein Objekt in einem Set nur einmal vorkommen. Zu diesem Zweck sollten die Methoden equals() und hashCode() sinnvoll überschrieben werden. Die Reihenfolge, in der die Objekte gespeichert werden, ist von den Hashcodes abhängig. Will man eine bestimmte Sortierreihenfolge festlegen, wird ein SortedSet verwendet. Die Reihenfolge kann über einen Comparator bestimmt werden. Dieser wird im Mapping mithilfe des 3.6 Collections 32 sort-Parameters angegeben. Wird dieser Parameter angegeben, muss die Instanzvariable im POJO als SortedSet deklariert werden. • <array> und <primitive-array> Hierbei handelt es sich um einfache Java-Arrays. Das Mapping ist ident zu <list>. • <map> Eine Map speichert Schlüssel-Werte-Paare. Auf Datenbankseite ist der Schlüssel ein Foreign-Key und der Wert ein bzw. mehrere Spalten einer anderen Tabelle. Die Instanzvariablen im POJO können als java.util.Map oder java.util.SortedMap deklariert werden. Beim Annotation-Mapping sind die oben gezeigten Unterscheidungen nicht nötig. Die Mapping-Informationen dazu sind bereits über die Deklaration der Instanzvariable gegeben. private List collection1; Bei allen Collection-Mappings gibt das key-Element die Datenbankspalte mit dem Fremdschlüssel an. Beim Annotation-Mapping wird dieser über @JoinTable definiert. Werden Collections von Wertobjekten verwendet, so gibt der element-Tag an, welche Spalte als Wert geladen werden soll. Es ist ebenfalls möglich, mittels <composite-element> mehrere Wertetypen (also mehrere Spalten) zu laden. Beim Annotation-Mapping wird hierfür die @Column-Annotation verwendet. Wichtige Paramater, welche bei fast allen Collection-Mappings konfigurierbar sind, werden nachstehend erklärt. • lazy Über diesen Parameter kann die Fetching-Strategie konfiguriert werden. Standardmäßig ist das sogenannte Lazy Fetching aktiviert. Mehr Informationen dazu gibt es im Kapitel 3.14. • batch-size Definiert die Anzahl der Elemente, die beim Batch-Fetching aus der Datenbank geladen werden. Standardmäßig ist dieser Parameter auf 1 gesetzt. Dies entspricht der Select-Fetching Strategie. Dabei ist auf das sogenannte n+1-Problem zu achten. Mehr Informationen dazu gibt es wieder im Kapitel 3.14. • sort Wie oben beschrieben, kann eine Collection durch diesen Parameter sortiert werden. 3.7 Beziehungen 33 Wird er auf natural gesetzt, so wird die natürliche Reihenfolge der Elemente verwendet. Hier kann aber auch eine Comporator-Klasse angegeben werden. Standardmäßig wird keine Sortierung durchgeführt. • cascade Wird eine Entität persistiert, welche Collections anderer Entitäten referenziert, werden diese standardmäßig nicht mitgespeichert, verändert oder gelöscht. Mithilfe dieses Parameters kann dies verändert werden. Weitere Informationen zur Kaskadierung referenzierter Objekte gibt es im Kapitel 3.7. • order-by Hier kann eine Tabellenspalte angegeben werden, nach welcher sortiert werden soll. Optional kann nach dem Spaltennamen noch asc oder desc angegeben werden. Bei asc (engl. ascending) wird aufsteigend, bei desc (engl. descending) absteigend sortiert. • where Mithilfe dieses Parameters können Kriterien für das Einladen von Entitäten festgelegt werden. Z.B.: where="geg_msp_id=12" 3.7 Beziehungen 3.7.1 Übersicht Im Mapping können Assoziationen zwischen Entitäten definiert werden. Zur Erklärung der Beziehungstypen werden in diesem Kapitel die Namens- und Zeichnungskonventionen von ER4 -Modellen nach ISO5 -Standard verwendet. Binär/Reflexiv Grundsätzlich ist zwischen binären und reflexiven Beziehungen zu unterscheiden. Binäre Beziehungen sind Verbindungen zwischen verschiedenen Entitätstypen, während reflexive Beziehungen Verbindungen eines Entitätstyps zu sich selbst sind. 4 5 Entity Relationship International Organization for Standardization 3.7 Beziehungen 34 Abbildung 3.4: binaere/reflexive Beziehungen Unidirektional/Bidirektional Des Weiteren wird zwischen unidirektional und bidirektional unterschieden. Bei einer bidirektionalen Beziehung besteht die Verbindung in beiden Richtungen. D.h. eine Entität kann auf die Andere schließen und umgekehrt. Bei unidirektionalen Beziehungen besteht die Verbindung in nur einer Richtung. Auf Datenbankseite ist jede Beziehung bidirektional. Von jeder Seite der Beziehung kann auf die Andere geschlossen werden. Im Objektmodell von Java sind diese Rückschlüsse nicht möglich. Hibernate erlaubt die Verwendung bidirektionaler Beziehungen in Java. Dabei wird in beiden Mappings, der an der Beziehung beteiligten Entitäten, eine Referenz angelegt. Hibernate muss dabei wissen, welche Seite der Beziehung die Objekte in die Datenbank schreibt. Schließlich sind durch diese Form der Implementierung Dateninkonsistenzen möglich und Entitäten würden doppelt abgespeichert. Zu diesem Zweck gibt es die sogenannte owning“-Seite, welche für die Persistierung ” zuständig ist. Die andere Seite der Beziehung kann die Entitäten lesen, jedoch nicht speichern. Beim Annotation-Mapping kann die owning-Seite mittels des Parameters mappedBy definiert werden. Beim XML-Mapping dient hierzu der Parameter inverse. In den meisten Fällen ist es jedoch nicht sinnvoll, den Zusatzaufwand einer biderektionalen Beziehung zu haben. Stattdessen kann bei Bedarf eine Query abgesetzt werden. 3.7 Beziehungen 35 In diesem Kapitel wird ein Beispiel einer bidirektionalen Beziehung gezeigt. 3.7.2 Kardinalitäten Die Kardinalität beschreibt den Grad der Beziehung. Es wird auf Datenbankseite, und somit auch von Hibernate, zwischen folgenden Kardinalitäten unterschieden: • 1:n / n:1 Entspricht many-to-one / one-to-many bzw. @ManyToOne und @OneToMany • 1:1 Entspricht one-to-one bzw. @OneToOne • m:n Entspricht many-to-many bzw. @ManyToMany Zu jedem Beziehungstyp stellt Hibernate zusätzliche (optionale) Parameter zur Verfügung. Die Wichtigsten sind: • lazy Dieser Parameter konfiguriert das Verhalten von Hibernate in Bezug auf die Ladestrategie. true und false sind mögliche Werte. Wird dieser Parameter mit false gesetzt, so tritt das sogenannte Eager Loading in Kraft und die/das referenzierte(n) Objekt(e) werden sofort mitgeladen. Mehr Informationen dazu gibt es im Kapitel 3.14. • cascade Wird eine Entität auf die Datenbank geschrieben, werden dessen referenzierten Entitäten standardmäßig nicht mitgeschrieben. Wird beispielsweise im Gegenstand-Beispiel die Muttersprache als Objekt mitgeladen und dabei verändert (z.B. wird der Name der Mutterprache von Englisch auf english geändert), hätte das beim Speichern des Gegenstands keine Auswirkung. Zu diesem Zweck gibt es den Parameter cascade. Dieser kann mit create, merge, save-update, delete, lock, refresh, evict und replicate gesetzt werden. Sollen alle diese Operationen durchgeführt werden, gibt es zusätzlich den Wert all. Zu beachten ist, dass dieser Parameter mit Vorsicht zu genießen ist und nur dann eingesetzt werden sollte, wenn es wirklich sein muss. Schließlich wird sonst bei jeder Änderung eines Objekts eine zusätzliche Änderung auf die referenzierten Objekten durchgeführt. Dies kann zu ungewollten Änderungen und erheblichen Performanceverlusten führen. 3.7 Beziehungen 3.7.3 36 many-to-one / one-to-many Ein normaler“ Fremdschlüssel auf Datenbankseite stellt eine 1:n-Beziehung dar. Die” se Kardinalität kommt daher am häufigsten vor. Beliebig viele Datensätze können den selben Fremdschlüssel besitzen und somit den selben Datensatz referenzieren. Im Gegenstand-Beispiel ist dies beispielsweise bei der Muttersprache der Fall. Bis jetzt wurde lediglich der Wert, also der Fremdschlüssel als Wert geladen. Will man beispielsweise aber den Namen der Muttersprache anzeigen, ist es sinnvoll, die MutterspracheEntität mitzuladen. Viele (engl. many) Gegenstände können eine (engl. one) Mutterprache haben. Abbildung 3.5: 1:n-Beziehung Die 1:n-Beziehung entspricht dem <many-to-one>-Tag im Mapping. 1 <many-to-one name="muttersprache" class="Muttersprache" lazy=" ←false"> 2 <column name="geg_msp_id" not-null="true"/> 3 </many-to-one> Listing 3.9: XML-Mapping bei einer 1:n-Beziehung Der Parameter column gibt den Namen der Datenbankspalte an. Mit name wird der Name des Objektes im POJO angegeben. Wie oben beschrieben, können auch zusätzliche Parameter angegeben werden. In diesem Beispiel wird Lazy Loading deaktiviert. Diese Beziehung besteht in nur einer Richtung. Sie ist somit unidirektional. Eine bidirektionale Beziehung wird erreicht, indem auch im Mapping auf Seiten der Muttersprache eine Beziehung angelegt wird. Aus Sicht der Muttersprache stellt die Assoziation zu Gegenstand eine n:1-Beziehung dar. 3.7 Beziehungen 37 Hier wird one-to-many verwendet. Eine (engl. one) Muttersprache kann mehrere (engl. many) Gegenstände haben. Abbildung 3.6: n:1-Beziehung Es handelt es sich um eine mehrwertige Beziehung. Somit muss eine Collection verwendet werden. 1 <set inverse="true" name="gegenstaende"> 2 <key> 3 <column name="geg_msp_id" not-null="true"/> 4 </key> 5 <one-to-many class="Gegenstand"/> 6 </set> Listing 3.10: XML-Mapping bei einer n:1-Beziehung Wie anfangs beschrieben, ist bei bidirektionalen Beziehungen eine Seite für die Persistierung der Entitäten zuständig ( owning“-Seite). Die andere Seite darf die Entitäten ” zwar lesen, jedoch nicht speichern. Im Collection-Mapping wird der Parameter inverse auf true gesetzt. Dadurch ist dies die lesende Seite der Beziehung. Änderungen wirken sich nur dann aus, wenn sie auf Seite einer Gegenstand-Entität gemacht werden. 3.7.4 one-to-one Jeder Entität ist höchstens eine andere Entität zugeordnet. Dieser Beziehungstyp ist eher selten und stellt eine Sonderform dar. Beispiel für diesen Beziehungstyp: Ein Mann ist höchstens mit einer Frau verheiratet und umgekehrt. 3.7 Beziehungen 38 Abbildung 3.7: 1:1-Beziehung Frau und Mann sind dabei unterschiedliche Entitätstypen und stehen daher auch in unterschiedlichen Tabellen. Somit ist diese Beziehung binär. Würden Frauen und Männer in einer Tabelle, z.B. Personal, stehen würde es sich um eine reflexive Beziehung handeln. Auf Datenbankseite wird eine echte 1:1-Beziehung mit demselben Primärschlüssel dargestellt. Daraus ergibt sich, dass eine echte 1:1-Beziehung nicht reflexiv sein kann. Denn das würde eine Primärschlüssel-Verletzung bedeuten. Eine 1:1-Beziehung wird in der Mapping-Datei folgendermaßen realisiert: <one-to-one name="frau"> Wie im Kapitel Identität beschrieben, gibt es die Generatorstrategie foreign. Diese wird hier eingesetzt um den in Beziehung stehenden Entitäten Frau und Mann denselben Primärschlüssel zu vergeben. Um eine bidirektionale-Beziehung anzulegen, dient der Parameter property-ref. Beim Annotation-Mapping gibt es für eine 1:1-Beziehung die @OneToOne-Annotation. Wird der jeweiligen Instanzvariable neben dieser Annotation auch @PrimaryKeyJoinColumn beigefügt, so geht Hibernate von einer echten 1:1 Beziehung aus. Dadurch wird für beide an der Referenz beteiligten Entitäten derselbe Primärschlüssel verwendet bzw. vergeben. 3.7.5 many-to-many Eine m:n-Beziehung erfordert immer eine Verbindungstabelle. Diese Tabelle wird als Kreuztabelle bezeichnet. In der Kreuztabelle stehen üblicherweise nur Primärschlüssel beider Tabellen, welche an der Beziehung beteiligt sind. 3.7 Beziehungen 39 In relationalen Datenbanken hat sich der Standard etabliert, die Kreuztabelle folgendermaßen zu benennen: Name der ersten Tabelle, ein Unterstrich und anschließend der Name der zweiten Tabelle. Eine Kreuztabelle muss in Hibernate nicht gemappt werden. Illustriert wird die m:n-Beziehung wieder am Gegenstand-Beispiel. Man nehme an, dass ein Gegenstand in mehreren Sprachen unterrichtet werden kann. Abbildung 3.8: m:n-Beziehung Die Kreuztabelle wird mit gegenstand_muttersprache bezeichnet. Sie enthält im einfachsten Fall zwei Spalten. Dies sind die beiden Primärschlüssel der an der Beziehung beteiligten Entitäten. In diesem Beispiel sind das die Spalten geg_id (Primärschlüsselspalte von Gegenstand) und msp_id (Primärschlüsselspalte von Muttersprache). Es ergibt sich folgendes Mapping: 1 <set name="muttersprachen" table="gegenstand_muttersprache"> 2 <key column="geg_id"/> 3 <many-to-many class="Muttersprache" column="msp_id"/> 4 </set> Listing 3.11: XML-Mapping einer m:n-Beziehung Im key- und many-to-many-Tag werden die für die Beziehung benötigten beiden Spalten über das column-Attribut angegeben. Hier wird ersichtlich, dass die Kreuztabelle nicht gemappt werden muss. Des Weiteren ist es auch möglich, mehr als zwei Spalten in der Kreuztabelle zu haben. Durch obiges Mapping wird auf Gegenstand-Seite eine Collection vom Typ Muttersprache geladen. Soll diese Beziehung bidirektional sein, ist dies wie bei der 1:n/n:1-Beziehung gezeigten Form mittels inverse bzw. @mappedBy möglich. 3.8 Vererbung 40 Beim Annotation-Mapping gibt es für m:n-Beziehungen die @ManyToMany-Annotation. 1 2 3 4 5 6 7 8 public class Gegenstand{ ... private Set<Muttersprache> muttersprachen; ... @ManyToMany public Set<Muttersprache> getMuttersprachen(){ return muttersprachen; } Listing 3.12: Annotation-Mapping einer m:n-Beziehung 3.8 Vererbung 3.8.1 Übersicht Vererbung ist ein Grundelement jeder objektorientierten Programmiersprache. Wie im Kapitel Objekt-Relationaler-Bruch beschrieben, gibt es das Konzept der Vererbung bei relationalen Datenbanken nicht. Hibernate unterstützt Vererbung bereits ab der allerersten Version. Bei JPA wurde dies erst mit der EJB 3.0-Spezifikation hinzugefügt. Bei der Implementierung in JPA hatte Hibernate Vorbildwirkung. Daher unterstützen Hibernate und JPA dieselben Technologien. Zur Abbildung von Vererbungshierarchien auf relationale Datenbanken gibt es folgende drei Strategien: • Tabelle pro Klassenhierarchie • Tabelle pro Subklasse • Tabelle pro konkreter Klasse Diese Strategien sind in JPA mittels der Enumeration InheritanceType deklariert. Diese Enumeration besitzt die Werte SINGLE TABLE, JOINED und TABLE PER CLASS. Mit der @Inheritence-Annotation kann die Vererbungsstrategie festgelegt werden. 3.8 Vererbung 41 @Inheritance(strategy = InheritanceType.JOINED) In diesem Kapitel werden die Konzepte anhand des Mappings mit XML gezeigt. Auf das Annotation-Mapping wird nicht näher eingegangen. Die Konzepte sind bei beiden Varianten gleich. Um die verschiedenen Strategien zu verdeutlichen, wird das Gegenstand-Beispiel herangezogen. Dabei ist die Klasse Gegenstand abstrakt. Sie kapselt die drei Felder id, kurzBez und langBez. Die konkreten Klassen TheoretischerGegenstand und FachpraktischerGegenstand erben von Gegenstand und implementieren so diese drei Eigenschaften. Abbildung 3.9 verdeutlicht diesen Sachverhalt. Abbildung 3.9: Vererbungshierarchie Gegenstand Hierbei handelt es sich jedoch nur um ein theorethisches Beispiel. Tatsächlich existiert diese Vererbungshierarchie im Projekt SAS III nicht. Es ergeben sich nachfolgende Klassen: 1 abstract class Gegenstand { 2 int id; 3 String kurzBez; 4 String langBez; 3.8 Vererbung 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 42 //Konstruktor, Getter und Setter... } class TheoretischerGegenstand extends Gegenstand { String lehrsaal; //Konstruktor, Getter und Setter... } class FachpraktischerGegenstand extends Gegenstand{ String werkstaette; //Konstruktor, Getter und Setter... } Listing 3.13: Implementierung der Vererbungshierarchie Gegenstand 3.8.2 Tabelle pro Klassenhierarchie (SINGLE TABLE) Bei dieser Strategie werden alle Klassen, die innerhalb einer Vererbungslinie liegen, auf eine einzige Tabelle abgebildet. Polymorphie wird mithilfe einer zusätzlichen Spalte implementiert. In dieser steht der Name des konkreten Objektes, welches zur Laufzeit gespeichert wurde. Beim Mapping dieser Strategie sind die Eigenschaften discriminator und subclass von entscheidender Bedeutung. 1 <hibernate-mapping> 2 <class name="Gegenstand" schema="public" table="gegenstand"> 3 <id name="id" type="int"> 4 <column name="id"/> 5 <generator class="sequence"/> 6 </id> 7 8 <discriminator column="gegenstand_type" type="string"/> 9 10 <property name="kurzBez" type="string"> 11 <column name="kurzbez"/> 12 </property> 3.8 Vererbung 13 14 15 16 17 18 19 20 21 22 23 24 43 <property name="langBez" type="string"> <column name="langbez"/> </property> <subclass discriminator-value="Theoretischer Gegenstand" ←name="TheoretischerGegenstand"> <property name="lehrsaal" column="lehrsaal"/> </subclass> <subclass discriminator-value="Fachpraktischer Gegenstand" ←name="FachpraktischerGegenstand"> <property name="werkstaette" column="werkstaette"/> </subclass> </class> </hibernate-mapping> Listing 3.14: Tabelle pro Klassenhierarchie Das discriminator-Element in Zeile 8 gewährleistet Polymorphie. Zu diesem Zweck wird die Spalte gegenstand_type angelegt. Diese Spalte kapselt den Namen des Objekttyps, welcher zur Laufzeit in die Datenbank geschrieben wurde. Die zwei subclass-Tags binden die Klassen der Vererbungshierarchie ein. Das Attribut discriminator-value setzt den Objekttyp-Namen der gegenstand_typeSpalte. Es ergibt sich folgende Tabelle gegenstand. Diese wird zur Illustration mit Beispieldaten gefüllt: id 1 2 gegenstand_type kurzbez Fachpraktischer .. Schweissen Theoretischer .. Maschinen langbez lehrsaal werkstaette Schutzgasschweissen L205 null Maschinenkunde null Trakt 3b Vor- und Nachteile Größter Vorteil dieser Strategie ist die Einfachheit. Auf den ersten Blick ist auch die Performance ein weiterer Vorteil. Jede Abfrage greift auf genau eine Tabelle zu. Es werden keine Joins benötigt. Jedoch wächst diese Tabelle bei größeren Vererbungshierarchien mit. Dabei kann es zu sehr 3.8 Vererbung 44 großen Tabellen mit mehreren Hundert Spalten kommen und das wirkt sich entsprechend negativ auf die Performance aus. Ein weiterer Nachteil dieser Version sind redundante Spalten. Während bei einem FachpraktischenGegenstand die Spalte lehrsaal leer bleibt, ist dies beim TheoretischenGegenstand bei der Spalte werkstaette der Fall. Bei größeren Vererbungshierarchien würde dieses Problem in noch größerem Ausmaß auftreten. Des Weiteren wird durch redundante Spalten die Datenbankintegrität verletzt, da Spalten nullable sein müssen. Sinn macht diese Strategie daher bei kleinen Vererbungshierarchien. 3.8.3 Tabelle pro Subklasse (JOINED) Bei dieser Vererbungsstrategie wird für jede Subklasse, d.h. abstrakte und konkrete Klasse, eine Tabelle verwendet. Die Subklassen der Vererbungshierarchie werden über den <joined-subclass>Tag eingebunden. 1 <hibernate-mapping> 2 <class name="Gegenstand" table="gegenstand"> 3 <id name="id" type="int"> 4 <column name="id"/> 5 <generator class="sequence"/> 6 </id> 7 8 <property name="kurzBez" type="string"> 9 <column name="kurzbez"/> 10 </property> 11 <property name="langBez" type="string"> 12 <column name="langbez"/> 13 </property> 14 15 <joined-subclass name="TheoretischerGegenstand" ←table="theoretischergegenstand"> 16 <key column="gegenstand_id"/> 17 <property name="lehrsaal" column="lehrsaal"/> 18 </joined-subclass> 19 20 <joined-subclass name="FachpraktischerGegenstand" ←table="fachpraktischergegenstand"> 3.8 Vererbung 45 21 <key column="gegenstand_id"/> 22 <property name="werkstaette" column="werkstaette"/> 23 </joined-subclass> 24 </class> 25 </hibernate-mapping> Listing 3.15: Tabelle pro Subklasse Hierbei werden drei Tabellen verwendet. In der gegenstand-Tabelle gibt es den Primärschlüssel id und die beiden Spalten kurzBez und langBez. Also genau jene Eigenschaften der Gegenstand-Klasse. Des Weiteren gibt es die Tabellen theoretischergegenstand und fachpraktischergegenstand, bei welchen der Primärschlüssel zeitgleich Fremdschlüssel auf die Super“-Tabelle Gegenstand ist. ” In diesem Beispiel ist dies die Spalte gegenstand_id. Diese wird über den key-Tag gesetzt. Es ergeben sich somit folgende drei Tabellen: Tabelle gegenstand: id kurzbez 1 Schweissen 2 Maschinen langbez Schutzgasschweissen Maschinenkunde Tabelle fachpraktischergegenstand: gegenstand_id werkstaette 1 Trakt 3b Tabelle theoretischergegenstand: gegenstand_id 2 lehrsaal L205 Vor- und Nachteile Vorteil dieser Strategie ist, dass im Gegensatz zur Strategie Tabelle pro Subklasse, die Datenbankintegrität gewährleistet wird, da keine Attribute nullable sein müssen. 3.8 Vererbung 46 Der Nachteil liegt in der Performance. Die Daten eines Gegenstands liegen verteilt auf mehreren Tabellen. Will man einen bestimmten Gegenstand laden, so wird immer ein inner-join notwendig. Will man eine Abfrage aller Gegenstände durchführen, so wird ein outer-join notwendig. 3.8.4 Tabelle pro konkreter Klasse (TABLE PER CLASS) Hierbei wird für jede konkrete Klasse der Vererbungshierarchie eine Tabelle verwendet. Für Interfaces und abstrakte Klassen gibt es somit keine Datenbanktabelle. In diesem Beispiel werden also zwei Tabellen verwendet: fachpraktischerGegenstand und theoretischerGegenstand. Diese beinhalten jeweils neben ihren eigenen“ auch die geerbten Attribute. In diesem ” Beispiel also id, kurzBez, und langBez. Um die Attribute der Superklassen in die konkrete Klasse einzubinden dient der Tag <union-subclass>-Tag. 1 <hibernate-mapping> 2 <class name="Gegenstand" abstract="true"> 3 <id name="id" type="int"> 4 <column name="geg_id"/> 5 <generator class="sequence"/> 6 </id> 7 8 <property name="kurzBez" type="string"> 9 <column name="geg_kurzbez"/> 10 </property> 11 <property name="langBez" type="string"> 12 <column name="geg_langbez"/> 13 </property> 14 15 <union-subclass name="TheoretischerGegenstand" ←table="theoretischergegenstand"> 16 <property name="lehrsaal" column="lehrsaal"/> 17 </union-subclass> 18 19 <union-subclass name="FachpraktischerGegenstand" ←table="fachpraktischergegenstand"> 20 <property name="werkstaette" column="werkstaette"/> 21 </union-subclass> 3.8 Vererbung 47 22 </class> 23 </hibernate-mapping> Listing 3.16: Tabelle pro konkreter Klasse Beim Forward Engineering werden hier zunächst drei Tabellen angelegt. Neben fachpraktischergegenstand und theoretischergegenstand wird auch eine Tabelle zur abstrakten Klasse Gegenstand erzeugt. Dies ist jedoch nicht erwünscht. Schliesslich wird diese abstrakte“ Tabelle ja auch ” nie verwendet. Daher muss Hibernate gesagt werden, dass die Klasse Gegenstand abstrakt ist. Dazu dient die Eigenschaft abstract="true" im class-Tag. Es ergeben sich somit folgende Tabellen: Tabelle theoretischergegenstand: id 2 kurzbez langbez lehrsaal Maschinen Maschinenkunde L205 Tabelle fachpraktischergegenstand: id kurzbez 1 Schweissen langbez werkstaette Schutzgasschweissen Trakt 3b Vor- und Nachteile Probleme dieser Variante ergeben sich bei polymorphen Abfragen. Eine Abfrage über Gegenstände beliebigen konkreten Typs werden i.d.R. relativ komplex. Des Weiteren ist die Vererbung in der Datenbank nicht mehr ersichtlich. Vorteil dieser Variante ist, dass der Zugriff auf eine bestimmte konkrete Klasse einfach und performant ist. 3.9 Konfiguration 3.9 48 Konfiguration 3.9.1 Übersicht Hibernate ist dafür ausgelegt, in verschiedenen Umgebungen zu arbeiten. Dafür benötigt es Informationen über beispielsweise den verwendeten Datenbanktreiber, den Datenbankdialekt und viele andere. Zu diesem Zweck kann ein Objekt vom Typ org.hibernate.cfg.Configuration instanziert werden, welches diese Informationen kapselt. Es gibt verschiedene Möglichkeiten, dieses Objekt zu initialisieren. Diese werden in den nachfolgenden Kapiteln beschrieben. 3.9.2 hibernate.properties Bei der Erzeugung eines Configuration-Objekts versucht Hibernate, die hibernate.properties-Datei einzulesen. Kann die Datei gefunden werden, wird das Objekt mit den darin gespeicherten Werten initialisiert. Die Datei muss sich zu diesem Zweck in der Wurzel des Classpaths befinden. 1 2 3 4 5 hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect hibernate.connection.driver_class=org.postgresql.Driver hibernate.connection.username=sas hibernate.connection.password=sasii hibernate.connection.url=jdbc:postgresql://localhost:5432/SASDB Listing 3.17: Die Konfigurationsdatei hibernate.properties Eine Zeile dieser Datei wird als Property bezeichnet und entspricht einem java.util.Properties-Objekt. Diese Art der Konfiguration ist dann sinnvoll, wenn man mit genau einer Datenbank arbeitet. Der große Nachteil besteht darin, dass es nicht möglich ist, die Mapping-Dateien in dieser Datei anzugeben. Diese müssen mit entsprechenden Methoden im Java-Code hinzugefügt werden. 3.9 Konfiguration 3.9.3 49 hibernate.cfg.xml Es ist mögich, die gesamte Konfiguration in einer Datei anzugeben. Dies geschieht in Form einer XML-Datei. Das Configuration-Objekt besitzt die Methode configure(). Wird diese Methode aufgerufen, wird das Configuration-Objekt mit den Informationen dieser Datei gefüllt. In diesem Kontext muss die Datei im Hauptverzeichnis der Quelldateien liegen. D.h. die Konfigurationsdatei muss im default-Package liegen. Es ist aber ebenfalls möglich, den Pfad der Konfigurationsdatei als Parameter zu übergeben: configure(File configFile). 1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate ←Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/ ←hibernate-configuration-3.0.dtd"> 3 4 <hibernate-configuration> 5 <session-factory> 6 <property name="hibernate.dialect"> 7 org.hibernate.dialect.PostgreSQLDialect</property> 8 <property name="hibernate.connection.driver_class"> 9 org.postgresql.Driver</property> 10 <property name="hibernate.connection.url"> 11 jdbc:postgresql://localhost:5432/SASDB</property> 12 <property name="hibernate.connection.username">sas</property> 13 <property name="hibernate.connection.password">sas</property> 14 15 <mapping resource="model/Gegenstand.hbm.xml"/> 16 </session-factory> 17 </hibernate-configuration> Listing 3.18: Die Konfigurationsdatei hibernate.cfg.xml Das Wurzelelement dieser XML-Datei ist <hibernate-configuration>. Darunter befindet sich der Tag <session-factory>. In diesem sind die Einstellungen über die Tags <property> und <mapping> realisiert. In diesem Beispiel wird in Zeile 12 die Entität Gegenstand eingebunden. Das Einbinden von Entitäten kann auf verschiedene Weise erfolgen. Insbesondere ist es auch möglich, Entitäten, welche in externen Dateien gemappt werden, in das Projekt einzubinden. 3.9 Konfiguration 50 Der Vorteil dieser Variante ist, dass alle Konfigurationseinstellungen in einer einzigen Datei angegeben werden können. Des Weiteren ist es möglich, mehrere dieser Konfigurationsdateien verwenden zu könnnen. Der Methode configure(File configFile) kann die benötigte Kofigurationsdatei übergeben werden. Dies ist vor allem dann sinnvoll, wenn man mit mehreren Datenbanken arbeitet, oder mit unterschiedlichen Einstellungen auf eine Datenbank zugreifen will. 3.9.4 Zusätzliche Methoden Auch im Java-Quellcode ist es möglich die Konfiguration aufzbauen und zu verändern. Die Klasse Configuration liefert zu diesem Zweck untenstehende Methoden. Alle diese Methoden liefern ein Configuration-Objekt zurück. Hinzufügen bzw. ändern von Properties: • setProperty(String propertyName, String value) Verändert den Property-Eintrag. • addProperties(Properties extraProperties) Fügt Properties hinzu. • setProperties(Properties properties) Entfernt alle bereits existerenden Property-Einträge und ersetzt sie durch die Übergebenen. Die letzten beiden Methoden bekommen ein oder mehrere java.util.PropertiesObjekte übergeben. Wie oben beschrieben entspricht ein solches Objekt einer Zeile in der hibernate.properties bzw. hibernate.cfg.xml. Wie bereits erwähnt ist es in der Datei hibernate.properties nicht möglich MappingDateien einzubinden. Als Ersatz können dafür nachfolgende Methoden verwendet werden: • addFile(String xmlFile) Diese Methode erwartet entweder einen String mit dem Dateinamen oder ein File-Objekt. • addDirectory(File dir) Bindet alle Mapping - Dateien ein, die sich im angegebenen Verzeichnis befinden. 3.9 Konfiguration 51 1 new Configuration(). 2 setProperty("hibernate.connection.url", "jdbc:postgresql ←://10.0.110.1:5432/SASDBSERVER"). 3 addFile("model/Gegenstand.hbm.xml"); Listing 3.19: Setzen der Konfiguration im Quellcode In diesem Beispiel wird in Zeile 1 ein Configuration-Objekt instanziert. Hibernate versucht dieses Objekt standardmäßig mit den Werten der hibernate.properties-Datei zu initialisieren. Mit der Methode setProperty(String propertyName, String value) wird anschließend der Datenbankpfad geändert. Hier wird ersichtlich, dass alle Methoden ein Configuration-Objekt zurückliefern. Im nächsten Schritt wird mit der Methode addFile(String xmlFile) das Gegenstands-Mapping eingebunden. 3.9.5 Konfiguration beim Mapping mit Annotations Wurden die Mapping-Information mittels Annotations implementiert, so wird eine org.hibernate.cfg.AnnotationConfiguration benötigt. Diese Klasse erbt von Configuration. Die Konfiguration erfolgt nach demselben Konzept. Einziger Unterschied besteht beim Einbinden von Entitäten. In der Hibernate-Konfigurationsdatei werden Klassen weiterhin über das mappingElement eingebunden. Jedoch muss dabei der Parameter class mit dem Namen der .java-Datei angegeben werden. <mapping class="Gegenstand.java" /> Im Quellcode können Entitäten mittels der Methode addAnnotatedClass() eingebunden werden. AnnotationConfiguration conf = new AnnotationConfiguration(). addAnnotatedClass(Gegenstand.class).configure(); 3.10 Sessions 3.10 52 Sessions 3.10.1 Übersicht Hibernate-Sessions stellen die Schnittstelle zur Datenbank dar. Sie werden in Objekten vom Typ org.hibernate.Session repräsentiert. Mithilfe dieser Objekte werden alle Datenbankzugriffe realisiert. Intern verwendet eine Hibernate-Session eine org.hibernate.Connection. Connection repräsentiert eine JDBC-Verbindung. Dies wird im Kapitel 3.11 beschrieben. Auf Datenbankseite wird für jede dieser Verbindungen eine Datenbank-Session geöffnet, welche Ressourcen verbraucht. Sessions sollten daher so kurz als möglich offen gehalten werden. Mehr Informationen dazu gibt es im Kapitel 4. Um eine Hibernate-Session zu erzeugen, sind nachstehende Schritte erforderlich: 1. Erzeugen eines Configuration-Objekts Im Kapitel 3.9 wurde beschrieben, wie ein Configuration-Objekt erzeugt werden kann. Configuration configuration = new Configuration().configure(); 2. SessionFactory erzeugen Configuration stellt die Mehode buildSessionFactory() zur Erzeugung einer SessionFactory zu Verfügung. SessionFactory-Objekte sind immutable. Nachträgliche Änderungen an der Konfiguration haben keine Auswirkung auf eine bereits instanzierte SessionFactory. SessionFactory sessionFactory = configuration.buildSessionFactory(); 3. Session erzeugen SessionFactory stellt die Methode openSession() zur Verfügung. Diese gibt ein Session-Objekt zurück. Session session = sessionFactory.openSession(); 3.10 Sessions 53 Das Configuration-Objekt wird nach dem Erzeugen einer SessionFactory nicht mehr benötigt. Es stellt sozusagen ein Wegwerfobjekt“ dar. Sinnvollerweise wird die ” Referenz configuration danach gelöscht, um es bereit für den Garbage-Collector zu machen. Der Umweg“ zum Erzeugen einer Session über die SessionFactory wirkt zunächst ” überflüssig. Ein SessionFactory-Objekt wird unter anderem für das Second-Level-Caching benötigt. Mehr Informationen dazu gibt es im Kapitel 3.14. Des Weiteren macht es Sinn, ein SessionFactory-Objekt in der Applikation zu teilen. Dadurch wird bei jedem Datenbankzugriff Aufwand für die Erzeugung einer Configuration und SessionFactory eingespart. Session hat folgende Methoden zum Laden, Speichern und Löschen von Objekten: Methode save(Object o) update(Object o) saveOrUpdate(Object o) get(Class c, Serializable id) load(Class c, Serializable id) clear() close() createQuery() createSQLQuery flush() delete(Object o) Aufgabe Entität speichern Verändern einer Entität Ruft save(o) oder update(o) auf Laden einer Entität aus der Datenbank Laden einer Entität aus der Datenbank Session-Level-Cache leeren JDBC-Verbindung schließen und Ressourcen freigeben Erzeugen einer Query für ein HQL-Statement Erzeugen einer Query für ein SQL-Statement Änderungen am Session-Level-Cache persistieren Löschen einer Entität Tabelle 3.1: Wichtige Session Methoden Der Unterschied zwischen den Methoden get() und load() liegt in der Behandlung nicht existierender Entitäten. Während get() null zurückliefert wenn die Entität nicht existiert, wirft load() eine Exception. Die Methoden createQuery() bzw. createSQLQuery() werden im Kapitel 3.13 näher erklärt. 3.10 Sessions 3.10.2 54 Session-Level-Cache Eine Hibernate-Session verwendet einen einfachen Cache. Dieser beschränkt sich auf die Lebenszeit der Session, an der er hängt. Dieser Cache wird als Session-LevelCache oder First-Level-Cache bezeichnet. Wird eine Entität verändert, neu erzeugt oder gelöscht, geschehen diese Änderungen zunächst im Session-Level-Cache. Dadurch wird die Anzahl der Datenbankzugriffe reduziert. Hibernate verwaltet diesen Cache selbstständig, sodass sich AnwendungsentwicklerInnen kaum darum kümmern müssen. Werden Objekte manipuliert, geschieht dies zunächst im Session-Level-Cache. Wird ein Objekt aus der Datenbank geladen, so steht dieses Objekt im Cache. Bei jedem Lesevorgang aus der Datenbank wird zuvor geprüft, ob es dieses Objekt bereits im Cache gibt. Sollte dies der Fall sein, ist kein zusätzlicher Datenbankzugiff nötig. Wie bereits erwähnt, ist der Session-Level-Cache an eine Session gebunden. Um den Cache zu leeren dient die Methode session.clear(). Soll ein bestimmtes Objekt vom Cache gelöst werden, gibt es session.evict(Object o). Mit session.flush() wird der Zustand der Entitäten am Cache auf die Datenbank geschrieben. 3.10.3 Lebenszyklus von Entitäten Der Zustand einer Hibernate-Entität ist nicht immer synchron mit dem Zustand in der Datenbank. Wie bereits erwähnt, werden Entitäten an die Session gebunden, welche diese Objekte wiederum im Session-Level-Cache verwaltet. Die Session verwaltet zusätzlich den Zustand der Objekte. Es gibt drei mögliche Zustände, in denen sich eine Hibernate-Entität befinden kann: • Transient Es existiert (noch) keine Datenbankrepräsentation der Entität. Die Entität ist an keine Session gebunden. • Persistent Die Entität ist an eine Session gebunden und hat einen Primärschlüssel. Änderungen der Entität wirken sich auf den Session-Level-Cache aus. Erst bei einem flush des Caches wird die Entität auf die Datenbank geschrieben. Daher kann der Zustand einer persistenten Entität vom Datenbankzustand abweichen. 3.11 Connections 55 • Detached Die Entität wird von der Session losgelöst (engl. detached). Da die Entität vom Zustand Persistent in den Zustand Detached übergegangen ist, besitzt sie einen Primärschlüssel. Abbildung 3.10: Lebenszyklus von Entitaeten 3.11 Connections Wie im vorangegangenen Kapitel beschrieben, verwendet eine Hibernate-Session eine org.hibernate.Connection. Die Erzeugung eines Connection-Objekts ist sehr zeitaufwändig. Zu diesem Zweck gibt es die Möglichkeit, Connections auf Vorrat anzulegen. Dies ist mithilfe eines Connection-Pools möglich. 3.11.1 Connection-Pool Wird eine Connection gebraucht, wird sie aus diesem Pool genommen. Benutzte Connections werden wieder zurückgelegt. Hibernate bietet selbst eine Connection-Pool-Implementierung an. Dieser Pool wird per-Default von Hibernate-Applikationen verwendet. Die Größe des Pools wird standardmäßig mit 20 initialisiert. Sie kann mittels der Property 3.11 Connections 56 hibernate.connection.pool\_size verändert werden. Der Hibernate-Connection-Pool ist nur für Testzwecke konzipiert. Er ist unperformant und nicht weit entwickelt. Deshalb sollte er im laufenden Betrieb einer Applikation nicht eingesetzt werden. Eine wesentlich bessere Implementierung dieser Technologie wird bereits mit dem Hibernate-Core mitgeliefert. Dabei handelt es sich um den open-source ConnectionPool c3p0. Dieser ist sehr einfach zu konfigurieren. In der Hibernate-Konfugiration muss lediglich eine hibernate.c3p0-Property definiert werden. Z.B. hibernate.c3p0.max_size=20. Der c3p0-Pool bietet eine Fülle an Konfigurationsmöglichkeiten. Die Verwendung eines solchen Connection-Pools ist für viele Anwendungen bereits ausreichend. 3.11.2 JNDI Läuft die Anwendung in einem Application-Server, ist es in den meisten Fällen besser, die Connections über JNDI6 zu beziehen. JNDI ist Teil der Java Plattform und stellt mehrere Namens- und Verzeichnisdienste für Applikationen zur Verfügung. Ein Application-Server arbeitet in einem Computernetwerk. Auf ihm werden Client/ServerAnwendungen ausgeführt. Dies kann in einem internen Netz (Lan), sowie im Internet geschehen. Bei einem Webserver stellt der Browser den Client dar. Um Connections über JNDI vom Anwenungsserver zu beziehen, dient javax.sql.Datasource. Nachfolgende Tabelle zeigt wichtige Konigurations-Properties: Name der Property hibernate.connection.datasource hibernate.jndi.url hibernate.jndi.class Zweck Name der Datasource JNDI URL des JNDI-Providers Klasse der JNDI-InitialContextFactory Tabelle 3.2: Konfiguration der JNDI-Datasource 6 Java Naming and Directory Interface 3.12 Transaktionen 3.12 57 Transaktionen 3.12.1 Übersicht Um Änderungen, die auf dem Session-Level-Cache durchgeführt wurden auch auf die Datenbank zu übertragen, dienen Transaktionen. Eine Transaktion bezeichnet die Summe mehrerer logischer Operationen auf der Datenbank. Transaktionen folgen dem ACID-Prinzip: • Atomicity (Atomarität) Transaktionen werden entweder ganz, oder gar nicht durchgeführt. Sie sind unteilbar. • Consistency (Konsistenz) Der Datenbestand ist vor und nach der Transaktion konsistent. • Isolation (Isolation) Gleichzeitig ausgeführte Transaktionen dürfen sich gegenseitig nicht stören. • Durability (Dauerhauftigkeit) Änderungen einer Transaktion an den Daten müssen dauerhaft bestehen bleiben. Geschachtelte Transaktionen sind daher abzulehnen. Bei einem rollback einer übergeordneten Transaktion würde auch die Subtransaktion zurückgerollt. Dies stellt einen Verstoß gegen dieses Prinzip dar. Session stellt die Methode beginTransaction() bereit. Dadurch wird ein Objekt vom Typ org.hibernate.Transaction instanziert. Dies stellt gleichzeitig den Beginn der Transaktion dar. Geschachtelte Transaktionen werden nicht unterstützt. Transaktionen werden mittels folgender Methoden beendet: • commit() Ruft die Methode flush() der Session auf und schreibt so die Änderungen in die Datenbank. Danach wird ein Datenbank-Commit durchgeführt. • rollback() Bei einem Fehlerfall wird die Methode rollback() verwendet. Diese macht alle Änderungen, die in der Transaktion durchgeführt wurden, rückgängig. 3.12 Transaktionen 58 Im nachstehenden Beispiel wird zunächst ein Gegenstand-Objekt geladen, anschließend verändert und die Änderungen auf die Datenbank geschrieben. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package beans; import import import import import model.Gegenstand; org.hibernate.HibernateException; org.hibernate.Session; org.hibernate.Transaction; org.hibernate.cfg.Configuration; public class GegenstandBean { public void changeGegenstand(int pk, String kurzBezNeu) { Gegenstand aktGegenstand; Session session; Transaction transaction; 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 } session = new Configuration().configure(). buildSessionFactory().openSession(); transaction = session.beginTransaction(); ←- try { aktGegenstand = (Gegenstand) session.load(Gegenstand.class, pk); aktGegenstand.setGegKurzbez(kurzBezNeu); transaction.commit(); } catch (HibernateException he) { transaction.rollback(); //Weitere Fehlerbehandlung } finally { session.close(); } } Listing 3.20: Session und Transaktionen In Zeile 16 wird eine neue Session erzeugt. Diese öffnet in Zeile 17 eine Transaktion. Im try-Block wird dann mittels load() ein Gegenstands-Objekt mit dem PrimaryKey pk in den First-Level-Cache des session-Objekts geladen. Sollte der Schlüssel pk in der Datenbank nicht existieren, wird eine HibernateException geworfen. Anschließend wird der Gegenstand verändert und in Zeile 22 wird diese Änderung in die Datenbank geschrieben. Im finally-Block wird die Session geschlossen. Der finally-Block wird, unabhängig 3.12 Transaktionen 59 davon, ob eine Exception auftritt, abgearbeitet. Eine geöffnete Session sollte auf jeden Fall wieder geschlossen werden. Deshalb wird die in Listing 3.20 gezeigte Form der Implementierung vorgeschlagen. Nach dem Aufruf von commit() oder rollback() kann die Transaktion mittels begin() wiederverwendet werden. Standardmäßig verwendet Hibernate intern eine JDBCTransaction. Alternativ kann auch JTA7 verwendet werden. JTA erlaubt es, den Transaktionsmechanismus eines Anwendungsservers zu verwenden. 3.12.2 Konkurrierende Datenbankzugriffe Laufen mehrere Transaktionen zeitgleich nebeneinander, kann es zu Fehlern kommen. Hierbei sind zwei Fehlertypen zu unterscheiden. Einerseits sind das Transaktionen, die sich bei paralleler Ausführung gegenseitig stören. Im speziellen betrifft dies beispielsweise das unkommitierte Lesen von Änderungen. Andererseits logische Fehler, bei denen Transaktionen Änderungen verursachen, die bereits festgeschriebene Daten ungewollt überschreiben. Insgesamt können nachstehende 4 Fehler auftreten: • Lost Update • Dirty Read • Nonrepeatable Read • Phantom Read Der Lost Update stellt einen logischen Datenhaltungsfehler dar. Dies wird im Kapitel 3.12.4 behandelt. Die drei weiteren Fehler Dirty Read, Nonrepeatable-Read und Phantom Read betreffen die Transaktionsisolation. 7 Java Transaction API 3.12 Transaktionen 60 Lost Update Zwei Benutzer lesen zur selben Zeit den selben Datensatz aus. Die dafür nötigen beiden Transaktionen sehen dabei einander nicht. Nach der Bearbeitungszeit schreibt ein Benutzer zuerst in die Datenbank und kommitiert diese Änderungen. Kurz darauf kommitiert auch der zweite Benutzer und überschreibt somit die Änderungen des ersten Benutzers. Somit geht das Update verloren (engl. lost). Benutzer A Liest Datensatz 0815 Formular zum Bearbeiten des Datensatzes 0815 wird am Bildschirm angezeigt Schreibt Datensatz 0815 geändert in die Datenbank Benutzer B Liest Datensatz 0815 Formular zum Bearbeiten des Datensatzes 0815 wird am Bildschirm angezeigt Schreibt Datensatz 0815 geändert in die Datenbank und überschreibt somit die Änderungen von Benutzer A Tabelle 3.3: Logisches Lost Update Problem Dirty Read Tritt dann auf, wenn eine Transaktion Daten liest, die von einer anderen Transaktion geschrieben, jedoch nicht kommitiert wurden. Wird nach dem Lesen ein rollback der anderen Transaktion durchgeführt, wird mit schmutzigen (engl. dirty) Daten gearbeitet. Transaktion A Transaktion B Schreibt Datensatz 0815 in die Datenbank, kommitiert jedoch nicht Liest den unkommitierten Datensatz 0815 Transaktion wird mit Rollback zurückgerollt Transaktion B arbeitet mit einem schmutzigen Wert weiter Tabelle 3.4: Dirty Read 3.12 Transaktionen 61 Nonrepeatable Read Mehrere Lesevorgänge auf denselben Datensatz innerhalb einer Transaktion liefern unterschiedliche Werte. Der Lesevorgang ist nicht wiederholbar (engl. not repeatable). Transaktion A Liest Datensatz 0815 Transaktion B Verändert Datensatz 0815 Kommitiert die Änderung Liest Datensatz 0815 und erhält einen anderen Datensatz als beim ersten Lesevorgang Tabelle 3.5: Nonrepeatable Read Phantom Read Dieses Problem ist eine Besonderheit des Nonrepeatable-Read. Der Fehler tritt bei einer Menge von Datensätzen in Zusammenhang mit Aggregatfunktionen auf. Transaktion A Transaktion B Liest die Summe aller Gegenstände, die auf Deutsch unterrichtet werden. Ergebnis: 44. Verändert den Gegenstand Geschichte. Dieser wird nun in Englisch unterrichtet. Kommitiert die Änderung. Liest wieder die Summe aller Gegenstände, die auf Deutsch unterrichtet werden und erhält den Wert 43. Tabelle 3.6: Phantom Read 3.12.3 Transaktionsisolation Um den Problemen konkurrierender Transaktionen zu begegnen, gibt es auf Seiten der Datenbank den Isolationsgrad. 3.12 Transaktionen 62 Hibernate bietet hier die im ANSI-SQL92 Standard festgeschriebenen Isolationslevel an: • Read Uncommitted Bei diesem Isolationslevel findet praktisch keine Isolation statt. Bearbeitet eine Transaktion einen bestimmten Datensatz, sind diese Änderungen sofort für andere Transaktionen sichtbar. Auch wenn diese nicht kommitiert wurden. • Read Committed Änderungen einer Transaktion werden erst nach dem Kommitieren für andere Transaktionen sichtbar. Dieser Isolationslevel ist Standardverhalten vieler RDBMS, da er einen guten Kompromiss zwischen Sicherheit und Performance darstellt. • Repeatable Read Diese Strategie stellt sicher, dass mehrere Lesevorgänge auf denselben Datensatz innerhalb der Transaktion jeweils das selbe Ergebnis liefern. Das bedeutet, die Transaktion arbeitet mit jenem Datenbestand, der zu Beginn vorhanden war. • Serializable Der höchste Isolationsgrad. Parallele Transaktionen laufen vollkommen isoliert voneinander. Vereinfacht gesagt, wird vorgetäuscht, dass alle Transaktionen sequentiell hintereinander abgearbeitet werden. Aus Performancegründen ist dies bei den meisten RDBMS nicht der Fall. Meist werden am Beginn einer Transaktionen alle betroffenen Tabellen gesperrt. Bei dieser Strategie kommmt es relativ häufig zu Abbrüchen von Transaktionen. Die Applikation muss gegebenfalls auf diese reagieren können und die Transaktion neu starten. Folgende Tabelle gibt eine Übersicht über die Möglichkeit von Datenbankfehlern bei Wahl des jeweiligen Isolationsgrades. Isolationsgrad Dirty Read Read Uncommitted Read Committed Repeatable Read Serializable möglich unmöglich unmöglich unmöglich Nonrepeatable Read möglich möglich unmöglich unmöglich Phantom Read möglich möglich möglich unmöglich Tabelle 3.7: Fehlerfaelle der Isolationsgrade Allgemein gilt, je höher der Isolationslevel, desto niedriger die Performance und umgekehrt. 3.12 Transaktionen 63 Welcher Isolationslevel gewählt wird, hängt in erster Linie von den Anforderungen der Applikation ab. Mittels der Property hibernate.connection.Isolation kann der Transaktionsisolationsgrad bestimmt werden. In der Klasse java.sql.Connection stehen diese Strategien in Form einer Enumeration bereit. new Configuration().configure(). setProperty("hibernate.connection.isolation", java.sql.Connection.TRANSACTION_REPEATABLE_READ) Die Isolationslevel werden je nach verwendetem Datenbankmanagementsystem auf dessen Isolationsmechanismus umgelegt. Es kann jedoch sein, dass das Datenbanksystem nicht alle Isolationslevel unterstützt oder andere verwendet. Beispielsweise wird bei Oracle das Read Uncommited und Repeatable Read nicht unterstützt, dafür zusätzlich Read Only angeboten. Daher ist es wichtig, sich mit den Konzepten der Transaktionsisolation der verwendeten Datenbank auseinanderzusetzen. 3.12.4 Sperrstrategien Im Kapitel 3.12 wurde beschrieben, wie die technischen Datenbankfehler gehandhabt werden. Dieses Kapitel bezieht sich auf die logischen Fehler. Weiter oben wurde das Lost Update Problem erklärt. Grundsätzlich wäre es möglich, dieses Problem durch Lange Transaktionen zu umgehen. Bei langen Transaktionen wird das Laden und Speichern eines Datensatzes in einer Transaktion durchgeführt. Durch entsprechend hohe Transaktionsisolation würde das Problem nicht mehr auftreten. Jedoch kann keine Aussage darüber gemacht werden, wie lange BenutzerInnen zum Manipulieren der Daten brauchen. Lange Transaktionen wirken sich negativ auf die Performance aus. Deshalb ist diese Strategie abzulehnen. Standardmäßig arbeitet Hibernate nach dem Prinzip Wer zuerst kommt, malt zuerst, ” und wird zuerst überschrieben“. Spätere Updates übeschreiben frühere. Hier wird ersichtlich, das Hibernate ohne entsprechende Konfiguration nichts gegen den Lost Update unternimmt. 3.12 Transaktionen 64 Zu diesem Zweck gibt es Sperren (engl. Locks). Dabei gibt es optimistische und pessimistische Sperrstrategien. Pessimistische Sperrstrategie Bei dieser Strategie wird von häufig auftretenden Kollisionen ausgegangen. Hier wird auf Ebene von Datensatz bzw. Objekt gesperrt, indem sogenannte Locks gesetzt werden. Benutzer A, der den Datensatz bearbeiten will, setzt die Sperre und gibt sie nach Abschluss seiner Arbeiten wieder frei. Sollte Benutzer B innerhalb dieser Bearbeitungszeit ebenfalls auf diesen Datensatz zugreifen, kann er ihn zwar lesen, wird jedoch darüber informiert, dass es nicht möglich ist, den Datensatz zu verändern, bis Benutzer A die Sperre wieder freigibt. Vorteil dieser Strategie ist, dass keine Arbeit verloren geht. Da jeder Benutzer über etwaige Sperren informiert wird. Nachteilig ist der Umstand, dass z.B. bei einem Systemabsturz von Benutzer A die Sperre weiterhin gehalten wird und Benutzer B mitunter lange darauf warten muss, bis er die Sperre erhält. Hibernate erlaubt folgende Lock-Modi: • LockMode.NONE • LockMode.READ • LockMode.UPGRADE • LockMode.UPGRADE_NOWAIT • LockMode.WRITE • LockMode.FORCE Alle Lock-Modi bleiben nur innerhalb einer Transaktion in Kraft. Häufig ist es jedoch nötig, länger andauernde Sperren zu halten. Eine mögliche Variante dies zu bewerkstelligen ist, in den entsprechenden Tabelle zusätzliche Spalten anzulegen, welche Informationen zu etwaigen Sperren beinhalten. 3.13 Datenbankabfragen 65 Hibernate setzt die pessimistische Sperrstrategie über Datenbankmechanismen wie beispielsweise SELECT FOR UPDATE um. Optimistische Sperrstrategie Diese Strategie ist sinnvoll, wenn Kollisionen selten auftreten. Es wird gänzlich ohne Locks gearbeitet. BenutzerInnen können zu jeder Zeit Daten einladen. Dabei wird man nicht darüber infomiert, ob jemand Anderer ebenfalls darauf zugreift. Erst beim Speichern wird dieser Umstand geprüft. Zu diesem Zweck wird beim Einlesen die Versionsnummer des Datensatzes mitgelesen. Beim Speichern wird diese dann überprüft. Sollte sich diese Zahl verändert haben, wird der Benutzer darüber informiert und der Speichervorgang wird abgebrochen. Nachteil dieser Strategie ist, dass Arbeit verloren gehen kann. Vorteil ist die bessere Performance als bei der pessimistischen Sperrstrategie. Bei der Implementierung dieser Strategie, muss die Versionsnummer eine eigene Instanzvariable der Entität sein. Dabei muss es sich um eine Integer-Instanz handeln. Im Mapping muss dieses Attribut mittels des <version>-Tags deklariert werden. 3.13 Datenbankabfragen 3.13.1 Übersicht Neben den vordefinierten Zugriffsmethoden von Sessions können auch selbst formulierte Statements an die Datenbank gesendet werden. Dies ist nötig, wenn Entitäten nach bestimmten Mustern gesucht sind. Im Speziellen dann, wenn der Primärschlüssel einer Entität nicht bekannt ist. Dies kann in Form von HQL- und normalen SQL Statements, sowie mithilfe von Criterias erfolgen. 3.13 Datenbankabfragen 3.13.2 66 Hibernate Qery Language (HQL) HQL ist eine objektorientierte Zugriffssprache, welche stark an die Syntax von normalen SQL-Statements angelehnt ist. HQL-Statements sind ausschließlich für Entitäten (d.h. gemappte Java-Klassen) anwendbar, welche in der Configuration eingebunden sind. Die Schlüsselwörter wie beispielsweise from oder insert sind nicht Case-Sensitiv. Bei SQL-Statements hat es sich etabliert, Schlüsselwörter groß zu schreiben. Da Tabellenund Spaltenbezeichnungen auf Datenbankseite meist klein geschrieben werden bringt dies einen großen Vorteil in der Lesbarkeit. HQL-Statements werden mit den Bezeichnungen der Klassen und deren Instanzvariablen formuliert. Bezeichnungen dieser Klassen bzw. Variablen werden nach dem JavaBean-Standard vergeben. Dadurch beginnt der Name von Java-Klassen mit einem Großbuchstaben. Deshalb bietet es sich eher an, HQL-Statements mit kleingeschriebenen Schlüsselwörtern zu formulieren. Die Bezeichnungen der Variablen und Klassen sind Case-Sensitiv. Wie im Kapitel 3.10 beschrieben, besitzt ein Objekt vom Typ Session die Methode createQuery(String hql). Diese Methode liefert eine org.hibernate.Query zurück. Query q = session.createQuery("from Gegenstand"); Mit einer Query ist es möglich, HQL-Statements auszuführen. Abfragen Zum Ausführen einer Abfrage dienen folgende Methoden: • list() • iterator() • scroll() 3.13 Datenbankabfragen 67 • uniqueResult() Am häufigsten werden die Methoden list() und uniqueResult() verwendet. list() liefert eine java.util.List mit den Ergebnissen der Suchanfrage. uniqueResult() wird eingesetzt, wenn die Abfrage genau einen Datensatz liefern soll. Sollte kein Datensatz den Suchkriterien entsprechen, wird null zurückgegeben. Wird mehr als ein Datensatz gefunden, wird eine HibernateException geworfen. In einem HQL-Statement können wie oben erwähnt nur die Bezeichnungen der JavaKlassen und deren Instanzvariablen verwendet werden. Diese Java-Klassen müssen gemappt sein. Es ist egal, ob man den voll qualifizierten Namen oder nur den Klassennamen angibt. from Gegenstand ist gleichbedeutend mit from model.Gegenstand Diese beiden Abfragen liefern alle Datensätze der Tabelle gegenstand. Mit as kann innerhalb der Abfrage ein anderer Name vergeben werden. from Gegenstand as geg Das Schlüsselwort as ist optional. from Gegenstand geg Will man die Abfrage einschränken, gibt es äuquivalent zu normalem SQL die whereKlausel. from Gegenstand geg where geg.gegLangbez = ’Projektentwicklung’ Abfragen können mit order by sortiert und mit group by gruppiert werden. Diese beiden Mechanismen entsprechen exakt jenen von SQL. 3.13 Datenbankabfragen 68 Query q = session.createQuery("‘ from Gegenstand geg group by geg.muttersprache order by geg.gegKurzbez asc"’); Die optionalen Parameter asc und desc beim order-by geben die Sortierreihenfolge an. Durch group by wird bei Ausführung dieses Statements eine List<Object[]> zurückgeliefert. List<Object[]> gruppiert = q.list(); Select-Abfragen Mithilfe des Schlüsselwortes select kann das Ergebnis einer Abfrage bestimmt werden. Es kann angegeben werden, welche Objekte oder Attribute zurückgeliefert werden sollen. select gegId, gegMspId from Gegenstand Es ist auch möglich, typsichere Java-Objekte als Ergebnistyp zu definieren. select new GegenstandKlein(gegId, gegKurzBez, gegMspId) from Gegenstand Im obigen Beispiel muss es eine Klasse geben, welche einen Konstruktor mit dieser Schnittstelle besitzt. Polymorphe-Abfragen Wie im Kapitel 3.8 gezeigt, ist es möglich, Vererbungshierarchien zu mappen. Mithilfe von HQL sind polymorphe Abfragen möglich. from Gegenstand 3.13 Datenbankabfragen 69 Diese Abfrage liefert nicht nur Gegenstand-Objekte, sondern auch Objekte der Subklassen. Bei der im Kapitel 3.8 gezeigten Vererbungshirarchie werden so auch Instanzen von FachpraktischerGegenstand und TheoretischerGegenstand geliefert. from java.lang.Object Diese Abfrage liefert als Ergebnis alle Entitäten und somit jeden Datensatz der Datenbank. HQL-Expressions Folgende Tabelle gibt eine Übersicht über wichtige HQL-Expressions. Gruppe Mathematische Operatoren Vergleichsoperatoren Logische Operatoren Abfrageoperatoren Stringmanipulation Zeit und Datum Umgang mit Collections Syntax +, -, *, /, abs(), sqrt(), bit_length(), mod() =, >=, <=, <>, !=, like and, or, not in, not in, between, is null, is not null, is empty, is not empty, member of, not member of concat(), substring(), str() trim(), lower(), upper(), length(), locate(), abs(), sqrt(), bit_length(), mod() current_date(), current_time(), current_timestamp(), second(), minute(), hour(), day(), month(), year() size(), min_element(), max_element(), minindex(), maxindex() Tabelle 3.8: Wichtige HQL Expressions 3.13 Datenbankabfragen 70 In-Parameter In-Parameter können wie nachfolgend angegeben eingefügt werden: • ? Diese Form ist ähnlich der JDBC-Schreibweise. Einziger Unterschied besteht in der Nummerierung der Parameter. JDBC beginnt bei der Nummerierung mit 1. Hibernate hingegen fängt bei 0 an. Query q = session.createQuery("from Gegenstand geg where geg.gegLangbez = ?"); • :name Doppelpunkt gefolgt vom Namen des Parameters. Dadurch kann das Statement übersichtlicher gestaltet werden. Des Weiteren entfällt die umständliche Nummerierung. Query q = session.createQuery("from Gegenstand geg where geg.gegLangbez = :gegenstandsname"); Zum Setzen der Parameter gibt es Methoden für die entsprechenden Datentypen. Hibernate liefert zwei überladene Methoden zu jedem Datentyp: Eine davon bekommt an erster Stelle einen Integer übergeben. Damit werden die Parameter, welche in Form eines Fragezeichens angegeben wurden, gesetzt. q.setString(0, "Programmieren"); Die zweite bekommt einen String übergeben, der den Namen des Parameters enthält. Der Doppelpunkt wird nicht angegeben. q.setString("gegenstandsname", "Programmieren"); In HQL-Statements ist das Vergleichen ganzer Entitäten möglich. Dazu dient die Methode setEntitiy. Query q = session.createQuery("from Gegenstand geg where geg.mutterprache = :muttersprache"); q.setEntity("muttersprache", muttersprache); 3.13 Datenbankabfragen 71 Im Kapitel Objekt-Relationaler Bruch wurde beschrieben, dass Tabellen keine Datentypen sind. Deshalb ist es in der relationalen Welt auch nicht möglich, einen Vergleich zweier Tabellen in dieser Form durchzuführen. Hier muss auf den Primärschlüssel der Tabelle geprüft werden. Hibernate setzt die objektorientierte Denkweise von HQL-Statements auf die relationale Denkweise um. Im Speziellen heißt das, dass in der oben gezeigten where-Klausel Fremd- und Primärschlüssel verglichen werden. Anhand dieses Beispiels wird die arbeitsweise des objektorientierten HQL ersichtlich. Joins Hibernate unterstützt die in ANSI8 -SQL 92 standardisierten Join-Typen. Diese sind in Abbildung 3.11 ersichtlich. Abbildung 3.11: HQL - Joins Verknüpfungen (engl. joins) werden großteils implizit verwendet. Es sind aber auch explizite Joins möglich. Hibernate liefert bei einem expliziten Join immer Objektfelder, welche die beteiligten Entitäten enthalten. Dabei wird die Reihenfolge der Objekte in einem Objektfeld von 8 American National Standards Institute 3.13 Datenbankabfragen 72 der Abfolge der Entitäten im HQL-Statement bestimmt. Folgendes Listing zeigt einen expliziten join. Es werden alle Gegenstände ausgegeben, die in der Sprache Deutsch oder Englisch unterrichtet werden. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Query q = session.createQuery( "from Gegenstand geg " + "inner join geg.muttersprache Muttersprache " + "with geg.muttersprache.mspLangbez = :sprache1 " + "or geg.muttersprache.mspLangbez = :sprache2"); q.setString("sprache1", "Deutsch"); q.setString("sprache2", "Englisch"); List<Object[]> l = q.list(); for (Object [] obj : l) { Gegenstand geg = (Gegenstand) obj[0]; Muttersprache msp = (Muttersprache) obj[1]; System.out.println(geg.getGegLangbez() + " - " + msp.getMspLangbez()); } Listing 3.21: Expliziter HQL-Join Die In-Parameter werden in den Zeilen 7 und 8 gesetzt. In diesem Beispiel wird in Zeile 9 eine Liste von Objektfeldern erzeugt. An erster Stelle (Index 0) eines Objektfeldes wird jeweils ein Gegenstand-Objekt geliefert und an zweiter Stelle (Index 1) ein Muttersprache-Objekt. Beim impliziten Join wird das Schlüsselwort join nicht verwendet. Stattdessen wird mithilfe des Punktoperators auf die jeweilige Entität zugegriffen. Dabei muss im GegenstandMapping eine Beziehung zur Muttersprache vorhanden sein. Beim impliziten Join verwendet Hibernate den inner join. from Gegenstand geg where geg.muttersprache.mspLangbez = ’Englisch’ 3.13 Datenbankabfragen 73 Updates und Deletes Seit Hibernate 3 ist es möglich, mithilfe von HQL Objekte in der Datenbank zu manipulieren. • Update update Gegenstand geg set geg.gegLangbez = ’Englisch’ where geg.gegId = 3 Wie in Kapitel 3.12.2 beschrieben, wird für die optimistische Sperrstrategie eine Versionsnummer benötigt. Mithilfe der Schlüsselwörter update versioned wird die Versionsnummer hochgezählt. • Delete delete Gegenstand where geg.gegId = 3 Die gelöschten bzw. veränderten Objekte müssen vorher nicht geladen werden. Änderungen werden mittels der Methode executeUpdate() durchgeführt. Diese Methode liefert die Anzahl der veränderten Datensätze zurück. 3.13.3 Criteria Abfragen können neben HQL auch mittels Criteria formuliert werden. Diese Abfragen richten sich an genau einen Entitätstypen. Der Vorteil dieser Technik ist, dass keine HQL-Kenntnisse benötigt werden. Des Weiteren wird Typsicherheit garantiert. Jedoch ist es nicht möglich, Joins zu verwenden. Um Abfragen zu formulieren dient ein Objekt vom Typ org.hibernate.Criteria. Zum Erzeugen eines Criteria-Objekts liefert Session die Methode createCriteria(). Dieser Methode wird die gewünschte Klasse übergeben. Ausgeführt wird eine Criteria mit äuqivalenten Methoden zu HQL. Einzig die Methode iterate gibt es nicht. Criteria criteria = s.createCriteria(Gegenstand.class); List<Gegenstand> gegenstaende = criteria.list(); 3.13 Datenbankabfragen 74 Diese Abfrage liefert alle Entitäten der Gegenstand-Tabelle. Um Abfragen einzuschränken gibt es die Klasse org.hibernate.criterion.Restrictions. Diese Einschränkungen (engl. Restrictions) können zu einer Criteria mittels der Methode add(Criterion c) hinzugefügt werden. Die Klasse Restrictions liefert dazu eine Vielzahl an statischen Methoden, welche alle ein Criterion-Objekt zurückliefern. Mithilfe der Methode setMaxResults(int maxResults) ist es möglich, die Anzahl der zurückgelieferten Objekte einzuschränken. criteria.add(Restrictions.between("gegId", 1, 22)); criteria.setMaxResults(10); Die Methode add liefert immer ein Criteria-Objekt zurück. Dadurch kann dieser Ausdruck wesentlich übersichtlicher gestaltet werden. Des Weiteren ist es seit Java 5 möglich, statische Imports zu verwenden. Dadurch können die Methoden der Klasse Restrictions, welche ja alle statisch sind, nach dem Einbinden ohne Angabe des Klassennamens verwendet werden. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import import import import java.util.List; org.hibernate.Session; org.hibernate.cfg.Configuration; static org.hibernate.criterion.Restrictions.*; public class GegenstandBean(){ public void getSomeGegenstaende(){ Session session = new Configuration().configure() .buildSessionFactory().openSession(); List<Gegenstand> gegenstaende = session.createCriteria(Gegenstand.class) .add(between("gegId", 1, 22)) .add(isNotEmpty("gegNotenattrArt")) .setMaxResults(10) .list(); return gegenstaende; } } Listing 3.22: Datenbankabfrage mit Criteria 3.14 Fetching und Caching 3.13.4 75 SQL Es ist auch möglich, normale SQL-Statements im jeweils verwendeten Datenbankdialekt zu formulieren. Das kann unter anderem dann sinnvoll sein, wenn datenbankoptimierte Abfragen definiert werden müssen. Um ein SQL-Statement zu benutzen, dient das Interface SQLQuery, welches mit dem Aufruf der Methode createSQLQuery() an einer Session erzeugt wird. s.createSQLQuery("SELECT * FROM Gegenstand").list(); Zum Ausführen einer SQLQuery gibt es wie in HQL die äuqivalenten Methoden list(), scroll(), iterate() und uniqueResult(). Bei obenstehender SQLQuery wird eine List<Object[]> zurückgeliefert. 3.14 Fetching und Caching 3.14.1 Übersicht Datenbankzugriffe stellen in vielen Applikationen den Flaschenhals hinsichtlich Performance dar. Insbesondere ist dies bei Webapplikationen der Fall. Anforderung an die Applikation ist es, die Datenbankzugriffe auf ein Minimum zu beschränken und wirklich nur jene Daten zu laden, welche auch tatsächlich benötigt werden. Hibernate stellt zu diesem Zweck verschiedene Mechanismen zur Verfügung. Fetching Dieser Begriff bezieht sich darauf, wann und wieviel Daten zu einem Zeitpunkt geladen werden sollen. Grundsätzlich gibt es zwei Strategien: Lazy Loading und Eager Loading. Caching Dabei geht es um das Puffern bestimmter Datensätze. Wird ein gesuchter Datensatz im Cache gefunden, muss keine Datenbankabfrage ausgeführt werden. 3.14 Fetching und Caching 3.14.2 76 Lazy Loading Hibernate setzt standardmäßig das faule Laden (engl. lazy loading) um. Hat eine Entität Beziehungen zu anderen Entitäten, werden diese erst dann geladen, wenn sie tatsächlich benötigt werden. Hibernate verwendet dazu Proxies, mit denen die referenzierten Objekte bis zum tatsächlichen Einladen dargestellt werden. Diese Proxies sind Stellvertreterobjekte und haben dieselbe Schnittstelle wie das eigentliche Objekt. Wird schließlich auf das Objekt zugegriffen, werden diese Proxies durch echte Objekte aus der Datenbank ersetzt. Dabei ist zu beachten, dass beim Zugriff auf den Primärschlüssel nicht nachgeladen wird. Erst beim Zugriff auf eine Nicht-Primärschlüssel-Spalte wird nachgeladen. Zur Illustrierung referenziert die Entität Gegenstand die Entität Muttersprache. Diese Assoziation entspricht, wie im Kapitel 3.7 gezeigt, dem many-to-one-Beziehungstyp. Folgendes Listing verdeutlicht das Lazy Loading anhand dieses Beispiels. 1 2 3 4 5 6 7 8 Configuration c = new Configuration().configure(); SessionFactory sf = c.getSessionFactory(); Session session = sf.openSession(); Gegenstand geg = session.load(Gegenstand.class, 5); int i = geg.muttersprache.getMspId(); String mspKurzBez = geg.getMuttersprache().getMspKurzBez(); session.close(); Listing 3.23: Lazy Loading Hier wird in Zeile 5 ein Gegenstand geladen. Durch das Lazy Laoding wird für die referenzierte Entität Muttersprache zunächst ein Proxy verwendet. In Zeile 6 wird auf ein Attribut der Muttersprache zugegriffen. Hier wird jedoch noch nicht nachgeladen, da es sich bei diesem Attribut (mspId) um den Primärschlüssel handelt. Erst in Zeile 7, beim Zugriff auf ein Nicht-Primärschlüssel-Attribut (mspKurzBez), wird das komplette Muttersprache-Objekt aus der Datenbank geladen. 3.14 Fetching und Caching 77 Hibernate wendet das Lazy Loading nur bei Entitäten an. Werttypen werden immer sofort mitgeladen. Hibernate erlaubt es, dass Verhalten beim Nachladen von assoziierten Entitäten zu verfeinern. Es gibt nachstehende vier Strategien: • Select-Fetching Diese Strategie ist das Standardverhalten von Hibernate. Wird auf einen Proxy zugegriffen, wird nur die eine, dem Proxy zugehörige Entität, nachgeladen. Ein Problem dieser Strategie liegt in dem Umstand, dass beim Einladen einer Entität genau eine Datenbankabfrage (1 Select Statement) benötigt wird. Stehen jedoch beispielseise weitere referenzierte Entitäten in einer Collection, werden diese erst beim Zugriff darauf nachgeladen. Würde man 100 referenzierte Entitäten nachladen, würde dies bedeuten, dass auch 100 Abfragen (100 SELECT Statements) an die Datenbank gesendet werden. Zum Einladen einer Entität wird 1 Datenbankzugriff benötigt. Zum Nachladen aller assoziierten Entitäten werden n Datenbankzugriffe notwendig. Daher wird dieses Problem als 1+n-Problem bezeichnet. • Batch-Fetching Hier wird eine Batchgröße definiert. Wird auf eine Entität einer Collection zugegriffen, die noch nicht eingeladen wurde, wird diese selbst und die mit der Batchgröße eingestellte Anzahl an weiteren Entitäten im selben SELECT-Statement mitgeladen. Es werden sozusagen Entitäten auf Vorrat“ geladen. Nachteil dieser Version kann sein, dass mitgeladene ” Entitäten ähnlich dem Eager Loading nicht benötigt werden. Bei dieser Strategie vermindert sich das 1+n-Problem auf das 1+n/Batchgröße“” Problem. • Join-Fetching Bei dieser Variante werden alle assoziierten Entitäten durch Joins sofort und in einem Select-Statement geladen. Dabei werden OUTER JOINS verwendet. Problematisch wird diese Strategie beim Einladen von Entitäten mit mehreren Collections (von Entitäten). Durch die Bildung des kartesischen Produkts bei Joins können die Ergebnismengen sehr groß werden. • Subselect-Fetching Die Probleme der Bildung eines kartesischen Produktes durch Join-Fetching fallen hierbei weg. Dies wird duch ein Subselect bewerkstelligt. Zunächst wird die Entität eingeladen. Anschließend, in einem zweiten Select-Statement, werden die assoziierten Entitäten geladen. Dieser zweite Schritt wird über ein Subselect 3.14 Fetching und Caching 78 realisiert. Daher auch der Name dieser Strategie. Beim Einladen von Entitäten mit mehreren Collections (von Entitäten) ist diese Strategie dem Join-Fetching vorzuziehen. Der Nachteil dieser Variante besteht darin, das ein SELECT-Statement mehr als beim Join-Fetching benötigt wird. Die gewählte Strategie kann im Mapping definiert werden. Es ist aber auch möglich, diese Strategie bei einer Abfrage (HQL, Criteria) explizit anzugeben und somit die Konfiguration des Mappings zu überschreiben. from Gegenstand g join fetch g.muttersprache Sessions offenhalten Beim Lazy Loading von Hibernate gibt es eine Einschränkung. Referenzierte Entitäten können nur solange nachgeladen werden, solange die Session offengehalten wird. Wird die Session geschlossen und auf ein Element eines Proxies zugegriffen, welcher noch nicht nachgeladen wurde, wird eine LazyInitializationException geworfen. Abhilfe schafft hier die statische Methode Hibernate.initialize(Object proxy). Diese Methode läd die Entität proxy nach, auch wenn die Session bereits geschlossen wurde. Mithilfe der Methode Hibernate.isInitialized(Object proxy) kann überprüft werden, ob das übergebene Objekt bereits eingeladen wurde. Problem der Langen Sessions Wie oben beschrieben, muss die Session beim Lazy Loading offengehalten werden, wenn man sich die Mühe des händischen“ Nachladens mittels der Methode ” initialize() ersparen will. Dabei kann es zu langen Sessions kommen. Benötigt der Benutzer viel Zeit zum Bearbeiten eines Gegenstandes und ändert er anschließend noch die Muttersprache, welche bis dorthin lediglich als Proxy gehalten wurde, so muss auch die Session dementsprechend lange offengehalten werden. Greifen viele Benutzer in dieser Zeit auf die Datenbank zu und halten dabei ebenfalls weitere Sessions offen, kann es vorkommen, dass sehr viele Sessions geöffnet sind. Dies drückt stark auf die Performance der Datenbank, da viele Sessions auch einen erhöhten Verwaltungsaufwand erfordern. 3.14 Fetching und Caching 79 Im Kapitel 4 werden Lösungen zu diesem Problem gezeigt. 3.14.3 Eager Loading Das Gegenteil von lazy loading ist das sogenannte eager loading. Dabei werden alle referenzierten Objekte sofort mitgeladen. Diese Strategie ist nur in Sonderfällen sinnvoll. Würde ein Objekt beispielsweise drei andere Objekte referenzieren, würden diese sofort mitgeladen. Das ist noch kein Problem. Referenzieren diese drei Objekte jedoch wiederum jeweils drei Objekte und sind diese ebenfalls so konfiguriert, dass sie eager nachladen, müssen schon insgesamt 13 Objekte auf einmal geladen werden, unabhängig davon, ob darauf zugegriffen wird oder nicht. 3.14.4 Caching Jedes Session-Objekt hat einen Cache. Dieser wird als Session-Level-Cache bezeichnet und wurde im Kapitel 3.10 beschrieben. Second-Level-Cache Der Second-Level-Cache ist ein weiterer Cache. Dieser arbeitet Session-übergreifend. Genauer arbeitet er auf SessionFactory-Ebene. Im Kapitel 3.10 wurde gezeigt, wie eine Session aus einer SessionFactory erzeugt wird. Sinnvoll ist es, ein SessionFactory-Objekt applikationsweit zu teilen. Dadurch existiert der Cache genau einmal für das gesamte System, was der Java Virtual Machine (JVM) entspricht. Wird ein Datenbankzugriff ausgeführt, wird bei aktiviertem Second-Level-Cache zunächst überprüft, ob die Daten bereits im Cache bereitstehen. Werden die entsprechenden Daten gefunden, wird kein Datenbankzugriff mehr nötig. Dadurch ergibt sich ein Performance-Vorteil, da der Zugriff auf den Cache wesentlich schneller als jener auf die Datenbank ist. Jedoch entsteht durch den Second-Level-Cache auch ein Verwaltungs-Overhead. Zum Einen muss dieser Cache von Hibernate verwaltet werden. Zum Anderen wird bei jeder Datenbankabfrage zunächst der Cache durchsucht. Wird die Entität dort nicht gefunden, so war dieses Durchsuchen überflüssig und stellt einen Performancenachteil dar. 3.14 Fetching und Caching 80 Ein systemweiter Cache sollte deshalb dann verwendet werden, wenn häufig auf dieselben Entitäten zugegriffen wird. Es gibt folgende Caching-Strategien: • Read-Only Bei dieser Strategie wird ausschließlich aus dem Cache gelesen. • Read-Write Hierbei können die Daten nicht nur gelesen, sondern auch verändert werden. Diese Strategie ist nicht mit dem Transaktionsisolationslevel Serializable verwendbar. • Nonstrict Read-Write Auch hier können Daten verändert und gelesen werden. Bei dieser Strategie wird keine Transaktionsisolation vorgenommen. Sie ist daher nur dann sinnvoll, wenn selten konkurrierende Datenzugriffe erfolgen. Vorteil dieser Variante ist die bessere Performance im Vergleich zur Strategie Read-Write. • Transactional Diese Strategie wird in einer JTA-Umgebung verwendet. Es gibt verschiedene Implementierungen des Hibernate Second-Level-Caches. Nicht jeder Cache unterstützt jede Caching-Strategie. Die Wichtigsten sind: EHCache, OSCache, SwarmCache, JBoss Cache. Diese vier Implementierungen sind open sourceProdukte. 3.15 Entwicklungsstrategien 3.15 81 Entwicklungsstrategien Im Wesentlichen gibt es vier Strategien: Top down, Bottom up, Middle-Out und Meetin-the-Middle. 3.15.1 Top down Bei dieser Strategie wird zunächst ein Java Domainenmodell erstellt. Im nächsten Schritt werden die Mapping-Informationen hinzugefügt. Daraus kann später ein Datenbankschema erzeugt werden. Dies erfolgt mittels geeigneter Hibernate-Tools wie z.B. hbm2ddl. 3.15.2 Bottom up Hier ist das Datenbankschema bereits vorhanden. Mittels geeigneter Reverse-Engineering-Tools ist es möglich, das komplette Domainenmodell inklusive aller Mapping-Informationen generieren zu lassen. Die meisten Datenbanken leben länger als die Software, die darauf aufsetzt. Somit ist dies gängige Praxis in der Softwareentwicklung. Nachteil dieser Strategie ist, dass Konzepte relationaler Datenbanken häufig nur händisch“ in die objektorientierte Welt übertragbar sind. Ein Beispiel hierfür ist die ” Vererbung. 3.15.3 Middle-Out Hierbei werden zunächst die Mapping-Informationan (XML-Mapping) erstellt. Daraus wird sowohl das Datenbankschema, als auch das Objektmodell erzeugt. Diese Strategie ist vor allem bei kleinen, übersichtlichen Projekten vorteilhaft. 3.15.4 Meet-in-the-Middle Hier existieren Objektmodell und Datenbankschema bereits. Die Abstimmung dieser beiden Komponenten erfolgt über die Mapping-Informationen. 3.15 Entwicklungsstrategien 82 Diese Strategie ist nur in Ausnahmefällen sinnvoll. Im Speziellen dann, wenn andere Systeme auf bestehendem Objektmodell und Datenbank aufsetzen. Kapitel 4 Pattern 4.1 Übersicht In diesem Kapitel werden Pattern vorgestellt, welche unterschiedliche Verwendungsmöglichkeiten von Sessions und Transaktionen beschreiben. In diesem Zusammenhang sind Detached Entities von entscheidender Bedeutung. Detached Entities Im Kapitel 3.10 wurde der Lebenszyklus einer Hibernate-Enität gezeigt. Wird eine Entität von der Session losgelöst (d.h. die Session wird geschlossen), so befindet sich diese im Zustand detached. Änderungen an der Entität wirken sich nicht auf den SessionLevel-Cache aus. Um Detached Entities wieder in den Zustand persistent zu überführen, muss die Entität wieder an eine Session gebunden werden. Detached Entities haben den Nachteil, dass das Lazy Loading von Hibernate hier nicht mehr ohne Zusatzaufwand anwendbar ist. Wie im Kapitel 3.14 beschrieben muss die Entität beim Lazy Loading an eine Session gebunden sein. D.h. die Entität muss im Zustand persistent sein. Beim Arbeiten mit Detached Entities muss daher entweder auf Lazy Loading verzichtet werden, oder die Methode Hibernate.initialize(Objekt proxy) verwendet werden. 4.2 Session per Request 84 Bei den nachfolgend vorgestellten Pattern wird nicht auf die Überprüfung der konkurrierenden Benutzerzugriffe eingegangen. Diese wurden bereits im Kapitel 3.12 beschrieben. 4.2 Session per Request Hierbei wird pro Datenbankanfrage eine Session geöffnet. Die Lebensdauer einer Session ist sehr kurz. Bei einer Benutzeranfrage wird eine Session geöffnet, alle Operationen der Anfrage durchgeführt und anschließend wird die Session sofort wieder geschlossen. Werden Datenbankänderungen durchgeführt, so entspricht die Lebenszeit einer Transaktion jener der Session. Wird eine Entität mit dieser Strategie geladen, handelt es sich nach dem Schließen der Session um eine Detached Entity. Soll die geladene Entität wieder in die Datenbank gespeichert werden, so muss diese an eine Session gebunden werden. Zu diesem Zweck wird eine neue Session geöffnet, alle Operationen durchgeführt und anschließend wird die Session wieder geschlossen. Bei der Bearbeitung eines Gegenstands würde dies folgendermaßen realisiert: 1. Benutzeranfrage Der/Die Benutzer/in wählt den Menüpunkt Gegenstand bearbeiten“. ” 2. Session öffnen Der ausgewählte Datensatz wird eingeladen. 3. Session schließen Unmittelbar nach dem Einladen wird die Session wieder geschlossen. Die Entität befindet sich somit im Zustand detached. 1 2 3 4 5 //.. Session session = sessionFactory.openSession(); aktGegenstand=(Gegenstand)session.load(Gegenstand.class,22); session.close(); //.. Listing 4.1: Einladen einer Entitaet bei Session per Request 4.3 Session per Conversation 85 4. Bearbeitungszeit Der/Die Benutzer/in bekommt ein Formular zum Bearbeiten des Gegenstandes am Bildschirm angezeigt. Er/Sie hat dafür viel Zeit. Schließlich ist keine Session geöffnet. 5. Benutzeranfrage Der/Die Benutzer/in ist mit dem Bearbeiten des Gegenstandes fertig, und will diese Änderungen speichern. 6. Session öffnen Eine Transaktion wird geöffnet und die Änderungen der Entität persistiert. 7. Session schließen Die Session wird sofort wieder geschlossen. Die Interaktion mit dem/der Benutzer/in ist abgeschlossen. 1 2 3 4 5 6 7 //.. Session session = sessionFactory().openSession(); Transaction tx = session.beginTransaction(); session.update(aktGegenstand); tx.commit(); session.close(); //.. Listing 4.2: Speichern einer Entitaet bei Session per Request Diese Strategie hat den großen Vorteil, dass die Sessions nur sehr kurz offengehalten werden. Benötigt der/die Benutzer/in lange um eine Entität zu bearbeiten, ist das kein Problem. Schließlich können Detached Entities unbegrenzt lange gehalten werden. Des Weiteren ist diese Strategie einfach realisierbar und für Mehrbenutzerapplikationen gut geeignet. Nachteil sind die Probleme von Detached Entities im Zusammenhang mit Lazy Loading. 4.3 Session per Conversation Diese Strategie ist bei längeren Benutzerinteraktionen zu empfehlen. Ein Beispiel hierfür ist eine Konversation mit Frage/Antwort-Zyklen zwischen Benutzern und System. Wie der Name schon sagt, wird hier die Session erst wieder geschlossen, wenn die Benutzerinteratkion beendet wird. Werden Entitäten geladen, bleiben diese im Zustand persistent. Somit werden Änderungen an der Entität sofort im Session-Level-Cache 4.3 Session per Conversation 86 wirksam. Anders als beim Session per Request kann es in einer Session mehrere Transaktionen geben. Die Lebenszeit von Session und Transaktion ist bei diesem Pattern somit nicht gleich. Bei der Bearbeitung eines Gegenstands würde dies folgendermaßen realisiert: 1. Benutzeranfrage Der/Die Benutzer/in wählt den Menüpunkt Gegenstand bearbeiten“. ” 2. Session öffnen Der ausgewählte Datensatz wird eingeladen, die Session bleibt geöffnet. 3. Bearbeitungszeit Der/Die Benutzer/in bekommt ein Formular zum Bearbeiten des Gegenstandes am Bildschirm angezeigt. Die Session wird dabei die ganze Zeit offengehalten. 4. Benutzeranfrage Der/Die Benutzer/in ist mit dem Bearbeiten des Gegenstandes fertig, und will diese Änderungen speichern. 5. Transaktion öffnen Die Änderungen an der Entität werden persistiert. 6. Session schließen Die Session wird erst jetzt geschlossen. Die Interaktion mit dem/der Benutzer/in ist abgeschlossen. Folgendes Listing zeigt das grundsätzliche Schema der Implementierung bei Session per Conversation: 1 2 3 4 5 6 7 8 9 10 11 12 13 //1. Benutzerinteraktion Session session = sessionFactory().openSession(); aktGegenstand = (Gegenstand) session.load(Gegenstand.class, 22); //.. //2. Benutzerinteraktion aktGegenstand.setGegKurzbez("Prog"); aktGegenstand.setGegLangbez("Programmieren"); //.. //3.Benutzerinteraktion Muttersprache msp = aktGegenstand.getMuttersprache(); 4.4 Open Session in View 14 15 16 17 18 19 87 //.. //4. Benutzerinteraktion Transaction tx = session.beginTransaction(); session.update(aktGegenstand); tx.commit(); session.close(); Listing 4.3: Session per Conversation In Zeile 12 wird erstmals auf die Muttersprache zugegriffen und diese aus der Datenbank geladen. Vor Abarbeitung dieser Zeile, wurde diese Muttersprache-Entität durch einen Proxy repräsentiert. Dieses Pattern hat Vorteile bei längeren Benutzerinteraktionen. Bei dieser Strategie ist, anders als bei Session per Request, Lazy Loading kein Problem. 4.4 Open Session in View Dieses Pattern ist zum Einsatz in einer Webapplikation konzipiert und stellt eine Weiterentwicklung des Session per Request-Pattern dar. Ein typisches Problem einer Webapplikation ist, dass die Ausgabe, z.B. ein Bearbeitungsformular, erst nach der Abarbeitung der Programmlogik gerendert wird. Zu diesem Zeitpunkt wurde die Session aber bereits geschlossen. Der Zustand einer geladenen Entität ist somit detached. Beim Rendern der Ausgabe kann es vorkommen, dass auf assoziierte Entitäten zugegriffen wird, welche noch nicht geladen wurden. Dadurch wird eine LazyInitializationException geworfen. Zur Illustrierung dieses Problems wird das Gegenstand-Beispiel herangezogen. Will ein/e Benutzer/in einen Gegenstand bearbeiten, so wird der Gegenstand in einer Session eingeladen. Die Session wird danach geschlossen. Beim Gegenstand-Mapping wurde das Standardverhalten Lazy Loading beibehalten. Dadurch wird die assozierte Entität Muttersprache nicht geladen. Stattdessen wird ein Proxy verwendet. Beim Rendern des Bearbeitungsformulars wird auf den Namen der Muttersprache zugegriffen. Dieses Objekt ist jedoch nicht geladen. Und da die Entität Gegenstand im Zustand detached ist, wird beim Zugriff darauf eine LazyInitializationException geworfen. 4.4 Open Session in View 88 Beim Open Session in View-Pattern wird die Session solange offengehalten, bis alle benötigten Daten zum Rendern der Ausgabe geladen wurden. Dafür müssen in der Webapplikation entsprechende Listener, Servlet-Filter u.Ä. implementiert werden. 1. Benutzeranfrage Der/Die Benutzer/in wählt den Menüpunkt Gegenstand bearbeiten“. ” 2. Session öffnen Der ausgewählte Datensatz wird eingeladen. 3. Die Ausgabe wird gerendert Dabei werden beim Zugriff des Systems auf die Proxies alle benötigten Entitäten eingeladen. 4. Session schließen Unmittelbar nach dem Rendern wird die Session wieder geschlossen. Die Entität befindet sich somit im Zustand detached. Das Schließen der Session kann zum Beispiel in einem PhaseListener erfolgen. 5. Bearbeitungszeit Der/Die Benutzer/in bekommt ein Formular zum Bearbeiten des Gegenstandes am Bildschirm angezeigt. Er/Sie hat dafür viel Zeit. Schließlich ist keine Session geöffnet. 6. Benutzeranfrage Der Benutzer ist mit dem Bearbeiten des Gegenstandes fertig, und will diese Änderungen speichern. 7. Session öffnen Eine Transaktion wird geöffnet und die Änderungen der Entität persistiert. 8. Session schließen Die Session wird sofort wieder geschlossen. Die Interaktion mit dem Benutzer ist abgeschlossen. Bei der Strategie Open Session in View können die Stärken des Lazy Loading voll ausgeschöpft werden. Es werden für ein Web-Formular genau jene assoziierten Entitäten mitgeladen, die zur Erzeugung der Ausgabe benötigt werden. Wie oben erwähnt, ist dieses Pattern eine Weiterentwicklung des Session per Request-Pattern für Webapplikationen. Das Konzept, Sessions kurzlebig zu halten wird beibehalten. Kapitel 5 Umsetzung im Projekt SAS III 5.1 Übersicht Im Projekt SAS III stand die Datenbank beim Projektbeginn bereits zur Verfügung. Es handelt sich dabei um eine PostgreSQL-Datenbank. PostgreSQL ist eine Open-Source Datenbank, welche großteils konform dem ANSISQL 92 Standard ist. Sie wird unter der BSD1 -Lizenz verbreitet. Als Entwicklungsstrategie wurde Bottom up gewählt. Aufgabe dieser Projektgruppe war die Erstellung der SAS-Webapplikation, welche es dem/der Benutzer/in ermöglicht, Daten einzugeben bzw. zu manipulieren. Dies wurde in From von Webformularen realisiert. Diese Formulare sind dabei sehr häufig datenintensiv. Schließlich gibt es in der Datenbank, auf die dieses Projekt aufsetzt, Tabellen mit einer Spaltenanzahl von mehr als 80. Dadurch war die Performance bei der Implementierung eine große Herausforderung. 1 Berkeley Software Distribution 5.2 Aufbau der Applikation 5.2 90 Aufbau der Applikation 5.2.1 Übersicht Die SAS III-Webapplikation verwendet neben Hibernate die Technologien Facelets und ICEfaces. Der OR-Mapper Hibernate ist für die Datenbankanbindung zuständig. Facelets und ICEfaces werden für die Oberflächengestaltung der Webapplikation eingesetzt. Der interne Aufbau der Applikation entspricht der Komponentenstruktur von EJB. 5.2.2 POJO und Mapping-Informationen Die Mapping-Informationen werden in der Form des XML-Mappings implementiert. Die Entscheidung zu dieser Variante war großteils dadurch bedingt, dass die Implementierung dieses Softwareprojektes mit dieser Technik begann und erst im Laufe dieser Diplomarbeit wurde das Annotation-Mapping analysiert. Ein Vorteil dieser Variante ist jedoch, das Konzepte des OR-Mappings dadurch besser ersichtlich sind und die Einarbeitung in Hibernate einfacher ist. Des Weiteren ist es für Hibernate-unerfahrene Anwendungsentwickler einfacher, Persistenzklassen zu verwenden, welche nicht durch Annotations an Übersichtlichkeit verlieren. Es gibt somit zu jeder Entität zwei Klassen. Die Entitäts-Klasse (POJO) und die zugehörige XML-Mapping-Datei. Beide Dateien liegen im Paket model. Eine Mapping-Datei enthält Mapping-Information zu genau einem zugeordneten POJO. Mehrfachmappings in einer XML-Datei werden nicht verwendet. Die Namen der beiden zugehörigen Dateien sind äuqivalent (z.B. Gegenstand.java - Gegenstand.hbm.xml, Muttersprache.java - Muttersprache.hbm.xml, ...). Die POJOs verfügen jeweils über einen parameterlosen Konstruktor, einen Konstruktor für alle Spalten die einen not null-Constraint besitzen (diese Instanzvariablen dürfen beim Speichern nicht null sein) und einen Konstruktor mit allen Instanzvariablen. Des Weiteren gibt es zu jeder Instanzvariable entsprechende Get- und Set-Methoden. Hibernate greift standardmäßig auf Instanzvariablen über diese Methoden zu. Dieses Verhalten wurde beibehalten. 5.2 Aufbau der Applikation 91 Die POJOs sind serialisierbar. Sie implementieren das Interface java.io.Serializable. Die Methode getLastChange() wurde in nahezu jeder Entitäts-Klasse implementiert. Sie liefert die letzte Datenbankänderung der Entität. Ansonsten stellt ein POJO keine weitere Programmlogik bereit. Ein POJO im Projekt SAS III hat somit grundsätzlich folgenden Aufbau: 1 public class Entity implements java.io.Serializable{ 2 //Instanzvariablen 3 4 //Parameterloser Konstruktor 5 6 //Not-Null-Konstruktor 7 8 //Konstrukor für alle Instanzen 9 10 //Getter & Setter 11 12 //Methode getLastChange() 13 } Listing 5.1: Aufbau eines POJOs in SAS III Bei den Mapping-Dateien wurde soviel Information als möglich integriert. D.h. auch optionale Parameter wurden weitestgehend gefüllt. Werden im Mapping nur zwingend vorgeschriebene Parameter mit Werten versorgt, holt sich Hibernate diese Informationen zur Laufzeit. Es wäre beispielsweise möglich, den Datentyp (Mapping-Element type) einer Property nicht anzugeben. Hibernate könnte sich den Typ zur Laufzeit über die Dekleration im POJO holen. Dies wirkt sich jedoch negativ auf die Performance aus. 5.2.3 Konfiguration Hibernate wird in der Applikation SAS III mittels der hibernate.cfg.xml-Datei konfiguriert. Diese befindet sich im default-package. Sie wird somit beim Erzeugen eines Configuration-Objekts mittels der Methode configure() ohne implizite Angabe des Pfades eingelesen. 5.2 Aufbau der Applikation 92 Da die Applikation eine Postgres-Datenbank verwendet, wird der PostgreSQL JDBC Treiber verwendet. Der Hibernate Dialekt dafür ist in org.hibernate.dialect.PostgreSQLDialect definiert. 1 2 3 4 5 6 <property name="hibernate.dialect"> org.hibernate.dialect.PostgreSQLDialect </property> <property name="hibernate.connection.driver_class"> org.postgresql.Driver </property> Listing 5.2: Konfiguration des Datenbankdialekts 5.2.4 SessionFactory Im Projekt SAS III hat jeder/jede Benutzer/in eine eigene SessionFactory. Beim Anmelden am System wird für den/die Benutzer/in eine neue SessionFactory instanzieert. Somit besitzt jeder/jede Benutzer/in auch einen eigenen Second-Level-Cache. Die Implementierung des Second-Level-Caches ist Aufgabe einer anderen Diplomarbeit. Deshalb wird dieses Thema hier nicht näher behandelt. Die Klasse HibernateUtil, welche nach dem Entwurfsmuster Singleton implementiert wurde, hat folgenden Aufbau: 1 public class HibernateUtil { 2 3 private SessionFactory sessionFactory; 4 private Configuration configuration; 5 6 public HibernateUtil() { 7 } 8 9 public synchronized SessionFactory getSessionFactory(String ←dbUser, String dbPassword) { 10 if (sessionFactory == null) { 11 configuration = new Configuration() 12 .configure() 13 .setProperty("hibernate.connection.username", ←dbUser) 5.2 Aufbau der Applikation 14 15 16 17 18 19 20 21 22 23 } .setProperty("hibernate.connection.password", dbPassword); sessionFactory = configuration.buildSessionFactory(); 93 ←- } return sessionFactory; } public Configuration getConfiguration() { return configuration; } Listing 5.3: SessionFactory im Projekt SAS III Die beiden Attribute sessionFactory und configuration können sich über die entsprechenden Getter gegeben werden lassen. Die Methode getSessionFactory() erzeugt das Configuration- und SessionFactory()-Objekt beim Anmelden eines Benutzers. Dies geschieht pro Benutzer/in nur einmal, anschließend wird bei jedem Aufruf von getSessionFactory() die bereits instanzieerte SessionFactory zurückgeliefert. Es ist zu beachten, dass in Zeile 12 über die Methode configure() die Konfigurationseinstellungen der hibernate.cfg.xml eingelesen wird. In den Zeilen 13 und 14 wird die Konfiguration im Bezug auf den/die Benutzer/in durch die Methoden setProperty() überschrieben. Somit geht jeder/jede Benutzer/in mit seinen/ihren individuellen Rechten auf die Datenbank und besitzt eine eigene SessionFactory. 5.3 Views 5.3 94 Views 5.3.1 Übersicht Im Projekt SAS III werden in vielen Fällen Views verwendet. Ziel dahinter ist zum Einen die Verbesserung der Performance. Zum Anderen werden bestimmte Systeme dadurch vereinfacht. Zur Illustration wird in folgendem Beispiel die Personal-View gezeigt: In der Datenbank existiert die Tabelle personal. Diese Tabelle besitzt einen ForeignKey auf die Tabelle titel. In der Tabelle titel stehen alle möglichen Titel, die für eine Person in SAS III relevant sein können. Wird auf einen Datensatz zugegriffen und soll dabei auch der Titelname geladen werden, wird ein Join oder ein zusätzliches Select-Statement nötig. Abbildung 5.1 verdeutlicht diesen Sachverhalt. Abbildung 5.1: Tabellenstruktur ohne Views In vielen Formularen wird eine Dropdownbox des gesamten Personals angezeigt. Beim Formular zur Bearbeitung einer Abteilung ist dies, wie in Abbildung 5.2 ersichtlich, bei der Wahl des Abteilungsvorstandes der Fall. 5.3 Views 95 Abbildung 5.2: Dropdownfeld auf einem Webformular Um die Titel vor dem Namen der potentiellen Abteilungsvorstände stehen zu haben, wäre für jede Personal-Entität ein zusätzlicher Select nötig. Mithilfe einer View wird dieses Problem gelöst und die Performance gesteigert. Abbildung 5.3: Views anstelle von Tabellen In der View gibt es nun, wie in Abbildung 5.3 ersichtlich, die neue Spalte per_name, welche Titel und Name zusammenfasst. Dieses Konzept kommt in SAS III in vielen Fällen zum Einsatz. 5.3 Views 5.3.2 96 Navigationsbäume Eine weitere Anwendung von Views im Projekt SAS III sind die in Abbildung 5.4 ersichtlichen Navigationsbäume. Abbildung 5.4: Navigationsbaum der SAS III-Webapplikation Dieser Navigationsbaum wird für die Benutzer individuell dargestellt. Ein Lehrer bekommt beispielsweise nur jene Klassen bzw. Schüler angezeigt, die er unterrichtet. Hierzu wurden Views angelegt, welche genau diese Informationen individuell für die angemeldeten Benutzer liefert. Des Weiteren bestehen diese Views nur aus Spalten, welche zur Anzeige des Baums benötigt werden. 5.3 Views 97 Jeder Knotenpunkt ist ein Java-Objekt, welches eine Collection der Subknoten hat. Wie in Abbildung 5.4 ersichtlich, ist dies beispielsweise bei der Klasse der Fall. Diese enthält eine Collection aller Schueler-Subknoten. Zu diesem Zweck wird folgende Klasse verwendet: 1 public class Klasse_Baum { 2 3 private Integer id; 4 private String bezeichnung; 5 private Set<Schueler_Baum> schueler; 6 7 //Getter und Setter... 8 } Listing 5.4: Baumknotenklasse Diese Klasse enthält nur notwendige Daten, um den Baum performant zu halten. Das Mapping wird folgendermaßen realisiert: 1 <hibernate-mapping> 2 <class name="model.tree.Klasse_Baum" table="vi_tree_klasse"> 3 <id column="kla_id" name="id"> 4 <generator class="assigned"/> 5 </id> 6 <property column="kla_bezeichnung" name="bezeichnung" ←type="string"/> 7 8 <set name="schueler" order-by="ssd_schueler" ←table="vi_tree_schueler"> 9 <key column="ssd_kla_id"/> 10 <one-to-many class="model.tree.Schueler_Baum"/> 11 </set> 12 </class> 13 </hibernate-mapping> Listing 5.5: Klassen-Knotenpunkt im Navigationsbaum Die Wahl der Generatorstrategie in Zeile 4 ist unwichtig. Schließlich wird diese Entität niemals in die Datenbank geschrieben. 5.3 Views 98 In den Zeilen 7,8,9 und 10 wird eine Collection erzeugt. Diese wird mittels order-by="ssd_schueler" sortiert. Es handelt sich, wie in Zeile 9 ersichtlich um den one-to-many Beziehungstyp. Eine Klasse hat mehrere Schüler. 5.3.3 Mapping Generator Bei der Entwicklungsstrategie Bottum up können Mapping-Dateien und POJOs mittels geeigneter Reverse-Engineering-Tools erzeugt werden. Auch die Entwicklungsumgebung Netbeans 6.7.1, in der SAS III entwickelt wird, stellt ein solches Tool zur Verfügung. Diese Tools sind jedoch nicht für das mappen von Views geeignet. Der Grund dafür liegt in der Tatsache, dass bei Views keine Informationen über Constraints hinterlegt sind. Es können daher nur Name und Datentyp der Spalten ausgelesen werden. Im Projekt SAS III werden, wie oben beschrieben, häufig Views verwendet. Es wurde daher nötig, ein Tool zu entwickeln, welches das mappen von Views realisiert. Zu diesem Zweck wurde im Rahmen dieser Diplomarbeit ein Reverse-EngineeringTool entwickelt. Es handelt sich dabei um eine Java-Applikation. 5.3 Views 99 Beim Start der Applikation muss, wie in Abbildung 5.5 ersichtlich, zunächst die Datenbankverbindung konfiguriert werden. Abbildung 5.5: Datenbankverbindung beim Mappinggenerator herstellen Der Inhalt des Passwortfeldes wird nicht angezeigt. Im Feld Server wird die Adresse und Port des Servers angegeben. Die Checkbox Anmeldedaten speichern sorgt dafür, dass beim nächsten Start des Tools alle Felder vorausgefüllt sind. Einzig das Passwortfeld wird aus Sicherheitsgründen nicht gespeichert. Die Dropdown-Box DB Typ dient der Konfiguration des verwendeten Datenbanktreibers. Dieser Generator ist für die Verwendung in verschiedenen Umgebungen konzipiert. So ist es zurzeit möglich, die Open-Source-Datenbanken Postgres und MySQL zu verwenden. Abbildung 5.6: Auswahl der Datenbank Werden weitere Datenbanken verwendet, ist es kein Problem, diese zu implementie- 5.3 Views 100 ren. Datenbankunabhängiger Quelltext wird dadurch erreicht, das die Datentypen nicht nach Name überprüft werden. Der Name von Datentypen unterscheidet sich schließlich von Datenbank zu Datenbank. Stattdessen wird nach den in java.sql.Types definierten Typen verglichen. Nach Eingabe aller Verbindungsdaten, wird die Verbindung nach einem Klick auf den Button Connect hergestellt. Sind die eingegebenen Einstellungen nicht korrekt, so wird eine Fehlermeldung angezeigt und die Verbindungsdaten können neu eingegeben werden. Ist die Verbindung erfolgreich, so wird das Haupfenster des Generators geöffnet. Wie oben erwähnt, ist dieser Generator speziell für Views erzeugt worden. Zu diesem Zweck wird eine Tabelle mit allen Spalten der ausgewählten View angezeigt. Der/Die Benutzer/in kann dann Constraints hinzufügen. Das Hauptfenster des Mappinggenerators ist in Abbildung 5.7 zu sehen. 5.3 Views 101 Abbildung 5.7: Hauptfenster des Mappinggenerators 5.3 Views 102 In der Combobox, welche sich am oberen Ende des Hauptfenster des Mappinggenerators befindet, kann jede Tabelle und View, die es in der Datenbank gibt ausgewählt werden. In diesem Fall wurde die View vi_t_personal verwendet. Alle Vorgaben des Generators wurden belassen. Eine Spalte einer Tabelle/View wird in einer Zeile angezeigt. Der Button POJO und Mapping-File erzeugen erstellt die im Namen des Buttons enthaltenen Dateien in der Form des Mapping mit XML. Der Button Konfigurationsdatei erzeugen erstellt eine hibernate.cfg.xml mit den Einstellungen, die zuvor beim Herstellen der Datenbankverbindung eingegeben wurden. Folgend werden die Spalten der Tabelle des Mapping Generators, welche in Abbildung 5.7 zu sehen sind, beschrieben: • PK Diese Spalte definiert den Primärschlüssel. In Abbildung 5.7 ist kein Primärschlüssel vergeben, da die Constraints bei einer View wie oben erwähnt nicht ausgelesen werden können. Würde nun versucht, die Ausgabedateien zu erstellen, wird eine Fehlermeldung ausgegeben. Abbildung 5.8: Fehlermeldung wenn kein Primaerschlüssel ausgewaehlt wurde Jede Hibernate-Entität muss einen Primärschlüssel besitzen. Wird mehr als eine Spalte ausgewählt, werden diese Spalten in einer ID-Klasse zusammengefasst und mittels der composite-id, wie im Kapitel Identität beschrieben, eingebunden. Der Generator implementiert auch gleich die Methoden equals() und hashCode() in der ID-Klasse. Es werden daher drei Ausgabedateien erzeugt. Der Generator ist somit in der Lage, fachliche Primärschlüsseltabellen und Views zu mappen. • DB Spaltenname In dieser Spalte wird der Datenbankspaltenname angezeigt. Diese Spalte kann sinnvollerweise von Benutzern nicht verändert werden. 5.3 Views 103 • Instanzvariablenname In der Spalte Instanzvariablenname werden die in Java-Bean-Standard konvertierten Namen der Instanzvariablen angezeigt. Die Benutzer haben hierbei die Freiheit, diese vorgeschlagenen Bezeichnungen zu verändern. In Abbildung 5.7 wurden die Namen so belassen wie sie vom Generator vorgeschlagen wurden. • Java-Typ Diese Spalte zeigt den jeweils verwendeten Java-Datentyp der Instanzvariablen an. • Length In dieser Spalte wird die maximale Zeichenanzahl der Datenbankspalten angezeigt. Dieses Feld kann von Benutzern verändert werden. Durch dieses Feld werden Datenintegritätsregeln in das Mapping einbezogen. • Not Null Auch diese Spalte hat den Zweck, Datenintegritätsregeln in das Mapping einzubeziehen. Diese Spalte ist in engem Kontakt mit der Spalte Java-Typ zu sehen. Ist das Not Null-Feld für eine bestimmte Zeile ausgewählt, so wird hier ein primitiver Datentyp für die Instanzvariable verwendet (z.B. int). Schließlich muss diese Spalte bei einem Datenbank-INSERT einen Wert besitzen und darf nicht null sein. Ist das Feld Not Null nicht ausgewählt, so darf in diese Spalte auch null eingefügt werden. Deshalb wird hier ein Wrappertyp verwendet (z.B. java.lang.Integer). • Kommentar In diesem Feld kann eine Anmerkung zur Datenbankspalte bzw. Instanzvariable gegeben werden. Diese wird in das Mapping mittels des <comment>-Elements und ins POJO als Kommentar neben der Instanzvariablendeklaration übernommen. • Objekt laden Mithilfe dieser Spalte kann eine Assoziation zu einer Entität erzeugt werden. Dabei wird eine normale Datenbankreferenz auf das Mapping abgebildet. Dies wird, wie im Kapitel 3.7 beschrieben, durch das <many-to-one>-Element implementiert. Der Mappinggenerator erstellt jeweils unidirektionale Beziehungen. 5.3 Views 104 Wird dieses Feld ausgewählt, erscheint ein Dialog, mit welchem die Beziehung konfiguriert werden kann. Abbildung 5.9: Anlegen einer Beziehung Hier kann optional eine Tabelle ausgewählt werden. Diese Auswahl ist jedoch optional. Entscheidend ist der Klassenname. Ist man mit der Konfiguration fertig, so kann der Button POJO & Mapping - File erzeugen gedrückt werden. Dadurch werden die Ausgabedateien erstellt. Der Ort und Name der Dateien kann, wie in Abbildung 5.10 zu sehen, in einem JFileChooser-Dialog bestimmt werden. Standardmäßig wird hier der Desktop als Speicherziel vorausgewählt. 5.3 Views 105 Abbildung 5.10: Speicherort festlegen Der Mappinggenerator geht noch einen Schritt weiter und erlaubt neben dem Mappen von Views auch das Reverse-Engineering an normalen Tabellen. Constraints werden dabei aus der Tabelle ausgelesen und im Hauptfenster vorausgewählt. In Abbildung 5.11 ist ein vorausgefülltes Hauptfenster zu sehen. 5.3 Views Abbildung 5.11: Vorausgefuelltes Hauptfenster beim Mappen von Tabellen 106 5.4 Sessions, Transaktionen und Lazy Loading 5.4 107 Sessions, Transaktionen und Lazy Loading Wie schon in der Übersicht erwähnt, war die Performance ein wesentlicher Bestandteil bei der Implementierung der SAS-Webapplikation. Aufgabe war es, die Dateneingabe bzw. Manipulation zu implementieren. Die Generierung von Reports, wie z.B. Zeugnisse oder Schülerlisten, war Aufgabe einer anderen Projektgruppe. In Abbildung 5.12 ist ein Bearbeitungsformular zu sehen. Abbildung 5.12: Webformular von SAS III 5.4 Sessions, Transaktionen und Lazy Loading 108 Hier wird ersichtlich, dass es keinen Sinn machen würde, Objekreferenzen im Mapping zu verwenden. Bei den Feldern Anrede, Titel und Status wird eine DropdownBox verwendet, welche jeweils alle möglichen Eingaben, die Benutzer tätigen können, beinhalten. Diese werden mittels Criteria wie in Kapitel 5.7 gezeigt, aus den jeweiligen Tabellen geladen. Im Mapping werden lediglich die einzelnen Spalten als Werttypen geladen. Auch bei Referenzen auf Datenbankseite wird der Fremdschlüssel ebenfalls als Werttyp geladen. Wird das assoziierte Objekt benötigt, kann es über den Fremdschlüssel nachgeladen werden. Die Tabellen und Views werden daher flach geladen. Lazy Loading wird nicht verwendet. Werttypen werden von Hibernate sofort geladen und bei den wenigen im Mapping enthaltenen mitreferenzierten Entitäten wird Lazy Loading mittels des Parameters lazy="false" deaktiviert. Somit ergeben sich auch nicht die Probleme des Lazy Loadings, wie beispielsweise das 1+n-Problem oder der Zugriff auf Proxies außerhalb einer Hibernate-Session. Bei Datenbankzugriffen wird das Session per Request-Pattern angewandt. Hibernate-Sessions werden dadurch so kurz als möglich gehalten. In der Bearbeitungszeit des Benutzers auf einem Webformular sind die Entitäten im Zustand detached. Beim Speichern einer Entität wird eine neue Session geöffnet. Diese Strategie bietet für den benötigten Aufgabenbereich der Webapplikation die beste Performance. Die nachstehend gezeigte Implementierung wird, stellvertretend für alle anderen Entitäten, anhand der Entität Gegenstand gezeigt. Das Einladen der Entität Gegenstand wird in der Methode loadAktGegenstand vorgenommen. 1 public void loadAktGegenstand(int id) { 2 Session session = getUb().getSessionFactory().openSession(); 3 try { 4 aktGegenstand=(Gegenstand)session.load(Gegenstand.class,id); 5 } finally { 6 session.close(); 7 } 8 } Listing 5.6: Typisches Laden einer Entitaet in SAS III Wie im Listing ersichtlich, wird die Session in Zeile 2 geöffnet und nach dem Einladen des Gegenstandes wieder geschlossen. Die Datenbankoperationen werden im tryBlock durchgeführt. Dadurch wird sichergestellt, dass egal ob ein Fehler auftritt oder nicht, der Code im finally-Block ausgeführt wird. 5.4 Sessions, Transaktionen und Lazy Loading 109 Lazy Loading wurde im Mapping von Gegenstand deaktiviert. Probleme des Lazy Loading, wie der Zugriff auf noch nicht initialisierte Proxies, stellen somit kein Problem dar. Nach erfolgreichem Einladen wird das Bearbeitungsformular am Browser des Benutzers angezeigt. In der Bearbeitungszeit befindet sich die Entität Gegenstand im Zustand detached. Ist der/die Benutzer/in mit der Bearbeitung fertig und klickt auf den Speicher-Button, so kommt folgende Methode zur Ausführung: 1 public void saveAktGegenstand() { 2 Session session = getUb().getSessionFactory().openSession(); 3 Transaction tx = session.beginTransaction(); 4 try { 5 session.update(aktGegenstand); 6 tx.commit(); 7 //Erfolgsmeldung am Bildschirm des Benutzers anzeigen 8 } catch (Exception e) { 9 tx.rollback(); 10 //Weitere Fehlerbehandlung und Ausgabe einer Fehlermeldung 11 } finally { 12 session.close(); 13 } 14 } Listing 5.7: Typische Speichermethode in SAS III In Zeile 2 wird eine neue Session erzeugt. Die Gegenstand-Entität wird in Zeile 4 mittels der Methode update() an diese Session gebunden. In Zeile 5 werden die Änderungen mittels commit() in die Datenbank geschrieben. Auch hier erfolgen die Datenbankoperationen im try-Block. Tritt ein Fehler auf, wird die Transaktion mittels rollback() zurückgerollt. Im finally-Block, welcher sicher zur Ausführung kommt, wird die Session geschlossen. 5.5 Sperren 5.5 110 Sperren Bei der Webapplikation SAS III handelt es sich um eine Multi-User-Applikation. Beim Verändern von Entitäten kann es daher bei konkurrierenden Benutzerzugriffen zum Überschreiben von Änderungen kommen. In diesem Projekt werden jedoch weder pessimistische noch optimistische Sperrstrategien verwendet. Das Standardverhalten von Hibernate, Letzter Commit gewinnt (engl. last commit wins), wird beibehalten. Die Änderungen des späteren Speichervorganges überschreiben somit den Speichervorgang des früheren. Die Entscheidung zu dieser Strategie hatte folgende Gründe: Zum einen wurde auch die Vorgängerapplikation SASII in dieser Form implementiert. Es hat sich dabei als Best Practice bewährt, diese Strategie zu verwenden. Zum Anderen werden Benutzern im Projekt SAS III Zugriffsrechte vergeben. Beispielsweise ist der Direktor einer Schule als einziger berechtigt, z.B. die Anschrift der Schule zu ändern. Ein weiteres Beispiel ist, dass ein Lehrer nur Noten seiner Gegenstände für Schüler eintragen darf, die er auch wirklich unterrichtet. Diese Beispiele zeigen, dass Überschneidungen nur selten auftreten. Der Mehraufwand beim Einsatz von Sperrstrategien wäre daher nicht gerechtfertigt. 5.6 Component-Mapping In einigen Tabellen der SAS III-Datenbanken, werden Telefonkontakte direkt in die jeweiligen Tabellen integriert. Dies ist beispielsweise bei der Tabelle Abteilung der Fall. Vier Spalten bilden einen Telefonkontakt. Dies sind: • arttel Art der Telefonnummer. Z.B. Fax, Handy, Telefon. • staattel Staatsvorwahl. Z.B. +43 für Österreich. • vorwahltel Regionale Vorwahl. Z.B. 02742 für St. Pölten. 5.6 Component-Mapping 111 • rufnrtel Rufnummer. In die Tabelle Abteilung können höchstens vier dieser Telefonkontakte eingetragen werden. Die Tabelle Abteilung enthält somit 16 Telefonkontaktspalten, wobei jeweils 4 davon einen Telefonkontakt bilden. In der Hibernate-Implementierung wurde dies über Component Mapping vereinfacht. Zu diesem Zweck wurde die Klasse Telefonkontakt implementiert, welche die oben gezeigten vier Spalten als Instanzen kapselt. Es handelt sich dabei um ein Wertobjekt. Diese Klasse hat folgenden Aufbau: public class Telefonkontakt { private private private private Character arttel; Integer staattel; String vorwahltel; String rufnrtel; //... //Konstruktoren, Getter und Setter } Diese Klasse wird in jeder Entität, in der Telefonkontakte vorhanden sind, eingesetzt. Das Abteilungs-POJO kapselt nun vier Telefonkontakt-Objekte. public class Abteilung implements java.io.Serializable { //... private private private private //... Telefonkontakt Telefonkontakt Telefonkontakt Telefonkontakt telefonkontakt1; telefonkontakt2; telefonkontakt3; telefonkontakt4; } In der Mapping-Datei zur Klasse Abteilung ergibt sich dadurch folgender Aufbau: 5.7 Criteria 112 1 <component name="telefonkontakt1" class="model.Telefonkontakt"> 2 <property name="arttel" type="java.lang.Character"> 3 <column length="1" name="abt_arttel1"> 4 <comment>Art 1. Telefon (Foreign-Key telefonart)</comment> 5 </column> 6 </property> 7 <property name="staattel" type="java.lang.Integer"> 8 <column name="abt_staattel1"> 9 <comment>ID des Staates mit der Landesvorwahl 1. Telefon ( ←Foreign-Key staat)</comment> 10 </column> 11 </property> 12 <property name="vorwahl" type="string"> 13 <column length="10" name="abt_vorwahltel1"> 14 <comment>Vorwahl 1. Telefon</comment> 15 </column> 16 </property> 17 <property name="rufnummer" type="string"> 18 <column length="15" name="abt_rufnrtel1"> 19 <comment>Rufnummer 1. Telefon</comment> 20 </column> 21 </property> 22 </component> 23 24 <!-...und drei weitere Component Mappings...--> Listing 5.8: Component Mapping eines Wertobjekts Diese Strategie des Component-Mappings wurde in allen Entitäten, welche Telefonkontaktspalten besitzen, verwendet. 5.7 Criteria Wie bereits erwähnt, werden in den Webformularen häufig Dropdownfelder benötigt, welche alle möglichen Benutzereingaben beinhalten. Hierzu wird eine zentrale Klasse verwendet, welche Collections vom Typ SelectItem über entsprechende Getter zurückliefert. 5.8 Identität 113 Beispielsweise liefert folgende Methode alle möglichen Titel: 1 public synchronized List<SelectItem> getTitel() { 2 if (titel == null) { 3 Session session = getSessionFactory().openSession(); 4 try { 5 List<Titel> titelList = session.createCriteria(Titel.class). ←list(); 6 titel = new ArrayList<SelectItem>(); 7 for (Titel t : titelList) 8 titel.add(new SelectItem(t.getTitId(), t.getTitKurzbez())); 9 } catch (HibernateException ex) { 10 //Fehlerbehandlung 11 } finally { 12 if (session != null && session.isOpen()) 13 session.close(); 14 } 15 } 16 return titel; 17 } Listing 5.9: Collections moeglicher Eingaben in Dropdownfeldern Dabei wird eine Criteria verwendet. In Zeile 3 wird eine Session geöffnet und in Zeile 5 eine Datenbankabfrage erzeugt. Diese liefert alle Datensätze der Tabelle titel. Die Session wird im finally-Block geschlossen. 5.8 Identität Die Datenbank von SAS III verwendet in den meisten Fällen synthetische Primärschlüssel. Dabei wird die Erzeugungsstrategie Sequence verwendet. Dies ist eine einfache und zuverlässige Art, Primärschlüssel zu beziehen. Nachteil dieser Variante ist, dass sie nicht von jeder Datenbank unterstützt wird. Bei der Verwendung einer anderen Datenbank ist es jedoch kein großer Aufwand, die Vergabestrategie zu ändern. 1 <id name="gegId" type="java.lang.Integer"> 2 <column name="geg_id"/> 3 <generator class="sequence"> 4 <param name="sequence">gegenstand_geg_id_seq</param> 5.8 Identität 114 5 </generator> 6 </id> Listing 5.10: Vergabestrategie bei synthetischem Primaerschluessel in SAS III Der Entwickler muss sich im Projekt SAS III somit in den meisten Fällen nicht um die Vergabe des Primärschlüssels einer Entität kümmern. Kapitel 6 Konklusion/Ausblick Bei der Implementierung der Softwarearchitektur zum Projekt SAS III hat sich Hibernate als sehr gutes OR-Mapping Framework bewiesen. Das in Hibernate gesetzte Vertrauen wurde bei der Implementierung vollends erfüllt. Ein wichtiger Faktor war dabei, das Hibernate auf Standards aus der Java-Welt setzt. Ersichtlich wird dies beispielsweise beim von Hibernate verwendetem Grundelement der Datenpersistierung: Dem POJO. Hier ist aber wohl auch ein anderer Schluss möglich. Hibernate setzt selbst Standards. Schließlich war Hibernate bei der Entwicklung der EJB 3.0-Spezifikation maßgeblich beteiligt und die Konzepte von Hibernate nahmen auf diese Entwicklung großen Einfluss. Wie auch immer geht Hibernate bei beiden Interpretationsmöglichkeiten konform mit Standards um. Dies stellt einen großen Vorteil dieses OR-Mapping Frameworks im Vergleich zu anderen Frameworks dar. Hibernate wird seit 2001 entwickelt. Es unterstützt nahezu alle Konzepte relationaler Datenbanken. Hibernate ist Teil der JBoss-Gruppe und wird laufend weiterentwickelt. Die Gefahr eines Entwicklungsstops von Hibernate ist gering. Ein weiterer großer Vorteil von Hibernate ist, dass es neben der OR-Mapping Komponente auch zusätzliche Komponenten anbietet. Dies sind im Moment Hibernate Search, Hibernate Validator und Hibernate Shards. In den OR-Mappings von Hibernate können diese Komponenten konfiguriert werden. So ist beispielsweise die Volltextsuche, die Validierung und die Verwendung verteilter Datenbanken schnell und einfach zu implementieren. 6 Konklusion/Ausblick 116 Hibernate hat sich als de facto Standard in der Java-Welt etabliert. Hibernate ist flexibel, robust und transparent. Der Einsatz eines OR-Mapping-Frameworks ist in großen Softwareprojekten unerlässlich. Hibernate hat sich im Projekt SAS III als exzellentes OR-Mapping-Framework erwiesen. Abbildungsverzeichnis 3.1 Hibernate Ueberblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.2 POJO - Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.3 Vererbungshierarchie von Collections und Maps . . . . . . . . . . . . . 30 3.4 binaere/reflexive Beziehungen . . . . . . . . . . . . . . . . . . . . . . . 34 3.5 1:n-Beziehung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 3.6 n:1-Beziehung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.7 1:1-Beziehung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.8 m:n-Beziehung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 3.9 Vererbungshierarchie Gegenstand . . . . . . . . . . . . . . . . . . . . . 41 3.10 Lebenszyklus von Entitaeten . . . . . . . . . . . . . . . . . . . . . . . . 55 3.11 HQL - Joins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 5.1 Tabellenstruktur ohne Views . . . . . . . . . . . . . . . . . . . . . . . . . 94 5.2 Dropdownfeld auf einem Webformular . . . . . . . . . . . . . . . . . . . 95 5.3 Views anstelle von Tabellen . . . . . . . . . . . . . . . . . . . . . . . . . 95 5.4 Navigationsbaum der SAS III-Webapplikation . . . . . . . . . . . . . . . 96 5.5 Datenbankverbindung beim Mappinggenerator herstellen . . . . . . . . 99 5.6 Auswahl der Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 ABBILDUNGSVERZEICHNIS 118 5.7 Hauptfenster des Mappinggenerators . . . . . . . . . . . . . . . . . . . 101 5.8 Fehlermeldung wenn kein Primaerschlüssel ausgewaehlt wurde . . . . 102 5.9 Anlegen einer Beziehung . . . . . . . . . . . . . . . . . . . . . . . . . . 104 5.10 Speicherort festlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 5.11 Vorausgefuelltes Hauptfenster beim Mappen von Tabellen . . . . . . . . 106 5.12 Webformular von SAS III . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Tabellenverzeichnis 3.1 Wichtige Session Methoden . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.2 Konfiguration der JNDI-Datasource . . . . . . . . . . . . . . . . . . . . . 56 3.3 Logisches Lost Update Problem . . . . . . . . . . . . . . . . . . . . . . 60 3.4 Dirty Read . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 3.5 Nonrepeatable Read . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.6 Phantom Read . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.7 Fehlerfaelle der Isolationsgrade . . . . . . . . . . . . . . . . . . . . . . . 62 3.8 Wichtige HQL Expressions . . . . . . . . . . . . . . . . . . . . . . . . . 69 Listings 1.1 m:n-Beziehung in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 3.1 Klasse Gegenstand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.2 Gegenstand Mapping File . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3.3 Annotation Component-Mapping . . . . . . . . . . . . . . . . . . . . . . 22 3.4 XML Component-Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.5 Zusaetzliche Identifikationsklasse beim fachlichen Primaerschluessel . 27 3.6 Persistenzklasse beim Fachlichen Primaerschluessel . . . . . . . . . . . 28 3.7 Fachlicher Primaerschluessel in der Mapping-Datei . . . . . . . . . . . . 29 3.8 Annotation-Mapping des fachlichen Primaerschluessels . . . . . . . . . 29 3.9 XML-Mapping bei einer 1:n-Beziehung . . . . . . . . . . . . . . . . . . . 36 3.10 XML-Mapping bei einer n:1-Beziehung . . . . . . . . . . . . . . . . . . . 37 3.11 XML-Mapping einer m:n-Beziehung . . . . . . . . . . . . . . . . . . . . 39 3.12 Annotation-Mapping einer m:n-Beziehung . . . . . . . . . . . . . . . . . 40 3.13 Implementierung der Vererbungshierarchie Gegenstand . . . . . . . . . 41 3.14 Tabelle pro Klassenhierarchie . . . . . . . . . . . . . . . . . . . . . . . . 42 3.15 Tabelle pro Subklasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.16 Tabelle pro konkreter Klasse . . . . . . . . . . . . . . . . . . . . . . . . 46 3.17 Die Konfigurationsdatei hibernate.properties . . . . . . . . . . . . . . . . 48 LISTINGS 121 3.18 Die Konfigurationsdatei hibernate.cfg.xml . . . . . . . . . . . . . . . . . 49 3.19 Setzen der Konfiguration im Quellcode . . . . . . . . . . . . . . . . . . . 51 3.20 Session und Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . 58 3.21 Expliziter HQL-Join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 3.22 Datenbankabfrage mit Criteria . . . . . . . . . . . . . . . . . . . . . . . 74 3.23 Lazy Loading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 4.1 Einladen einer Entitaet bei Session per Request . . . . . . . . . . . . . 84 4.2 Speichern einer Entitaet bei Session per Request . . . . . . . . . . . . . 85 4.3 Session per Conversation . . . . . . . . . . . . . . . . . . . . . . . . . . 86 5.1 Aufbau eines POJOs in SAS III . . . . . . . . . . . . . . . . . . . . . . . 91 5.2 Konfiguration des Datenbankdialekts . . . . . . . . . . . . . . . . . . . . 92 5.3 SessionFactory im Projekt SAS III . . . . . . . . . . . . . . . . . . . . . 92 5.4 Baumknotenklasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 5.5 Klassen-Knotenpunkt im Navigationsbaum . . . . . . . . . . . . . . . . 97 5.6 Typisches Laden einer Entitaet in SAS III . . . . . . . . . . . . . . . . . 108 5.7 Typische Speichermethode in SAS III . . . . . . . . . . . . . . . . . . . 109 5.8 Component Mapping eines Wertobjekts . . . . . . . . . . . . . . . . . . 112 5.9 Collections moeglicher Eingaben in Dropdownfeldern . . . . . . . . . . 113 5.10 Vergabestrategie bei synthetischem Primaerschluessel in SAS III . . . . 113 Literaturverzeichnis [1] Beeger, Haase, Roock, Sanitz: Hibernate, Persistenz in Java-Systemen mit Hibernate und der Java Persistence API, dpunkt.verlag, 2007 ISBN: 978-3-89864-447-1 [2] Bauer, King: Java Persistence with Hibernate, Manning, 2007 ISBN 1-932394-88-5 [3] Hien, Kehle: Hibernate und die Java Persistence API, entwickler.press, 2007 ISBN: 978-3-935042-96-3 [4] Hibernate Reference Documentation, URL: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/ Datum: 14. Mai 2010 [5] InformIT, Improving Hibernate’s Performance, URL: http://www.informit.com/ Datum: 14. Mai 2010 [6] Kiltz Webanwendungen, URL: http://www.kiltz.de/ Datum: 14. Mai 2010 [7] Umgang mit Sessions und Transaktionen, URL: http://community.jboss.org/wiki/Sessionsandtransactions Datum: 14. Mai 2010 [8] Open Session in View Pattern, URL: http://community.jboss.org/wiki/OpenSessioninView Datum: 14. Mai 2010 [9] JBoss, URL: http://www.hibernate.org/ Datum: 14. Mai 2010 LITERATURVERZEICHNIS [10] Enterprise Java Beans, Java Persistence API, URL: http://www.jcp.org/en/jsr/detail?id=220 Datum: 14. Mai 2010 [11] PostgreSQL - Offizielles Handbuch, URL: http://www.postgresql.org/docs/ Datum: 14. Mai 2010 123