Analyse und praktischer Vergleich von neuen Access-Layer-Technologien in modernen Web-Anwendungen unter Java Oliver Kalz Analyse und praktischer Vergleich von neuen Access-Layer-Technologien in modernen Web-Anwendungen unter Java Oliver Kalz geb. am 15.02.1979 in Spremberg Diplomarbeit zur Erlangung des akademischen Grades Diplom-Informatiker (FH) eingereicht an der Fachhochschule Brandenburg – Fachbereich Informatik und Medien – Betreuer: Prof. Dr. S. Edlich (Fachhochschule Brandenburg) Prof. Dr. Th. Preuß (Fachhochschule Brandenburg) Brandenburg, den 08.01.2004 Danksagung Ich danke hiermit allen, die mich während meines Studiums tatkräftig unterstützt haben. Besonderer Dank gilt meinen Eltern, ohne die dies hier nicht möglich gewesen wäre. Ebenso möchte ich mich bei Prof. Dr. Edlich bedanken, der mir mit vielen Tips und Ideen zur Seite stand. Zu guter letzt danke ich den Korrekturlesern, die hoffentlich alle Tippfehler entdeckt haben. „Only two things are infinite, the universe and human stupidity, and I’m not sure about the former.“ – Albert Einstein Inhaltsverzeichnis 1 Einleitung 2 Grundlagen 2.1 Das Client/Server-Modell . . . . 2.2 Objektpersistenz . . . . . . . . . . 2.3 Der Java-Standard . . . . . . . . . 2.4 Die Extensible Markup Language 3 4 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 12 16 18 Enterprise JavaBeans 3.1 Die EJB-Architektur . . . . . . . . . . . . . . 3.2 Programmier-Restriktionen . . . . . . . . . 3.3 Bestandteile einer EJB . . . . . . . . . . . . . 3.3.1 Das Home-Interface . . . . . . . . . 3.3.2 Das Komponenten-Interface . . . . . 3.3.3 Die Bean-Klasse . . . . . . . . . . . . 3.3.4 Der Deployment-Deskriptor . . . . 3.4 Arten von EJBs . . . . . . . . . . . . . . . . 3.4.1 Session Beans . . . . . . . . . . . . . 3.4.2 Entity Beans . . . . . . . . . . . . . . 3.4.3 Message Driven Beans . . . . . . . . 3.5 Persistenzmechanismen . . . . . . . . . . . 3.5.1 Bean Managed Persistence . . . . . 3.5.2 Container Managed Persistence . . 3.6 Transaktionen . . . . . . . . . . . . . . . . . 3.6.1 Container-gesteuerte Transaktionen 3.6.2 Bean-gesteuerte Transaktionen . . . 3.6.3 Client-gesteuerte Transaktionen . . 3.7 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 22 22 24 25 25 27 27 28 31 35 36 36 37 45 46 47 48 48 Java Data Objects 4.1 Ziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 Metadaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 50 50 50 . . . . . . . . . . . . . . . . . . . . 6 Inhaltsverzeichnis 4.3 4.4 4.5 4.6 4.7 4.8 5 6 4.2.2 Bytecode Enhancement . . . . . . . . 4.2.3 JDO Query Language . . . . . . . . . Der Entwicklungsprozeß mit JDO . . . . . . Objektidentität . . . . . . . . . . . . . . . . . . 4.4.1 Datastore Identity . . . . . . . . . . . 4.4.2 Application Identity . . . . . . . . . . 4.4.3 Non-durable Identity . . . . . . . . . . Die JDO-API . . . . . . . . . . . . . . . . . . . 4.5.1 Speichern und Löschen von Objekten 4.5.2 Objekte laden und manipulieren . . . JDO-Implementierungen . . . . . . . . . . . . 4.6.1 Libelis LiDO . . . . . . . . . . . . . . . 4.6.2 Triactive JDO . . . . . . . . . . . . . . 4.6.3 XORM . . . . . . . . . . . . . . . . . . JDO und XDoclet . . . . . . . . . . . . . . . . Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 52 52 54 55 55 56 56 57 58 60 61 63 64 67 68 Objektrelationale Mapper 5.1 Apache ObJectRelational Bridge . . . . . . . 5.1.1 XML-Metadaten . . . . . . . . . . . . 5.1.2 Die PersistenceBroker-API . . . . . . . 5.2 Exolab Castor . . . . . . . . . . . . . . . . . . 5.2.1 Konfiguration . . . . . . . . . . . . . . 5.2.2 Mapping-Informationen . . . . . . . . 5.2.3 Abhängige und unabhängige Objekte 5.2.4 Die Castor-API . . . . . . . . . . . . . 5.3 Hibernate . . . . . . . . . . . . . . . . . . . . . 5.3.1 Mapping-Informationen . . . . . . . . 5.3.2 Die Hibernate-API . . . . . . . . . . . 5.3.3 SessionFactory und Session . . . . . . 5.4 Oracle9iAS TopLink . . . . . . . . . . . . . . . 5.4.1 Deskriptoren und Mapping . . . . . . 5.4.2 Mapping Workbench . . . . . . . . . . 5.4.3 Session und Unit of Work . . . . . . . 5.5 Weitere O/R-Mapper . . . . . . . . . . . . . . 5.6 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 72 73 75 77 77 78 80 81 83 84 86 88 89 89 90 92 95 96 Objektorientierte Datenbanken 6.1 Schwächen relationaler Systeme . . 6.2 Objektrelationale Datenbanken . . . 6.2.1 SQL99 . . . . . . . . . . . . . 6.2.2 Beispiel: Intersystems Caché 6.3 Objektdatenbanken . . . . . . . . . . 6.3.1 Beispiel: db4o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 100 101 102 103 107 107 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inhaltsverzeichnis 6.4 6.5 Objektorientierte Datenbanksysteme 6.4.1 Der ODMG-Standard . . . . 6.4.2 Beispiel: Objectivity . . . . . Fazit . . . . . . . . . . . . . . . . . . . 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 110 114 116 7 Performance 119 8 Kurzübersicht O/R-Mapping 125 9 Fazit 127 A Anhang A.1 Servlets und Java Server Pages . . . . . . . . . . . . . A.1.1 Servlets . . . . . . . . . . . . . . . . . . . . . . . A.1.2 Java Server Pages . . . . . . . . . . . . . . . . . A.2 Java Database Connectivity . . . . . . . . . . . . . . . A.3 Transaktionen und JTA . . . . . . . . . . . . . . . . . . A.4 Key-Generatoren . . . . . . . . . . . . . . . . . . . . . A.5 JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . A.6 Die Beispielanwendung . . . . . . . . . . . . . . . . . A.7 Die JDO-DTD . . . . . . . . . . . . . . . . . . . . . . . A.8 Primärschlüssel-Klasse für JDO Application Identity . A.9 JDODoclet . . . . . . . . . . . . . . . . . . . . . . . . . A.10 JDOMapper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 130 130 131 135 137 139 140 142 144 145 146 148 Abbildungsverzeichnis 151 Tabellenverzeichnis 153 Literaturverzeichnis 155 1 | Einleitung Haben vor einigen Jahren noch statische HTML-Seiten genügt, um Informationen zu präsentieren, so hat sich das World Wide Web (WWW) in den letzten Jahren zu einem immer wichtiger werdenden Medium entwickelt. Das Spektrum reicht dabei von der einfachen Informationsrepräsentation mit statischen HTML-Seiten bis zu dynamisch erzeugten Webseiten mit PHP/MySQL oder ASP. Das WWW wird immer häufiger auch als Plattform für vollständige, verteilte Anwendungen auf der Basis von Java (J2EE, Servlets, JSPs) genutzt. Da solche komplexen Webanwendungen ihre Daten meist aus Datenbanken beziehen, kommt dem Access Layer als unterste Schicht einer Anwendung eine große Bedeutung zu. Der Access Layer ist dafür verantwortlich, die zu verarbeitenden und anzuzeigenden Daten zuverlässig, vor allem aber performant zur Vefügung zu stellen. Eine der ersten Entwicklungen zum Datenbankzugriff aus Java war die Java Database Connectivity (JDBC), eine Sammlung von Interfaces für einen implementierungsunabhängigen Zugriff auf verschiedene Datenbanken. Da aber hier mit dem Impedance Mismatch der Unterschied zwischen objektorientierter und relationaler Welt negativ zum Tragen kommt, wurden in letzter Zeit neue Möglichkeiten entwickelt, dieses Problem zu umgehen. Aufgabenstellung Im Verlauf der Arbeit sollen alternative Persistenzmechanismen an praktischen CodeBeispielen vorgestellt und in Hinblick auf ihr Handling und ihre Performance verglichen werden. Zunächst soll auf einige theoretische Grundlagen eingegangen werden. Anschließend werden dann die einzelnen Technologien vorgestellt. Schwerpunkte sind Enterprise JavaBeans, Java Data Objects, objektrelationale Mapper und objektorientierte Datenbanken. Sie haben das Ziel, den Impedance Mismatch zu verhindern oder zumindest vor dem Entwickler zu verbergen und so die Entwicklung objektorientierter Anwendungen zu vereinfachen. Außerdem soll eine kleine Webanwendung entstehen, in deren Access Layer einige der vorgestellten Werkzeuge zum Einsatz kommen. Darin werden dann auch die Unterschiede im Handling, in der Performance und der Komplexität der Implementierung deutlich. 10 Die Begleit-CD Auf der beiliegenden CD befinden sich die im Rahmen der Arbeit entstandene Webanwendung sowie einige der verwendeten Werkzeuge in der zur Abgabe aktuellen Version. Die Webanwendung liegt im Quelltext als Eclipse-Projekt vor, außerdem wurde sie in einem vorkonfigurierten JBoss-Applikationsserver installiert. Weitere Informationen enthält die Datei readme.html im Hauptverzeichnis der CD. 2 | Grundlagen In diesem Kapitel sollen einige grundlegende Erklärungen gegeben werden, die für das Verständnis der folgenden Kapitel notwendig sind. Hierzu gehören Konzepte wie Persistenz und Objektorientierung sowie Technologien wie relationale Datenbanken, Java und XML. 2.1 Das Client/Server-Modell Das Client/Server-Modell beschreibt die am häufigsten verwendete Architektur für die Entwicklung verteilter Anwendungen. Ziel solcher Anwendungen ist die gleichmäßige Lastverteilung auf alle verfügbaren Rechner. Eine Client/Server-Architektur ermöglicht Clients die Arbeit mit zentral verfügbaren Diensten und Daten. Ein Server stellt hierbei einen Dienst zur Verfügung, der von anderen Rechnern genutzt werden kann. Es handelt sich meist um einen Hintergrundprozeß, der auf Anfragen von Clients wartet. Ein Client nutzt die von Servern bereitgestellten Dienste bei Bedarf. Zu diesem Zweck baut er eine Kommunikationsverbindung zum Server auf. Voraussetzung für die Kommunikation ist die Nutzung eines gemeinsamen Protokolls, welches die Regeln und Kommunikationskanäle für den Informationsaustausch beschreibt. Die Aufteilung einer Applikation in Schichten legt fest, wie die Programmfunktionalität auf Client und Server aufzuteilen ist. Speziell bei webbasierten Anwendungen hat diese Aufteilung einen großen Einfluß auf die Performance und die Funktionalität eines Systems. Grundsätzlich läßt sich eine Applikation in drei Schichten aufteilen1 : Präsentationsschicht (Presentation Layer), Geschäftslogik (Business Logic Layer) und Persistenzschicht (Access Layer). Die Präsentationsschicht kann eindeutig dem Client zugeordnet werden. Als Persistenzschicht kommt meist ein zentraler Datenbankserver zum Einsatz. Die zentrale Datenhaltung ermöglicht es, Datenbestände effizient zu verwalten, Redundanzen zu vermeiden und die Integrität der Daten gerade im Mehrbenutzerbetrieb sicherzustellen. Für die Verteilung der Geschäftslogik gibt es verschiedene Möglichkeiten. In der 2-Schichten-Architektur wird die Geschäftslogik vollständig im Client implementiert. Das in der Persistenzschicht verwendete Datenbanksystem ist ausschließlich für die 1 Diese Aufteilung gilt nicht zwangsläufig nur für verteilte Anwendungen, sondern wird auch bei Standalone-Anwendungen eingesetzt. 12 2.2 Objektpersistenz Verarbeitung von Anfragen zuständig und sendet die Ergebnisse über ein datenbankspezifisches Protokoll an den Client. Clients in einer zweischichtigen Architektur werden aufgrund ihrer komplexen Programmfunktionalität auch Fat Clients genannt. Sie sind zwar vergleichsweise einfach zu entwickeln, unterliegen jedoch hohen Wartungs- und Installationskosten, da Änderungen der Geschäftslogik bei allen Clients durchgeführt werden müssen. Bei der Kommunikation zwischen Client und Server wird außerdem das Netzwerk stark belastet, da zu keiner Zeit eine Vorverarbeitung von Ergebnissen stattfindet. Vielmehr sendet der Server eine komplette Ergebnismenge an den Client. Erst dort findet die Verarbeitung statt. Zudem ist die Skalierbarkeit einer zweischichtigen Architektur eingeschränkt. Meist bildet der Server einen Flaschenhals, eine Lastverteilung ist nicht möglich. In einer 3-Schichten-Architektur wird eine zusätzliche Schicht zwischen der Präsentationsund Persistenzschicht eingefügt. Diese Schicht kapselt die Geschäftslogik einer Applikation und liegt meist in Form eines Applikationsservers vor. Diese Architektur wird dann eingesetzt, wenn die gleiche Funktionalität von verschiedenen Clients genutzt werden soll. Die Geschäftslogik wird einmal programmiert und allen Clients über den Applikationsserver zentral zur Verfügung gestellt. Dadurch ist das System leichter zu warten und zu erweitern: Bei einer Änderung der Funktionalität ist nur die Geschäftslogik betroffen, die Clients bleiben davon unberührt. Clients, die in einer dreischichtigen Architektur eingesetzt werden, fallen durch eine geringere Komplexität und niedrigen Ressourcenverbrauch auf. Man spricht von sogenannten Thin Clients. Zudem wird die Entwicklung von derartigen Clients vereinfacht, da die Datenspeicherung transparent erfolgt. Der Applikationsserver abstrahiert vom Zugriff auf die Daten und kümmert sich außerdem um deren Konsistenz. Während die Kommunikation zwischen Applikationsserver und Datenbankserver über das datenbankspezifische Protokoll erfolgt, werden für die Kommunikation zwischen Client und Applikationsserver sogenannte Middleware-Lösungen eingesetzt. Dort können standardisierte Protokolle wie zum Beispiel HTTP zum Einsatz kommen. Speziell im Java-Umfeld bieten sich auch Web Services und RMI2 an. Dreischichtige Architekturen sind somit zwar schwieriger zu realisieren, zeichnen sich allerdings durch hohe Skalierbarkeit, einfache Wartung und gute Erweiterbarkeit aus. 2.2 Objektpersistenz Objekte, die in einer Anwendung erzeugt und benutzt werden, sollen eine beliebige Lebensdauer haben. Der Begriff Objektpersistenz beschreibt den Mechanismus, Objekte über die Laufzeit einer Anwendung hinaus zu erhalten. Solche Objekte werden persistente Objekte genannt und sind solange verfügbar, bis sie explizit gelöscht werden. Neben persistenten Objekten gibt es transiente Objekte, deren Lebenszeit maximal bis zum Ende einer Anwendung reicht. 2 RMI: Remote Method Invocation 2 | Grundlagen 13 Um die Verfügbarkeit zu gewährleisten, werden persistente Objekte meist in den Sekundärspeicher ausgelagert. So gehen diese Daten auch nicht bei einem Absturz der Anwendung verloren. Für die Auslagerung gibt es verschiedene Möglichkeiten. Objekte lassen sich zum Beispiel im Dateisystem ablegen. Java bietet hierfür die Serialisierung an. Dabei handelt es sich um eine einfache Lösung, die nicht für große Datenmengen geeignet ist. Alternativ lassen sich Objekte in Datenbanksystemen speichern. Solche Systeme bieten eine effiziente Datenhaltung und ermöglichen Anwendungen die Suche und den schnellen Zugriff auf Daten (vgl. [BG02]). Handelt es sich um eine objektorientierte Anwendung, so würde sich auch ein objektorientiertes Datenbanksystem als Speicher für die Objekte anbieten. Allerdings haben sich objektorientierte Systeme bisher nicht auf dem Markt durchsetzen können. Die meisten Anwendungen speichern ihre Daten derzeit in relationalen Datenbanksystemen. Sie sind in ihrer Entwicklung ausgereift und auf dem Markt weit verbreitet. Allerdings kommt es beim Einsatz relationaler Systeme als Datenspeicher für Anwendungen zum sogenannten Impedance Mismatch. Dieser Begriff steht für das Aufeinandertreffen zweier unterschiedlicher Programmierparadigmen: dem Programmierparadigma der Anwendung und dem relationalen Paradigma der Datenbank. So sind Programmiersprachen satzorientiert und je nach gewählter Sprache imperativ oder objektorientiert. Bei der von relationalen Datenbanken verwendeten Anfragesprache SQL3 handelt es sich hingegen um eine mengenorientierte und deklarative Sprache. Hinzu kommt, daß die Relationenalgebra der Datenbank durch Anwendung der Mengenlehre mathematisch erfaßbar ist. Auch die Typsysteme unterscheiden sich. So ist der Entwickler dafür verantwortlich, beide Systeme geeignet zu überbrücken und eine funktionierende Anbindung bereitzustellen. In den vergangenen fünf Jahren hat sich die Technik des objektrelationalen Mappings entwickelt, um speziell objektorientierte Anwendungen unter Vermeidung des Impedance Mismatch an relationale Datenbanken anzubinden. Das relationale Modell Im Relationenmodell werden Daten in Tabellen gespeichert. Eine Zeile in einer Tabelle wird Datensatz oder Tupel genannt, die Menge aller Tupel einer Tabelle heißt Relation. Die Informationen eines Datensatzes werden in Feldern oder Attributen organisiert. Das Relationenschema definiert alle Felder für die Relationen einer Tabelle. Diese Definition umfaßt einen Namen sowie einen Datentyp für jedes Feld. Die Tabellen einer relationalen Datenbank können zudem in verschiedenen Beziehungen zueinander stehen. Für die eindeutige Identifizierung eines Datensatzes innerhalb einer Tabelle wird der Primärschlüssel verwendet. Dieser kann aus einem oder mehreren Attributen des Datensatzes bestehen. Beziehungen werden über Fremdschlüssel hergestellt. 3 SQL: Structured Query Language 14 2.2 Objektpersistenz Das objektorientierte Modell Die objektorientierte Pogrammierung (OOP) ist ein modernes Programmierparadigma. Viele der heute verwendeten Programmiersprachen sind entweder von Grund auf objektorientiert (Java, Smalltalk) oder wurden um objektorientierte Konzepte erweitert (Basic, Pascal) [GK03]. Objektorientierte Programmierung soll zu robusten, fehlertoleranten und wartungsfreundlichen Programmen führen. Im folgenden sollen die wichtigsten Konzepte kurz vorgestellt werden. In der objektorientierten Programmierung wird mit einem hohen Grad der Abstraktion gearbeitet. So wird zwischen Konzeption und Umsetzung, also Klasse und Objekt unterschieden (nach [GK03]). Eine Klasse stellt einen abstrakten Datentyp dar und faßt Daten und die Methoden, die auf diesen Daten operieren, zusammen. Die in älteren Programmiersprachen wie C und Pascal realisierte Trennung zwischen Daten und Funktionen fällt weg. Die Daten (Attribute) sollen zudem nur über die zur Verfügung gestellten Methoden zugänglich sein. Zu diesem Zweck muß die Signatur einer Methode nach außen bekannt sein. Sie besteht aus einem Namen, einem Rückgabewert sowie einer Parameterliste. Dadurch kommt es zu einer vollständigen Kapselung, die die eigentliche Implementierung einer Klasse vor der Umgebung verbirgt. Eine Klasse faßt die Eigenschaften gleichartiger Objekte zusammen und gilt somit als Bauplan für Objekte. Ein konkretes Objekt entsteht durch Instantiierung einer Klasse. Das Objekt4 erhält eine eindeutige Identität und hat zu jeder Zeit einen bestimmten Zustand, der durch die Werte der Attribute gegeben ist. Das Konzept der Vererbung bringt Klassen in eine „ist-ein“-Beziehung. Das bedeutet, daß eine Klasse von einer Oberklasse abgeleitet werden kann. Diese Unterklasse erbt alle Eigenschaften der Oberklasse und kann weitere Eigenschaften definieren. Durch Vererbung kann eine Klassenhierarchie aufgebaut werden, die von abstrakten zu immer konkreteren Klassen führt. Oberklassen gelten in einer solchen Hierarchie als Generalisierung der Unterklassen. Umgekehrt gelten die Unterklassen als Spezialisierung der Oberklassen. Bei der Vererbung wird zwischen Einfachvererbung und Mehrfachvererbung unterschieden. Bei der Einfachvererbung hat jede Klasse höchstens eine direkte Oberklasse, bei der Mehrfachvererbung kann eine Klasse beliebig viele direkte Oberklassen besitzen. Ein weiteres Konzept der Objektorientierung ist der Polymorphismus. Er beschreibt die Möglichkeit, Methoden einer Klasse zu überschreiben oder zu überladen. Erbt eine Klasse eine Methode von der Oberklasse, so kann sie die Implementierung dieser Methode entweder unverändert übernehmen oder eine eigene Implementierung zur Verfügung stellen. Letzteres wird als Überschreiben bezeichnet. Hierbei ändert sich nur die Implementierung einer Methode, nicht jedoch die Signatur. Zur Laufzeit führt der Compiler eine Typüberprüfung durch und führt in Abhängigkeit vom ermittelten Typ eines Objekts die entsprechende Methode aus (späte Bindung). Es ist auch möglich, Methoden mit gleichen Namen, aber unterschiedlichen Parameterlisten zur Verfügung zu stellen. Man spricht dann von überladenen Methoden. Aus Vererbung und später Bindung folgt auch die Substituierbarkeit von Objekten. So 4 auch: Instanz 2 | Grundlagen 15 kann ein Objekt überall dort eingesetzt werden, wo eigentlich ein Objekt einer Oberklasse erwartet wird. Dies ist möglich, da ein Objekt alle Eigenschaften der Oberklasse erbt. Der Compiler ermittelt durch die eben genannte Typüberprüfung zur Laufzeit den richtigen Datentyp und führt eine aufgerufene Methode entsprechend aus. Objektrelationales Mapping Aus der Darstellung von Daten im relationalen und objektorientierten Modell lassen sich einige Gemeinsamkeiten erkennen (siehe [OTS02]). So definiert das Relationenschema die Struktur von Datensätzen, so wie eine Klasse die Struktur von Objekten definiert. Die Daten eines relationalen Datensatzes werden in den Feldern des Relationenschemas gespeichert, die Daten eines Objekts in dessen Attributen. Der Datentyp eines Feldes ist ebenso unveränderlich (statisch) wie der Datentyp eines Objektattributs. Auch Beziehungen können sowohl in relationalen Datenbanken als auch in einem objektorientierten Datenmodell realisiert werden. Ein Datensatz referenziert andere Datensätze über Fremdschlüssel, Objekte nutzen hierfür Referenzen. Abbildung 2.1: Relationenmodell vs. Klasse Beide Modelle weisen jedoch auch einige signifikante Unterschiede auf. So dienen relationale Datenbanken lediglich der Speicherung von Daten. Sie sind nicht in der Lage, Methoden oder Vererbungshierarchien zu speichern. Auch komplexe Beziehungen können nicht ohne weiteres dargestellt werden. Beziehungen zwischen Tabellen sind immer unidirektional. Hingegen kann im objektorientierten Modell eine Beziehung auch bidirektional, also von zwei Seiten aus navigierbar, sein. Prinzipiell ist es also möglich, Objekte auf relationale Strukturen abzubilden. Diese Abbildung wird objektrelationales Mapping (O/R-Mapping) genannt. Dabei wird genau festgelegt, wie die Daten eines persistenten Objekts und auch seine Beziehungen zu anderen persistenten Objekten in einer relationalen Datenbank abgebildet werden. Im Normalfall wird eine Klasse auf eine Tabelle abgebildet, die Attribute auf Tabellenspalten. Folglich wird ein Objekt über das Mapping mit seiner persistenten Repräsentation in der Datenbank verbunden. 16 2.3 Der Java-Standard Werkzeuge, die diese Abbildung realisieren, werden O/R-Mapper genannt. Sie stellen eine Schicht von Java-Klassen zur Verfügung, die zwischen einer Java-Applikation und einer relationalen Datenbank zum Einsatz kommt. Die Schicht agiert als objektorientierter Wrapper um die Datenbank und ermöglicht einer Applikation, ihre persistenten Objekte auf ein Datenbank-Schema abzubilden. Durch eine solche Persistenzschicht wird die Applikation vor Änderungen in der Datenbank geschützt, wodurch Änderungen am Datenbank-Schema einen geringeren Einfluß auf die Applikation haben. Zudem bietet die Persistenzschicht eine API5 mit einer Reihe von Methoden zum Laden, Speichern und Manipulieren von Objekten an. 2.3 Der Java-Standard Bei Java handelt es sich nicht nur um die Programmiersprache, sondern um eine Menge von Technologien und Konzepten, die zusammen den Java-Standard ausmachen. Zu diesem Standard gehören: • Die Programmiersprache Java als objektorientierte Sprache mit C-ähnlicher Syntax. Die Sprache ist an C++ angelehnt, allerdings ohne deren Komplexität, da Features wie zum Beispiel Zeiger fehlen. • Die Java Virtual Machine (JVM) als Interpreter für Java-Bytecode. Eine Java-Applikation ist portabel und kann auf jedem System ausgeführt werden, das über eine JVM verfügt. • Die Java-Plattform als Menge aller Packages und Klassen, die dem Entwickler als APIs für seine Programme zur Verfügung stehen. Im Jahre 1990 begann Sun Microsystems Inc. die Entwicklung von Java mit dem Ziel, eine Software-Plattform zu schaffen, die „anywhere on the network“ und auf beliebigen Rechnersystemen lauffähig sein sollte (vgl. [EK02]). Mittlerweile liegt Java in Version 2 vor und wurde in drei Editionen mit unterschiedlichen Zielen aufgespaltet: • Java 2 Standard Edition (J2SE) mit allen Werkzeugen, um Java-Anwendungen und -Applets zu entwickeln, • Java 2 Enterprise Edition (J2EE) für netzwerkzentrische, serverseitige EnterpriseApplikationen, • Java 2 Micro Edition (J2ME) für den Einsatz von Java auf Geräten wie Mobiltelefonen und PDAs. Abbildung 2.2 zeigt die wichtigsten Bestandteile von J2SE und J2EE. Es wird deutlich, daß die J2SE mit der integrierten JVM die Grundlage für die Entwicklung mit Java bildet, die J2EE ist als Erweiterung in Form von Spezifikationen und APIs zu sehen. Während 5 API: Application Programming Interface, Programmierschnittstelle 2 | Grundlagen 17 für Anwendungen auf J2SE-Basis lediglich die JVM für die Ausführung notwendig ist, benötigt man für Enterprise-Applikationen einen Applikationsserver. Ausgehend von der J2SE mit der integrierten JVM bietet J2EE zusätzliche Technologien für komplexe, verteilte Enterprise-Applikationen an. Dazu gehören unter anderem Enterprise JavaBeans, Servlets, Java Naming and Directory Interface (JNDI), Java Messaging Service (JMS), Java Transaction API (JTA) und JavaMail. " # " ! Abbildung 2.2: Komponenten von J2SE und J2EE Java-Komponentenmodelle Komponentenmodelle wurden von Sun entwickelt, um die Verteilung, Erweiterung und Pflege von Software zu vereinfachen. Eine Software-Komponente soll benutzerdefinierten Code durch generische, wiederverwendbare Module ersetzen (vgl. [JS01]). Hierbei definiert das Komponentenmodell die Struktur einer Software-Komponente. Die Plattformunabhängigkeit von Java ermöglicht, daß eine einmal entwickelte Komponente auf verschiedenen Systemen einsetzbar ist, solange dort eine Java-Umgebung verfügbar ist. Diese Austauschbarkeit wird außerdem durch die Konfigurierbarkeit von Komponenten gewährleistet. Das Verhalten von fertigen Komponenten wird nicht durch Eingriff in den Code, sondern über Konfigurationsparameter angepaßt. So können selbstentwickelte Komponenten mit zugekauften Komponenten zu einer Applikation zusammengefügt werden. Beispiele für Komponentenmodelle sind Applets (Browser), Servlets und Java Server Pages (Webserver) sowie Enterprise JavaBeans (EJB-Server). In Klammern angegeben ist jeweils der zur Ausführung nötige Container. Dieser Container stellt eine Laufzeitumgebung für Komponenten zur Verfügung. Eine Komponente wird dann im sogenannten Kontext des Containers ausgeführt. 18 2.4 Die Extensible Markup Language Bei allen Komponentenmodellen sind eine Reihe von Interfaces definiert, die von den Komponenten implementiert werden müssen. Diese Interfaces enthalten Callback-Methoden, die dem Container das Management des Objekt-Lebenszyklus sowie die Benachrichtigung der Komponente über Methodenaufrufe ermöglichen. Vor- und Nachteile von Java Die wichtigsten Vorteile von Java sind Plattformunabhängigkeit, Sicherheit und Robustheit. So bietet die Sprache Java moderne objektorientierte Sprachkonzepte und bleibt trotz umfangreicher API einfach und kompakt, da auf Mehrfachvererbung und Operatorüberladung verzichtet wurde. Eine hohe Sicherheit wird unter anderem durch den Verzicht auf Zeigerarithmetik und den Einsatz einer virtuellen Maschine gewährleistet. So ist ausgeschlossen, daß eine Java-Applikation ein System zum Absturz bringen kann. Lediglich die virtuelle Maschine, in der die Applikation läuft, kann abstürzen. JavaApplikationen liegen nicht als nativer Maschinencode vor, sondern als Bytecode. Dadurch sind Java-Applikationen portabel und können auf beliebigen javafähigen Plattformen ausgeführt werden. Der am häufigsten genannte Nachteil ist noch immer die geringere Ausführungsgeschwindigkeit von Java-Applikationen gegenüber Programmen aus anderen Sprachen. Da Java-Applikationen in Bytecode vorliegen und nicht in Maschinensprache, muß die Java Virtual Machine diesen zur Laufzeit interpretieren und ausführen. In den letzten Jahren wurden die sogenannten Just In Time Compiler eingeführt, die den Bytecode zur Laufzeit in Maschinensprache übersetzen und so vor allem bei häufig ausgeführten Codeblöcken zu einer merklichen Erhöhung der Geschwindigkeit führen. 2.4 Die Extensible Markup Language Bei der Extensible Markup Language - kurz XML - handelt es sich um eine textbasierte Beschreibungssprache. Es soll hier kurz auf die wichtigsten Eigenschaften von XML eingegangen werden, da alle im Verlauf der Arbeit vorgestellten Werkzeuge mehr oder weniger extensiv XML einsetzen, um notwendige Information zu speichern. Die Entwicklung von XML begann im Jahre 1996, im Jahre 1998 wurde die XML-Spezifikation in der Version 1.0 vom World Wide Web Consortium (W3C) zum Standard erhoben. Seitdem hat sich XML zur zentralen Technologie der Datenrepräsentation in Java-Systemen entwickelt. Im Jahr 2000 folgten dann einige Detailverbesserungen und Präzisierungen des Standards (vgl. [Se01], [Mc01]). XML ist eine sogenannte Metasprache, mit der weitere Sprachen definiert werden können. Dazu gehören unter anderem HTML6 , WML7 und SVG8 . Die wichtigste Eigenschaft von XML ist die Strukturierung der Daten durch eigene Tags und Attribute. Dabei definiert der Standard weder Tags noch Grammatik, sondern lediglich die allgemeine Syn6 HTML: Hypertext Markup Language WML: Wireless Markup Language 8 SVG: Scalable Vector Graphics 7 2 | Grundlagen 19 tax. Dadurch ist XML hochflexibel und erweiterbar und der Inhalt der Daten kann unter Beachtung der Struktur beliebig definiert werden. Der Standard beschreibt zwei grundlegende Konzepte: Zum einen muß ein XML-Dokument wohlgeformt sein; das bedeutet, daß jeder geöffnete Tag einen dazugehörigen schließenden Tag besitzen muß und die Schachtelungsreihenfolge nicht durchbrochen werden darf. Ein XML-Dokument ist also genau dann wohlgeformt, wenn es eine korrekte Syntax in Bezug auf die Spezifikation hat. Zum anderen kann ein XML-Dokument gültig sein, das heißt, es gehorcht einer Document Type Definition (DTD). Die DTD ist Bestandteil der XML-Spezifikation und definiert die Grammatik eines XML-Dokuments, also die Menge der erlaubten Tags und Attribute sowie mögliche Einschränkungen der Attributwerte. Wenn ein XML-Dokument also eine DTD spezifiziert und deren Regeln folgt, so ist es ein gültiges XML-Dokument. Das folgende Beispiel zeigt ein einfaches, wohlgeformtes XML-Dokument, das eine externe DTD einbindet und somit auch gültig ist: <?xml version="1.0" encoding="iso-8859-1" ?> <!DOCTYPE locations SYSTEM "locations.dtd"> <locations> <location> <name>Falkensee</name> <position latitude="52.5667" longitude="13.1" /> <zip>14612</zip> </location> </locations> Jedes XML-Dokument besteht aus einem Kopf mit Steueranweisungen für Parser9 und dem eigentlichen Inhalt. Die erste Zeile jedes XML-Dokuments enthält eine XML-Anweisung, die neben der Version als optionales Attribut das Encoding enthält, also mit welchem Zeichensatz das Dokument verfaßt ist. Standardeinstellung ist UTF-8, werden deutsche Umlaute verwendet, so muß ISO-8859-1 angegeben werden. Die nächste Anweisung gibt den Dokumenttyp sowie den Typ und den Pfad zur DTD an. Dabei steht SYSTEM für eine im lokalen Dateisystem abgelegte DTD, PUBLIC für eine öffentlich zugängliche DTD, die dann über einen vollständigen URI10 angegeben wird. Zwischen dem öffnenden und schließenden Tag des Wurzelelements stehen dann die eigentlichen Daten. Da es sich bei XML um kein proprietäres Dateiformat handelt, sondern um reinen Text, ist einer der Vorteile von XML die Plattformunabhängigkeit, da solche Dateien auf jeder Plattform gleichartig ausgewertet werden können. Als Folge ergeben sich als weitere Vorteile die hohe Skalierbarkeit sowie die immer stärker werdende Verbreitung. Es ergeben sich allerdings auch zwei Nachteile: Zum einen fehlt ein Sicherheitskonzept, da 9 10 Programm, das XML-Dateien verarbeitet URI: Uniform Resource Indicator 20 2.4 Die Extensible Markup Language XML textbasiert ist. Zum anderen enthält jedes XML-Dokument aufgrund seiner Struktur einen erheblichen Overhead an nicht nutzbaren Daten. Java & XML Aus den vorangegangenen Abschnitten wird klar, warum Java und XML so gut zusammenarbeiten: Während Java portablen Code liefert, der auf jedem Betriebssystem mit einer JVM läuft, steuert XML portable Daten bei (nach [Mc01]). Beide Technologien sind standardisiert, außerdem ist Java die Sprache mit der größten Unterstützung für die Verarbeitung von XML durch eine Vielzahl von Werkzeugen und APIs. Erst durch den Einsatz beider Technologien werden Anwendungen flexibel und können plattformunabhängig eingesetzt werden. 3 | Enterprise JavaBeans Bei Enterprise JavaBeans (EJBs) handelt es sich um ein verteiltes, transaktionales, serverseitiges Komponentenmodell speziell für Geschäftsanwendungen. Enterprise JavaBeans sind die zentralen Komponenten der Java 2 Enterprise Edition und werden in einer formalen Spezifikation beschrieben. Neben den EJBs selbst beschreibt diese Spezifikation auch detailliert die Umgebung und die zur Verfügung stehenden Dienste wie Transaktionen, Sicherheit, Persistenz und Namensdienst. Auf der Basis der Spezifikation können Applikationsserver implementiert werden, welche den installierten EJBs die genannten Dienste zur Verfügung stellen. Version 1.0 der EJB-Spezifikation wurde im März 1998 veröffentlicht und im Dezember 1999 durch Version 1.1 aktualisiert. Im August 2001 wurde Version 2.0 verabschiedet, sie bietet wesentliche Verbesserungen und Erweiterungen. Basierend auf der Version 2.0 [EJB2] soll im Rahmen dieses Kapitels ein kurzer Einblick in die Komponenten der Spezifikation gegeben werden. Allerdings soll der Fokus auf der Realisierung der Persistenz innerhalb dieses Komponentenmodells liegen. 3.1 Die EJB-Architektur Die EJBs als Kernkomponenten der EJB-Spezifikation stellen den Clients serverseitige Geschäftslogik in Form von Interfaces zur Verfügung. Enterprise JavaBeans werden in einem EJB-Container installiert (deployt), der ihnen die Laufzeitumgebung zur Verfügung stellt. Er verwaltet die installierten Instanzen, steuert ihren Lebenszyklus und bietet jeder Komponente über Schnittstellen den Zugang zu weiteren installierten Diensten an. Zu diesen Diensten gehören, wie eingangs erwähnt, unter anderem ein Transaktionsdienst (via JTA), Datenbankzugriff (via JDBC) und Messaging (via JMS). Zusammen mit weiteren Containern ist der EJB-Container Bestandteil eines J2EE-konformen Applikationsservers. Der Server hat die Aufgabe, alle Container mit grundlegender Funktionalität wie z.B. Namensdienst, Fehlermanagement, Prozeßmanagement und Lastausgleich zu versorgen. Außerdem gewährleistet er Skalierbarkeit, Verfügbarkeit und die Anbindung an die Kommunikations-Infrastruktur (nach [BG02], [DP02]). Für die Entwicklung und den Einsatz von EJBs definiert die Spezifikation außerdem Szenarien und Rollen, um die Entwicklung von Anwendungen für die Beteiligten zu vereinfachen [BG02]. Zu diesen Rollen gehören unter anderem der Bean-Entwickler, der die eigentliche Entwicklung einer Komponente und ihrer Geschäftslogik übernimmt, der 22 3.2 Programmier-Restriktionen Anwendungs-Assembler, der verfügbare Komponenten zu einer Anwendung zusammenstellt sowie der Deployer, der eine J2EE-Anwendung in einem dafür vorgesehenen J2EE-Applikationsserver installiert. 3.2 Programmier-Restriktionen Damit die Portabilität von EJBs gewahrt bleibt und eine EJB in jedem EJB-Container installiert werden kann, muß der Bean-Entwickler laut Spezifikation einige Restriktionen beachten. • Der schreibende Zugriff auf statische Variablen ist untersagt, daher sollten alle statischen Variablen in einer EJB als final deklariert werden. • Eine EJB darf keine Anweisungen zur Thread-Synchronisation benutzen. Ebensowenig darf eine EJB eigene Threads starten oder stoppen. • Eine EJB darf keine AWT1 -Funktionalitäten benutzen, um Ausgaben zu realisieren oder Eingaben von der Tastatur zu lesen. • Die Benutzung des Packages java.io zum Lesen und Schreiben von Dateien ist untersagt. • Die Nutzung von Teilen des Packages java.security, um Sicherheitsrichtlinien zu beeinflussen, ist verboten. • Das Setzen einer Socket-Factory, die Erzeugung von Sockets und das Akzeptieren von Verbindungen auf Sockets ist verboten. • Die Nutzung der Reflection API, um Informationen über Klassen zu erhalten, ist untersagt. • Eine EJB darf die Laufzeitumgebung nicht beeinflussen. Das bedeutet, sie darf keinen Classloader erzeugen oder benutzen, keinen Security-Manager setzen, die JVM nicht anhalten und die Ein-/Ausgabestreams nicht verändern. • Eine EJB darf keine nativen Bibliotheken laden. • Eine EJB darf das Schlüsselwort this nie als Argument oder Rückgabewert eines Methodenaufrufs nutzen. 3.3 Bestandteile einer EJB Eine Enterprise JavaBean ist nicht nur eine einfache Java-Klasse, sondern kann sich je nach Art aus mehreren Komponenten zusammensetzen (siehe auch Tabelle 3.2). Hierzu gehören: 1 Abstract Windowing Toolkit 3 | Enterprise JavaBeans 23 • Home-Interface Das Home-Interface bietet Methoden für die Verwaltung des Lebenszyklus sowie Lokalisierungsdienste an. • Komponenten-Interface Das Komponenten-Interface dient als Schnittstelle zu den Geschäftsmethoden einer Bean. • Bean-Klasse2 Die Bean-Klasse enthält die Implementierung der Geschäftslogik. Jede Methode, die in einem der beiden zuvor genannten Interfaces deklariert ist, enthält eine korrespondierende Methode in der Bean-Klasse. • Deployment-Deskriptor Der Deployment-Deskriptor enthält die Konfiguration der Bean, unter anderem alle zur Bean gehörigen Interfaces sowie Informationen über das Verhalten der Bean in Bezug auf Transaktionen und Sicherheit. Home- bzw. Komponenten-Interface können als lokale oder Remote-Interfaces vorliegen. Vor der EJB 2.0-Spezifikation war der Zugriff auf eine Bean nur über Remote-Interfaces möglich. Erst mit Version 2.0 der EJB-Spezifikation kamen lokale Beans und damit lokale Interfaces hinzu. Sie vermeiden den Kommunikationsaufwand, der bei Remote Beans durch RMI-IIOP3 entsteht. Auf lokale Beans kann nur lokal von anderen Beans oder Webkomponenten wie Servlets, JSPs oder Taglibs zugegriffen werden, nicht jedoch von entfernten Clients. Lokal bedeutet in diesem Zusammenhang, daß der Client in der gleichen Java Virtual Machine laufen muß, wie die lokale Bean, auf die er zugreifen möchte. Tut er dies nicht, so handelt es sich um einen entfernten Client, der dann auch die Remote-Interfaces für den Zugriff auf eine Bean benutzen muß. Bei CMP-Entity Beans sind lokale Interfaces die Voraussetzung für die Realisierung von Beziehungen. Bei der Installation einer EJB im EJB-Container - dem sogenannten Deployment - erzeugt der Container mit Hilfe der zur Bean gehörigen Interfaces konkrete Klassen. Aus dem Home-Interface wird das Home-Objekt erzeugt, das als Factory für die Bean-Instanz dient. Die meisten Container erzeugen für eine installierte EJB genau ein Home-Objekt, auf das alle Clients zugreifen. Aus dem Komponenten-Interface wird das EJB-Objekt erzeugt, welches als Wrapper für die Bean-Instanz dient. Hier wird der Container meist genau so viele Objekte erzeugen, wie Clients verbunden sind (nach [BG02]). Mit den erzeugten Home- bzw. EJB-Objekten fängt der Container alle Aufrufe an die Bean ab und kann so vor der Weiterleitung an die eigentliche Bean-Instanz Dienste des Systems ausführen. Ein Client greift also zu keiner Zeit direkt auf eine Bean-Instanz zu, sondern immer über das Home- oder EJB-Objekt. 2 3 auch: Bean-Implementierung RMI-IIOP: Remote Method Invocation over Internet Inter-Orb Protocol 24 3.3 Bestandteile einer EJB Abbildung 3.1: Bestandteile einer EJB 3.3.1 Das Home-Interface Das Home-Interface dient als Factory einer Bean, das heißt, mit Hilfe dieses Interfaces können Instanzen der Bean vom Client erzeugt werden. Hierzu stellt das Interface eine Reihe von Create-Methoden mit unterschiedlichen Signaturen bereit, die aber alle mit dem Präfix create... beginnen müssen. Jede im Home-Interface deklarierte CreateMethode muß in der Bean-Klasse eine korrespondierende Methode haben. Der Rückgabewert einer Create-Methode ist das Komponenten-Interface der Bean, bei einer lokalen Bean das lokale Komponenten-Interface. Auch das Entfernen einer Bean geschieht über das Home-Interface. Hierzu steht die Methode remove() bereit, die jedoch nicht explizit deklariert werden muß, da sie vom Interface EJBHome bzw. im Falle einer lokalen Bean vom Interface EJBLocalHome geerbt wird. Es folgt ein Code-Beispiel für das Home-Interface einer Bean. Da dieses vom Interface EJBHome erbt, handelt es sich um ein Remote Home-Interface, womit jede deklarierte Methode in ihrer throws-Klausel mindestens RemoteException deklarieren muß; bei Create-Methoden kommt zusätzlich CreateException hinzu. Methoden lokaler Beans werfen keine RemoteExceptions, da diese Beans nicht über RMI-IIOP kommunizieren. 3 | Enterprise JavaBeans 25 public interface LocationEntityHome extends EJBHome { public LocationEntity create(Long id, String name) throws CreateException, RemoteException; } Der Container stellt beim Deployment das Home-Objekt zur Verfügung, welches das Home-Interface implementiert. Dieses Home-Objekt wird innerhalb des EJB-Servers instantiiert, an den Namensdienst gebunden und somit den Clients als Factory für die Enterprise Bean verfügbar gemacht. Um eine Bean-Instanz zu erzeugen oder aufzufinden, sucht ein Client zunächst im Namenskontext das Home-Interface und kann danach über das erhaltene Objekt mit der Bean arbeiten. 3.3.2 Das Komponenten-Interface Das Komponenten-Interface enthält alle Geschäftsmethoden einer Bean, die vom Client aufgerufen werden können. Alle deklarierten Geschäftsmethoden müssen eine entsprechende Methode mit übereinstimmender Signatur in der Bean-Klasse haben. Außerdem dürfen als Parameter und Rückgabewert nur primitive oder serialisierbare Datentypen verwendet werden. Handelt es sich um eine Remote Bean, so erbt das Komponenten-Interface vom Interface javax.ejb.EJBObject. Alle deklarierten Methoden müssen außerdem eine RemoteException in ihrer throws-Klausel deklarieren. Handelt es sich um eine lokale Bean, so erbt das Komponenten-Interface von javax.ejb.LocalObject. public interface LocationEntity extends EJBObject { public Long getId(); public String getName(); public void setName(String name); } Die serverseitige Implementierung des Komponenten-Interfaces ist das EJB-Objekt. Dieses holt sich der Client zunächst über den Aufruf einer Create-Methode aus dem HomeInterface der Bean. Anschließend kann er auf die Geschäftsmethoden der Bean zugreifen. Dabei soll noch einmal darauf hingewiesen werden, daß der Client nie eine Referenz auf die Bean-Instanz erhält, sondern nur auf das EJB-Objekt. Ruft der Client eine Geschäftsmethode auf, so delegiert der Container diesen Aufruf vom EJB-Objekt zur Bean-Instanz. 3.3.3 Die Bean-Klasse Die Bean-Klasse enthält die eigentliche Applikationsfunktionalität. Sie implementiert abhängig von der Art der Bean eines der Interfaces SessionBean, EntityBean oder 26 3.3 Bestandteile einer EJB Gruppe Präfix Create-Methoden Finder-Methoden Home-Methoden Select-Methoden ejbCreate...() ejbFind...() ejbHome...() ejbSelect...() Tabelle 3.1: Spezielle Methoden der Bean-Klasse MessageDrivenBean (siehe Tabelle 3.2). Diese Interfaces enthalten unter anderem Callback-Methoden, die vom Container aufgerufen werden, um den Lebenszyklus einer Bean-Instanz zu verwalten. Neben den Callback-Methoden sind alle in den Home- bzw. Komponenten-Interfaces deklarierten Methoden in der Bean-Klasse mit ihren Implementierungen vorhanden. Keines dieser Interfaces wird jedoch direkt implementiert. Alle speziellen Methoden in der Bean-Klasse beginnen mindestens mit dem Präfix ejb. Hierzu zählen die in Tabelle 3.1 aufgezählten Methoden sowie ejbActivate(), ejbPassivate() usw. Lediglich Geschäftsmethoden, die in den Komponenten-Interfaces deklariert wurden, sind ohne Präfix mit gleicher Signatur in der Bean-Klasse implementiert. Das folgende Beispiel zeigt eine mögliche Bean-Klasse zu den in den beiden vorangegangenen Abschnitten definierten Interfaces (gekürzt um die Callback-Methoden des Containers): public class LocationEntityBean implements EntityBean { public Long create(Long id, String name) { ... } public Long getId() { ... } public void setId(Long id) { ... }; public String getName() { ... }; public void setName(String name) { ... }; } Da die Bean-Klasse das Interface javax.ejb.EntityBean implementiert, handelt es sich um eine Entity Bean. Neben der im Home-Interface deklarierten Create-Methode sind die Geschäftsmethoden aus dem Komponenten-Interface vorhanden. Nicht aufgeführt ist die Methode setEntityContext(), die vom implementierten Interface geerbt wird. Durch diese Methode erhält die Bean-Klasse den Kontext vom Container und kann ihn in einem Instanzattribut speichern. Ähnliche Methoden existieren für Session Beans und Message Driven Beans ebenfalls. Während alle Methoden in den RemoteInterfaces in ihrer Signatur mindestens RemoteException deklarieren, entfällt die Deklaration in der Bean-Klasse. 3 | Enterprise JavaBeans 3.3.4 27 Der Deployment-Deskriptor Der Deployment-Deskriptor enthält die Konfiguration einer oder mehrerer Enterprise JavaBeans. Diese umfaßt die Angabe aller zur Bean gehörigen Interfaces sowie der BeanKlasse, Referenzen zu anderen EJBs, Sicherheitsrollen usw. Mit den Konfigurationsangaben wird auch das Verhalten der Bean in Bezug auf Transaktionen kontrolliert. Der Deployment-Deskriptor liegt im XML-Format vor und ist somit zwischen den unterschiedlichen benutzten Plattformen und Werkzeugen portabel. <ejb-jar> <enterprise-beans> <session /> <entity> <ejb-name>LocationEJB</ejb-name> <home>LocationHome</home> <remote>Location</remote> <local-home>LocationEntityLocalHome</local-home> <local>LocationEntityLocal</local> <ejb-class>LocationEntityBean</ejb-class> [...] </entity> <message-driven /> </enterprise-beans> <assembly-descriptor /> </ejb-jar> Das Wurzelelement enthält ein Element <enterprise-beans>, in dem alle Enterprise JavaBeans mit ihren Konfigurationsdaten enthalten sind. Dabei existiert für jede Art EJB ein eigenes Element. Mindestangaben für eine EJB sind ein eindeutiger Name für die Komponente (<ejb-name>) und die Bean-Klasse (<ejb-class>). Abhängig davon, ob eine EJB lokale und/oder Remote-Interfaces anbietet, werden diese in entsprechenden Elementen angegeben: <home> für das Remote Home-Interface, <remote> für das Remote Komponenten-Interface, <local-home> für das lokale Home-Interface, <local> für das lokale Komponenten-Interface. Der Name der zugehörigen Klasse bzw. des zugehörigen Interfaces muß in allen Fällen vollqualifiziert angegeben werden. Unterhalb des optionalen <assembly-descriptor>-Elements können Sicherheitsrollen und Transaktionsattribute definiert werden sowie Angaben zur Zugriffskontrolle für Methoden gemacht werden. 3.4 Arten von EJBs Die EJB 2.0 Spezifikation definiert drei Arten von Enterprise JavaBeans: Session Beans, Entity Beans und Message Driven Beans. Eine Session Bean bietet einen Dienst in Form von 28 3.4 Arten von EJBs Geschäftsmethoden an und liegt in einer von zwei Varianten vor: als Stateless Session Bean oder als Stateful Session Bean. Eine Stateless Session Bean ist zustandslos und kann von mehreren Clients gleichzeitig benutzt werden. Eine Stateful Session Bean speichert einen Zustand und wird mit genau einer Client-Sitzung assoziiert. Eine Entity Bean repräsentiert ein persistentes Objekt mit seinen Daten und Zugriffsmethoden. Die Verwaltung der Persistenz kann von der Bean selbst übernommen werden oder dem EJB-Container überlassen werden. Eine Message Driven Bean stellt einen Nachrichtenempfänger dar, der auf asynchrone Nachrichten reagiert. Tabelle 3.2 gibt einen Überblick über die Arten von Enterprise JavaBeans sowie deren Aufgabe und Bestandteile. Session Beans und Entity Beans können vom Container bei Bedarf aus dem Hauptspeicher entfernt und in den sekundären Speicher ausgelagert werden. Dies geschieht entweder bei Speichermangel oder nach einem bestimmten Timeout. Der Vorgang des Auslagerns wird laut Spezifikation als Passivierung bezeichnet. Derart ausgelagerte EJBs werden durch Aufruf einer Geschäftsmethode durch den Client wieder reaktiviert, also im Hauptspeicher wiederhergestellt. Dieser Prozeß wird als Aktivierung bezeichnet. Um eine EJB über die Passivierung bzw. Aktivierung zu benachrichtigen, stehen die Callback-Methoden ejbPassivate() und ejbActivate() zur Verfügung. Der Container ruft ejbPassivate() unmittelbar vor der Auslagerung auf. Eine Bean sollte in dieser Methode alle Systemressourcen freigeben, die nicht ausgelagert werden dürfen (z.B. Datenbank-Verbindungen). Die Methode ejbActivate() wird vom Container aufgerufen, sobald eine Bean aktiviert wurde; in ihr können dann alle bei der Passivierung freigegebenen Ressourcen wieder angefordert werden. 3.4.1 Session Beans Eine Session Bean repräsentiert einen serverseitigen Dienst und modelliert Abläufe oder Vorgänge der Geschäftslogik einer Applikation. Session Beans werden daher auch als „verlängerter Arm des Clients auf dem Server“ beschrieben [EK02]. Session Beans implementieren das Interface javax.ejb.SessionBean und werden im Deployment-Deskriptor unterhalb des Elements <enterprise-beans> deklariert. Für Session Beans steht das Element <session> zur Verfügung, darunter erfolgen alle Angaben gemäß Abschnitt 3.3.4 sowie zusätzlich die Angabe der Ausprägung im <session-type>-Element, gültige Werte sind Stateful und Stateless. <session> [...] <session-type>Stateless</session-type> </session> Stateful Session Beans Stateful Session Beans werden verwendet, um eine Client-Sitzung zu verwalten und sitzungsbezogene Aufgaben auszuführen. Wenn ein Client eine Stateful Session Bean instantiiert, so folgt daraus eine eindeutige Zuordnung der Session Bean-Instanz zum entsprechenden Client [BG02]. Die Bean-Instanz ist für den Client dann exklusiv verfügbar 3 | Enterprise JavaBeans 29 Session Bean Entity Bean Zweck Prozeß für einen Client Ausprägung Stateless, Stateful gemeinsame Nutzung Stateful Session Bean einem Client zugeordnet; bei Stateless Session Beans keine Zuordnung transient, Lebenszyklus durch Client bestimmt Darstellung von Daten, Objekt im persistenten Speicher Bean Managed Persistence, Container Managed Persistence gemeinsame Nutzung durch mehrere Clients Persistenz HomeInterface KomponentenInterface DeploymentDeskriptor Bean-Klasse Bean-Klasse implementiert javax.ejb. Message Driven Bean Nachrichtenempfänger - keine direkte Zuordnung zu Clients transient, „stirbt“ bei Container-Terminierung lokal, remote persistent, Zustand bleibt im persistenten Speicher auch nach ContainerTerminierung erhalten lokal, remote lokal, remote lokal, remote nein ja ja ja ja SessionBean ja, abstrakt bei CMP EntityBean ja MessageDrivenBean nein Tabelle 3.2: Überblick über Enterprise JavaBeans (nach [BG02], [EK02]) 30 3.4 Arten von EJBs und speichert die Zustandsinformationen der Client-Sitzung. Der Client bestimmt den Lebenszyklus der ihm zugeordneten Session Bean-Instanz; die Lebensdauer ist meist auf die Dauer der Client-Sitzung begrenzt. !" # !" # Abbildung 3.2: Lebenszyklus einer Stateful Session Bean (nach [BG02], [EK02]) Eine Session Bean kann eine beliebige Anzahl von Create-Methoden mit unterschiedlichen Parametern besitzen, die der Initialisierung des Zustands dienen. Methodenaufrufe des Clients werden in den Zuständen bereit und bereit in TX entgegengenommen, eine Session Bean kann also einen Methodenaufruf innerhalb einer Transaktion ausführen. Stateless Session Beans Stateless Session Beans speichern keine Zustandsinformationen, daher kann ein beliebiger Client eine beliebige Instanz aus dem Pool benutzen. Der EJB-Container hält für diesen Zweck meist eine bestimmte Anzahl von Instanzen bereit, die von mehreren Clients geteilt werden. Es kann aber nur ein Client eine Methode zu einer bestimmten Zeit ausführen. Der Lebenszyklus einer Stateless Session Bean ist vergleichsweise einfach. Es existieren nur zwei Zustände: nicht existent und bereit. Da keine Zustandsverwaltung stattfindet, werden Stateless Session Beans auch nicht passiviert: Muß Speicher freigegeben werden, so werden einfach Instanzen aus dem Pool gelöscht. Zu einem Datenverlust kommt es nicht. Neben einer normalerweise parameterlosen Create-Methode besitzt eine Stateless Session Bean eine Reihe von Geschäftsmethoden, denen aufgrund des nicht vorhandenen 3 | Enterprise JavaBeans 31 Abbildung 3.3: Lebenszyklus einer Stateless Session Bean (nach [BG02], [EK02]) Zustands alle für die Ausführung nötigen Parameter übergeben werden; man kann eine Stateless Session Bean also mit einer Java-Klasse vergleichen, die statische Methoden anbietet. 3.4.2 Entity Beans Entity Beans werden benutzt, um persistente Objekte darzustellen; die häufigste Anwendung ist die Repräsentation von Daten einer relationalen Datenbank-Tabelle, wobei eine einfache Entity Bean genau eine Zeile der Relation darstellt. Entity Beans ermöglichen mehreren Clients den parallelen Zugriff auf transaktionsgesicherte Daten [DP02]. Sie sind also nicht wie eine Stateful Session Bean mit einer ClientSitzung assoziiert, sondern stehen allen Clients gleichermaßen zur Verfügung. Der Container ermöglicht den exklusiven Zugriff über den Einsatz von Transaktionen [DP02]. Eine Entity Bean definiert Methoden (u.a. Finder-, Home- und Select-Methoden), Attribute, Beziehungen und einen Primärschlüssel [DP02]. Sie implementiert außerdem das Interface javax.ejb.EntityBean und kann beliebig viele Create-Methoden haben. Zu jeder ejbCreate()-Methode in der Bean-Klasse muß jedoch eine korrespondierende ejbPostCreate()-Methode existieren. Der Rückgabewert der Create-Methode(n) entspricht dem Datentyp des Primärschlüssels der Entity Bean. Zusätzlich zu den Create-Methoden kann eine Entity Bean Finder-Methoden definieren, die dem Auffinden von Entity Bean-Instanzen dienen. Wird eine Finder-Methode aufgerufen, so nutzt der Container eine Entity Bean-Instanz ohne Identität aus dem InstanzenPool. Für diesen Zweck hält der Container meist eine bestimmte Anzahl anonymer BeanInstanzen bereit. Finder-Methoden werden im Home-Interface unter Verwendung des Präfixes findBy deklariert und haben eine korrespondierende Methode in der Bean-Klasse, die mit dem Präfix ejbFindBy beginnt. Alle Finder-Methoden müssen zudem eine Ausnahme vom Typ FinderException deklarieren. Mindestens eine dieser Finder-Methoden muß deklariert werden: findByPrimaryKey(). Sie dient dem Auffinden genau einer Instanz anhand des Primärschlüssels. Während die im Home-Interface deklarierten Finder-Methoden das Komponenten-Interface (also das EJB-Objekt) als Rückgabewert haben, liefern die Implementierungen in der Bean-Klasse den Primärschlüssel an den Container. Finder-Methoden, die mehr als ein Ergebnis liefern, haben als Rückgabewert Collection oder Set. Die Anwendung 32 3.4 Arten von EJBs von Set schließt Duplikate in der Ergebnismenge aus, entspricht also einem SELECT DISTINCT. Der Container nutzt den oder die ermittelten Schlüssel anschließend, um ein oder mehrere neue EJB-Objekte zu erzeugen. Durch den Aufruf einer Finder-Methode werden folglich keine Bean-Instanzen erzeugt, sondern nur EJB-Objekte, die den Primärschlüssel enthalten. Die zugehörige Bean-Instanz wird erst erzeugt, wenn beispielsweise ein Client eine Methode des EJB-Objekts aufruft (Lazy Initialization). Der Container sorgt außerdem dafür, daß pro Datenbankzeile nur eine Entity Bean-Instanz vorhanden ist. Der eigentliche Zweck der Finder-Methoden ist somit lediglich das Ermitteln der Primärschlüssel-Daten aus der Datenbank. Die Deklaration einer Entity Bean erfolgt im Deployment-Deskriptor unterhalb des Elements <entity>. Es erfolgen alle Angaben gemäß Abschnitt 3.3.4 sowie abhängig von der Art der Persistenzverwaltung (siehe Abschnitt 3.5). Außerdem anzugeben ist, ob die Bean reentrant sein soll oder nicht; gültige Werte des Elements <reentrant> sind True und False. Der Lebenszyklus Der Lebenszyklus einer Entity Bean weist einige wichtige Unterschiede zum Lebenszyklus einer Session Bean auf. Abbildung 3.4 stellt alle Zustände des Lebenszyklus inklusive der Übergänge dar. Abbildung 3.4: Lebenszyklus einer Entity Bean (nach [BG02], [EK02]) Nachdem eine Entity Bean-Instanz erzeugt wurde, handelt es sich um eine anonyme Instanz, die der Container in einem Pool bereithält, damit Clients beispielsweise FinderMethoden aufrufen können. Wenn eine Create-Methode aufgerufen wird, erzeugt der Container eine neue Zeile in der Datenbank und verbindet eine Bean-Instanz mit einer 3 | Enterprise JavaBeans 33 Bean-Identität. Ab diesem Zeitpunkt repräsentiert die Bean die persistenten Daten in der Datenbank und befindet sich im Zustand bereit. Wird die Methode remove() aufgerufen, so wird nicht wie bei Session Beans die Instanz aus dem Container entfernt, sondern zunächst die repräsentierte Zeile aus der Datenbank gelöscht. Anschließend wird die Bean-Instanz wieder als anonyme Instanz in den Pool überführt. Die Auslagerung von Entity Beans erfolgt ähnlich wie bei Session Beans mit Hilfe der Methoden ejbPassivate() und ejbActivate(). Die Passivierung entspricht hierbei jedoch nicht der Auslagerung in einen Sekundärspeicher, sondern der Trennung der Bean-Identität von einer Bean-Instanz und Rückführung der Bean-Instanz in den Pool als anonyme Entity Bean-Instanz. Diese Instanz steht dann wieder für den Aufruf von Finder-Methoden zur Verfügung. Der Primärschlüssel Jede Entity Bean besitzt ein oder mehrere Attribute, die zum Primärschlüssel gehören. Der Primärschlüssel dient als eindeutiger Identifikator für eine Bean-Instanz. Eine Instanz kann immer über die Methode findByPrimaryKey(Object pk) aufgefunden werden. Diese Methode wird vom Home-Objekt zur Verfügung gestellt. Außerdem stellt der Kontext die Methode getPrimaryKey() zur Verfügung, mit welcher der Wert des Primärschlüssels ermittelt werden kann. Somit kann jede Entity Bean immer mit ihrem Home-Objekt und dem Primärschlüssel identifiziert werden. Man bezeichnet das HomeObjekt zusammen mit einem konkreten Wert des Primärschlüssels auch als Bean-Identität [DP02]. Handelt es sich um einen zusammengesetzten Primärschlüssel, also einen Schlüssel, zu dem mehrere Attribute der Entity Bean gehören, so müssen diese Attribute zusätzlich in einer sogenannten Primärschlüsselklasse zusammengefaßt werden. Darin hat jedes Attribut den gleichen Namen und Datentyp wie in der Entity Bean. Außerdem muß diese Primärschlüsselklasse das Interface Serializable sowie die Methoden equals() und hashCode() implementieren. Die Primärschlüsselklasse einer Entity Bean wird mit ihrem vollqualifizierten Namen im <prim-key-class>-Element des Deployment-Deskriptors angegeben. Für nichtzusammengesetzte Primärschlüssel gibt das Element <primkey-field> das Attribut einer CMP-Entity Bean an, welches den Primärschlüssel enthält. Dabei müssen die Datentypen von Attribut und Primärschlüsselklasse übereinstimmen. <entity> <primkey-field>id</primkey-field> <prim-key-class>java.lang.Long</prim-key-class> [...] </entity> Im Normalfall obliegt der Bean-Klasse die Erzeugung eines Primärschlüssels für eine Instanz. Die meisten EJB-Container bieten für CMP-Entity Beans einen Key-Generator an. 34 3.4 Arten von EJBs Das Session Facade Pattern Entity Beans können ebenso wie Session Beans als lokale oder Remote Beans realisiert werden. Handelt es sich allerdings um CMP-Entity Beans, die zudem auch noch in Beziehungen zueinander stehen, so müssen sie als lokale Beans vorliegen. Damit sind sie von entfernten Clients aus nicht mehr erreichbar. Als Lösung bietet sich das sogenannte Session Facade Pattern an, welches eine Session Bean als Vermittler zwischen Clients und lokalen Entity Beans nutzt. Man spricht hierbei von einer sogenannten Session Fassade (siehe Abbildung 3.5). Beim Client kann es sich um einen lokalen oder entfernten Client handeln. Er benutzt die Session Bean, um indirekt auf die lokalen Entity Beans zuzugreifen. Abbildung 3.5: Prinzip einer Session Fassade Damit eine Bean auf eine andere Bean zugreifen kann, muß zunächst im DeploymentDeskriptor die lokale Referenz deklariert werden. Dies geschieht unterhalb des Elements <ejb-local-ref>. Es muß ein eindeutiger Name für die Referenz angegeben werden (<ejb-ref-name>), dieser wird später beim Lookup der Bean verwendet. Außerdem anzugeben ist die Art der referenzierten Bean (<ejb-ref-type>, im Falle einer Session Fassade also eine Entity Bean), das Home- und Komponenten-Interface sowie der Name der EJB, die referenziert wird. Dieser Name muß mit dem übereinstimmen, der im Element <ejb-name> der referenzierten Bean angegeben ist. <session> <ejb-name>StorageEJB</ejb-name> <ejb-local-ref> <ejb-ref-name>ejb/LocationEntity</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <local-home>LocationLocalHome</local-home> <local>LocationLocal</local> <ejb-link>LocationEJB</ejb-link> 3 | Enterprise JavaBeans 35 </ejb-local-ref> [...] </session> Die Session Bean hält üblicherweise Referenzen zu den Home-Objekten der verwendeten Entity Beans und kann diese auf die übliche Art und Weise benutzen. public class StorageBean implements SessionBean { private LocationEntityLocalHome locationHome; public void ejbCreate() throws CreateException { Context ctx = new InitialContext(); this.locationHome = (LocationEntityLocalHome) ctx.lookup("java:comp/env/ejb/LocationEntity"); } public void deleteLocation(Long id) { this.locationHome.remove(id); } } 3.4.3 Message Driven Beans Message Driven Beans (MDBs) sind transaktionale, zustandslose, serverseitige Komponenten, die JMS-Nachrichten empfangen und verarbeiten. Eine MDB besitzt keine Home- und Komponenten-Interfaces, sondern besteht nur aus einer Bean-Klasse und einem Deployment-Deskriptor. Damit kann ein Client auch nicht über einen JNDI-Lookup und RMI auf eine solche Bean zugreifen, sondern nur über das Absenden von Nachrichten per JMS. Client und MDB sind also völlig unabhängig voneinander, da der Client den oder die Nachrichtenempfänger nicht kennt. Auch MDBs können vom Container passiviert werden. Beim Nachrichtenempfang stellt der Container die Aktivierung sicher, so daß die Message Driven Bean die eintreffende Nachricht verarbeiten kann. Die Verarbeitung von ankommenden Nachrichten erfolgt seriell, MDBs sind somit nicht reentrant. Abbildung 3.6 stellt den Lebenszyklus einer Message Driven Bean dar. Jede MDB besitzt eine parameterlose Create-Methode und implementiert neben dem Interface MessageDrivenBean auch das Interface MessageListener, von dem es die Methode onMessage(Message m) erbt. Im Deployment-Deskriptor wird eine MDB mit dem Element <message-driven> deklariert. Neben der Bean-Klasse ist die Art der abonnierten Nachrichten (Topic oder Queue) anzugeben. Für die zu verarbeitenden Nachrichten kann außerdem ein Filter spezifiziert werden. Auch die Teilnahme an Transaktionen ist festzulegen, es gilt allerdings die Beschränkung auf Required und NotSupported. 36 3.5 Persistenzmechanismen Abbildung 3.6: Lebenszyklus einer Message Driven Bean (nach [BG02], [EK02]) 3.5 Persistenzmechanismen Der Persistenzmechanismus stellt das Verfahren dar, mit dem der Zustand einer Entity Bean gespeichert und wiederhergestellt werden kann. Er realisiert außerdem das Erzeugen, Auffinden und Löschen von Entity Bean-Instanzen [BG02]. Eine Bean kann den Persistenzmechanismus entweder selbst implementieren (Bean Managed Persistence) oder dies dem EJB-Container überlassen (Container Managed Persistence). Die Art der Persistenzverwaltung wird im Element <persistence-type> angegeben, gültige Werte sind Container für Container Managed Persistence und Bean für Bean Managed Persistence. 3.5.1 Bean Managed Persistence Entity Beans mit Bean Managed Persistence (BMP) kümmern sich selbst um die Kommunikation mit dem Datenspeicher. Alle lesenden und schreibenden Zugriffe werden in der Bean-Klasse programmiert. Der EJB-Container hat keinerlei Wissen darüber, welche Daten von der Entity Bean persistent gemacht werden. Die Entwicklung von BMP Entity Beans bedeutet nicht nur mehr Aufwand bei der Programmierung, sondern auch einen Verlust der Portabilität der Komponenten durch die Anwendung datenbankspezifischer Features. Allerdings können BMP Entity Beans unter anderem in performancekritischen Bereichen eingesetzt werden. Auch können mit ihrer Hilfe komplexe Datentypen wie BLOBs4 gespeichert und Sichten benutzt werden. In der Bean-Klasse müssen mindestens die in Tabelle 3.3 aufgeführten Methoden implementiert werden. Diese sind für die Kommunikation mit der Datenbank verantwortlich. Die Tabelle zeigt, welche Aufgabe jede Methode hat und welche Datenbank-Operation jeweils ausgeführt wird. Für die JDBC-spezifischen Abläufe innerhalb der Methoden sei auf Anhang A.2 verwiesen. Wird eine BMP Entity Bean über eine ihrer Create-Methoden erzeugt, so legt diese zunächst einen neuen Datensatz in der Datenbank an und setzt anschließend die persistenten Bean-Attribute. Wie bereits in Abschnitt 3.4.2 erklärt, wird zum Schluß der Wert des Primärschlüssels an den Container zurückgegeben. Die Methode ejbStore() speichert die Daten der Bean in die Datenbank, mit Hilfe von ejbLoad() werden die Daten aus der Datenbank in die persistenten Attribute der 4 BLOB: Binary Large Object 3 | Enterprise JavaBeans 37 Bean-Methode DB-Operation Aufgabe ejbCreate...() INSERT Erzeugen eines Datensatzes ejbRemove() ejbLoad() ejbStore() ejbFind...() DELETE SELECT UPDATE SELECT Löschen eines Datensatzes Laden der Daten einer Bean Aktualisieren der Daten einer Bean Auffinden von Bean-Instanzen Tabelle 3.3: Zuordnung von Bean-Methoden zu Datenbank-Operationen Bean geladen. Beide Methoden werden vom Container aufgerufen und können als Grenzen einer Transaktion eingesetzt werden. In dem Fall wäre durch die Sperrung der Datenbankzeile sichergestellt, daß die Bean immer mit der Datenbank synchronisiert ist. Folglich sollten die Methoden ejbPassivate() und ejbActivate() nicht zur Synchronisation mit der Datenbank benutzt werden. Die Finder-Methoden einer BMP Entity Bean laden nie die gesamten Daten aus einer Tabelle, sondern lediglich die Primärschlüsselwerte, die dann an den Container zurückgegeben werden. Der Zugriff auf den Datenspeicher mit den persistenten Daten sollte immer über die vom Container zur Verfügung gestellte und über den Namensdienst erreichbare Datenquelle erfolgen. Hierzu muß diese Datenquelle jedoch deklarativ im Deployment-Deskriptor angegeben werden. Notwendige Angaben sind der JNDI-Name und der Datentyp der Datenquelle sowie die Art der Authentifizierung. <entity> [...] <resource-ref> <description>Datenquelle</description> <res-ref-name>DefaultDS</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </entity> Für alle Methoden einer BMP Entity Bean muß im <assembly-descriptor>-Element des Deployment-Deskriptors der Transaktionskontext festgelegt werden (s. Abschnitt 3.6). 3.5.2 Container Managed Persistence Bei der Container Managed Persistence (CMP) werden die Details der Datenspeicherung dem Container überlassen. Der Bean-Entwickler spezifiziert alle persistenten Attribute sowie Beziehungen zu anderen Entity Beans deklarativ im Deployment-Deskriptor 38 3.5 Persistenzmechanismen statt programmatisch in der Bean-Klasse. In der Bean-Klasse muß lediglich ein abstraktes get-/set-Methodenpaar für jedes persistente Attribut existieren. Auch die Bean-Klasse selbst muß als abstrakt deklariert werden. Erst beim Deployment erzeugt der Container eine konkrete Klasse, die alle abstrakten Methoden implementiert. Die Persistenz wird von dieser konkreten Klasse gehandhabt. Wie dies genau geschieht, ist nicht in der EJB 2.0-Spezifikation definiert, sondern der Container-Implementierung überlassen. Die meisten Anbieter verwenden ein objektrelationales Mapping, um die Abbildung auf relationale Tabellen und Tabellenspalten zu realisieren. Einige Applikationsserver liefern zu diesem Zweck auch Deployment-Tools mit, die den Entwickler beim Erstellen eines Mappings unterstützen. Die in der EJB 2.0-Spezifikation standardisierte CMP-Version 2.0 wurde gegenüber der CMP-Version 1.1 stark verbessert und umfaßt neue Features wie Home- und SelectMethoden sowie die Anfragesprache EJB-QL. In der neuen Version kann der Container außerdem Beziehungen zwischen Entity Beans verwalten; zu diesem Zweck wurden die lokalen Beans eingeführt. Da die veraltete CMP-Version 1.1 in der EJB 2.0-Spezifikation weiterhin unterstützt wird, muß im Deployment-Deskriptor die verwendete CMP-Version angegeben werden. Attribute, die vom Container persistent gemacht werden sollen, werden CMP-Felder genannt und mit dem <cmp-field>-Element im Deployment-Deskriptor deklariert. In der Bean-Klasse muß ein get-/set-Methodenpaar nach dem JavaBeans-Standard (siehe Anhang A.5) existieren. Dabei spielt es zunächst keine Rolle, wie vom Client aus auf ein Attribut zugegriffen werden soll. In der Bean-Klasse müssen beide Methoden existieren, da der Container sie für den lesenden bzw. schreibenden Zugriff auf die CMP-Attribute benutzt. Die für den Client ausschlaggebenden Zugriffsmethoden werden im Komponenten-Interface deklariert. Fehlt dort beispielsweise die set-Methode für ein Attribut, so handelt es sich aus der Sicht des Clients um ein Read-Only-Attribut. Der Deployment-Deskriptor definiert neben dem obligatorischen Namen der Bean die zugehörigen Interfaces und die Bean-Klasse sowie alle CMP-spezifischen Angaben. Neben der Angabe des Persistenztyps und des Primärschlüssels gehören dazu die verwendete CMP-Version im Element <cmp-version> sowie der abstrakte Schema-Name im Element <abstract-schema-name>. Der dort vergebene Name dient als Abstraktion vom möglichen Tabellennamen in der Datenbank und wird in EJB-QL-Anfragen benutzt. Für jedes persistente Attribut wird schließlich ein <cmp-field>-Element mit dem Namen des CMP-Attributs angegeben. <entity> [...] <reentrant>False</reentrant> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.Long</prim-key-class> <primkey-field>id</primkey-field> <cmp-version>2.x</cmp-version> 3 | Enterprise JavaBeans 39 <abstract-schema-name>Location</abstract-schema-name> <cmp-field> <field-name>id</field-name> </cmp-field> <cmp-field> <field-name>name</field-name> </cmp-field> </entity> Die Bean-Klasse wird wie beschrieben als abstrakt deklariert, ebenso wie die notwendigen Zugriffsmethoden für die CMP-Attribute: public abstract class LocationEntityBean implements EntityBean { public abstract Long getId(); public abstract void setId(Long id); public abstract String getName(); public abstract void setName(String name); public Long ejbCreate(Long id, String name) throws CreateException { this.setId(id); this.setName(name); return null; } } Die Create-Methode nutzt die deklarierten Zugriffsmethoden, um die übergebenen Werte zu setzen und gibt laut Spezifikation den Wert null an den Container zurück. CreateMethoden werden vom Container unmittelbar vor dem Einfügen von Daten in die Datenbank ausgeführt, die korrespondierenden PostCreate-Methoden danach. Die Methoden ejbLoad() und ejbStore() müssen bei CMP-Entity Beans nicht implementiert werden. Beide Methoden werden nicht für den Datenbankzugriff genutzt, sondern als Callback-Methoden, um die Bean über Datenbankzugriffe zu informieren. So wird ejbLoad() nach dem Laden von Daten aufgerufen, ejbStore() unmittelbar vor dem Schreiben von Daten. Die Bean hat so die Möglichkeit, Werte von möglichen transienten Attributen zu bearbeiten. Container Managed Relations Mit der Einführung der lokalen Beans können seit EJB 2.0 auch Beziehungen zwischen CMP-Entity Beans realisiert werden, man spricht dann von Container Managed Relations (CMR). 40 3.5 Persistenzmechanismen Es können alle Arten von Beziehungen realisiert werden: 1:1-, 1:n- und n:m-Beziehungen, jeweils uni- oder bidirektional. Dabei hat eine Beziehung zwischen zwei Beans immer zwei Rollen, in der die beiden Seiten der Beziehung definiert werden. Ähnlich wie bei CMP-Feldern wird die eigentliche Implementierung und Verwaltung vom EJB-Container übernommen. Der Entwickler muß die Beziehung lediglich im Deployment-Deskriptor definieren und geeignete Zugriffsmethoden in der Bean-Klasse bereitstellen. Beziehungen werden unterhalb des Elements <relationships> im Deployment-Deskriptor definiert. Für jede Beziehung muß ein <ejb-relation>-Element angegeben werden. Darunter werden der Name der Beziehung sowie für jede der beiden Rollen ein <ejb-relationship-role>-Element angegeben. Für jede Rolle wird ein Name angegeben, eine Kardinalität (<multiplicity>) und eine Quelle (<relationship-rolesource>). Diese Quelle gibt den Namen der Entity Bean an, so wie er im zugehörigen <entity>-Element festgelegt wurde. Die Angabe eines CMR-Feldes mit Hilfe des <cmr-field>-Elements sowie das Vorhandensein der entsprechenden get-/setMethoden in der Bean-Klasse machen eine Beziehung verfügbar und bestimmen somit die Direktionalität. Bei unidirektionalen Beziehungen definiert nur eine der Rollen ein CMR-Feld. Gibt ein CMR-Feld mehr als ein Element zurück, wird zusätzlich das Element <cmr-field-type> angegeben. Es spezifiziert den genauen Typ und kann die Werte java.util.Collection oder java.util.Set haben. Auch hier gilt: Set vermeidet Duplikate. Für die n-Seite einer Beziehung kann innerhalb der Definition auch der Lebenszyklus für Elemente dieser Seite bestimmt werden. Das Element <cascade-delete /> gibt an, daß bei Löschung des Elternelements auch alle Kindelemente gelöscht werden sollen. Das folgende Beispiel definiert die Rolle der Location-Bean in einer n:1-Beziehung zu einer anderen Bean: <ejb-relation> <ejb-relation-name>state-location</ejb-relation-name> <ejb-relationship-role> <ejb-relationship-role-name> location-belongsto-state </ejb-relationship-role-name> <multiplicity>Many</multiplicity> <cascade-delete /> <relationship-role-source> <ejb-name>LocationEJB</ejb-name> </relationship-role-source> <cmr-field> <cmr-field-name>state</cmr-field-name> </cmr-field> </ejb-relationship-role> 3 | Enterprise JavaBeans 41 <ejb-relationship-role> [...] </ejb-relationship-role> </ejb-relation> Die CMR-Felder in der Bean-Klasse werden analog zu CMP-Feldern deklariert. Sie haben als Rückgabetyp immer das lokale EJB-Objekt (Komponenten-Interface) der referenzierten Bean. CMR-Felder sind allerdings erst gültig, nachdem vom Container eine ejbPostCreate()-Methode aufgerufen wurde. public abstract class LocationEntityBean implements EntityBean { public abstract StateEntityLocal getState(); public abstract void setState(StateEntityLocal state); } Entity Beans, die in einer Beziehung zueinander stehen, müssen im selben DeploymentDeskriptor definiert und auch im selben Enterprise-Archiv ausgeliefert werden. Finder-Methoden und EJB-QL Wie bereits in Abschnitt 3.4.2 erklärt wurde, dienen Finder-Methoden dem Auffinden einer oder mehrerer Bean-Instanzen anhand bestimmter Auswahlkriterien. Auch bei Container Managed Persistence erfolgt die Deklaration aller Finder-Methoden im HomeInterface der Bean. Eine Implementierung in der Bean-Klasse ist jedoch nicht erforderlich. Stattdessen erfolgt eine deklarative Definition im Deployment-Deskriptor. Die Methode findByPrimaryKey() muß nicht definiert werden, dies übernimmt der Container selbständig. Die Definition im Deployment-Deskriptor erfolgt mit Hilfe der in der EJB 2.0-Spezifikation standardisierten Anfragesprache EJB-QL. Diese Anfragesprache ist unabhängig vom eingesetzten Datenbanksystem, wodurch Finder-Methoden portabel werden. Erst der Container übernimmt die Übersetzung einer EJB-QL-Anfrage in den SQL-Dialekt der verwendeten Datenbank. EJB-QL ist eine Teilmenge von SQL92, unterstützt aber nicht alle Funktionen. So fehlen unter anderem Aggregatfunktionen wie avg, count und sum. Auch die Anwendung von ORDER BY oder eine Einschränkung der Ergebnismenge durch LIMIT ist nicht möglich. Die Anwendung des Operators LIKE ist wegen der fehlenden Parametrisierbarkeit eingeschränkt5 . Abgesehen davon kann jedoch mit EJB-QL über CMR-Beziehungen zwischen Entity Beans navigiert werden. Um fehlende Funktionalität zu ersetzen, bieten die meisten Applikationsserver eigene Erweiterungen der Anfragesprache an. Damit geht natürlich die Portabilität der Komponenten verloren. Das folgende Home-Interface deklariert drei Finder-Methoden für die CMP Entity Bean LocationEntity: 5 ab EJB 2.1 Parametrisierbarkeit möglich 42 3.5 Persistenzmechanismen +, -, *, / arithmetische Operatoren <, <=, =, >=, >, <> Vergleichsoperatoren AND, OR, NOT logische Operatoren [NOT] BETWEEN prüft, ob ein CMP-Attribut in einem Intervall liegt [NOT] IN prüft, ob ein CMP-Attribut in einer Aufzählung existiert IS [NOT] EMPTY prüft CMR-Collection-Attribute auf Inhalt IS [NOT] NULL [NOT] LIKE testet ein CMP- oder CMR-Attribut auf NULL Mustervergleich bei Strings [NOT] MEMBER OF prüft, ob ein Objekt in einer CMR-Collection enthalten ist CONCAT hängt zwei Strings aneinander LENGTH ermittelt die Länge eines Strings LOCATE liefert die Position eines Substrings SUBSTRING liefert einen Teilstring ABS SQRT DISTINCT berechnet den Absolutwert einer Zahl berechnet die Quadratwurzel einer Zahl Ausschluß von Duplikaten . Dereferenzierung von Membern Tabelle 3.4: Operatoren und Ausdrücke in EJB-QL public interface LocationEntityLocalHome extends EJBLocalHome { [...] public LocationEntityLocal findByPrimaryKey(Long id) throws FinderException; public Collection findAll() throws FinderException; public Collection findByPattern(String pattern) throws FinderException; } Für die Finder-Methode findByPrimaryKey() ist keine Definition notwendig. Für die beiden anderen wird im Deployment-Deskriptor jeweils ein <query>-Element angegeben, welches alle nötigen Daten enthält. Das folgende Beispiel zeigt die Definition der oben deklarierten Finder-Methode findAll(). Sie erwartet keine Parameter und liefert alle Instanzen der Entity Bean zurück. <entity> <abstract-schema-name>Location</abstract-schema-name> 3 | Enterprise JavaBeans 43 [...] <query> <description>find-all</description> <query-method> <method-name>findAll</method-name> <method-params /> </query-method> <ejb-ql> <![CDATA[SELECT OBJECT(l) FROM Location l]]> </ejb-ql> </query> </entity> Das <query>-Element besteht aus den Unterelementen <query-method>, <resulttype-mapping> und <ejb-ql>. Das Element <query-method> enthält den Namen der Finder-Methode im Element <method-name> sowie eine Liste von möglichen Übergabeparametern. Diese werden durch ihre Datentypen angegeben und können primitiv oder serialisierbar sein. Das optionale Element <result-type-mapping> gibt bei Entity Beans mit lokalen und Remote-Interfaces an, welchen Typ das gelieferte EJB-Objekt haben soll. Gültige Parameter sind Remote und Local. Im Element <ejb-ql> wird die eigentliche Anfrage definiert. Diese sollte immer in eine CDATA-Sektion eingehüllt werden, um spätere XML-Parsing-Probleme zu vermeiden. Die Syntax der EJB-QL-Anfrage entspricht weitgehend der SQL-Syntax. Die Anfrage operiert allerdings auf dem abstrakten Schema-Namen der Entity Bean und nicht auf einer Tabelle. Eventuelle Parameter werden mit dem Platzhalter ?n gekennzeichnet, die Numerierung beginnt bei 1. Die zweite Finder-Methode im Beispiel erwartet einen Parameter vom Typ String, dieser muß im Deployment-Deskriptor mit vollqualifiziertem Namen angegeben werden: <method-params> <method-param>java.lang.String</method-param> </method-params> Die zugehörige EJB-QL-Anfrage lautet dann: <![CDATA[ SELECT OBJECT(l) FROM Location l WHERE l.name LIKE ?1 ]]> Innerhalb der EJB-QL-Anfrage können alle CMP- und CMR-Felder der Entity Bean benutzt werden, außerdem sind Pfadausdrücke in der WHERE-Klausel möglich. Navigiert wird über die Punktnotation, womit eine Ähnlichkeit zur Java-Syntax bei der Navigation über Member gegeben ist. 44 3.5 Persistenzmechanismen Select-Methoden Bei Select-Methoden handelt es sich um ein neues Feature von EJB 2.0. Diese Methoden dienen im allgemeinen dazu, die Anzahl von Hilfsmethoden in einer Entity Bean zu verringern. Select-Methoden werden weder im Home- noch im Komponenten-Interface bekannt gemacht und stehen nur in der Bean-Klasse zur Verfügung, in der sie definiert wurden. Somit können sie nicht direkt von Clients benutzt werden, aber zum Beispiel über eine Home- oder Geschäftsmethode dennoch indirekt verfügbar sein. Select-Methoden ähneln den Finder-Methoden, sind aber weitaus flexibler. Während Finder-Methoden dem Auffinden von Entity-Bean-Instanzen dienen und somit an das Home-Interface gebunden sind, können Select-Methoden die Typen aller im Deployment-Deskriptor definierten CMP- und CMR-Felder sowie Collections dieser Felder als Ergebnis liefern. Bei der Deklaration von Select-Methoden müssen folgende Vorgaben beachtet werden: Sie werden in der Bean-Klasse als public abstract deklariert und tragen im Namen das Suffix ejbSelect. Eine Select-Methode muß eine FinderException werfen können, und der Rückgabetyp muß dem Datentyp eines der CMP- oder CMR-Felder bzw. Collections dieser Felder entsprechen. Da eine Select-Methode nicht mit der Identität der Bean arbeitet, aus der sie aufgerufen wird, müssen alle diesbezüglichen Daten als Parameter übergeben werden. Insbesondere gilt damit auch, daß kein Zugriff auf die CMPbzw. CMR-Felder einer Instanz möglich ist. Das folgende Beispiel definiert eine SelectMethode, die die Postleitzahl eines Ortes zurückgeben soll, wobei die ID des Ortes als Parameter übergeben wird: public abstract class LocationEntityBean implements EntityBean { public abstract Integer ejbSelectZip(Long id) throws FinderException; } Die eigentliche Logik einer Select-Methode wird, wie auch bei Finder-Methoden, im Deployment-Deskriptor innerhalb eines <query>-Elements definiert. Auch hier gehören neben einer optionalen Beschreibung die Angabe des Methodennamens, der MethodenParameter sowie eines EJB-QL-Statements zur Definition der Methode. Das EJB-QLStatement arbeitet jedoch nicht auf der Bean, sondern auf einem oder mehreren CMPFeldern der Bean. Zur eben deklarierten Methode gehört der folgende Abschnitt im Deployment-Deskriptor: <query> <description>get-zip-from-location</description> <query-method> <method-name>ejbSelectZip</method-name> <method-params> <method-param>java.lang.Long</method-param> </method-params> </query-method> 3 | Enterprise JavaBeans 45 <ejb-ql> <![CDATA[SELECT l.zip FROM Location l WHERE l.id=?1]]> </ejb-ql> </query> Select-Methoden werden häufig aus zwei Gründen eingesetzt: Neben der Verringerung des Codes durch Wiederverwendung ist kein Zugriff auf die CMP- bzw. CMR-Felder über eine Instanz notwendig, sondern eine direkte Rückgabe über eine Select-Methode möglich. Dadurch kann die Performance durchaus erhöht werden. Home-Methoden Neu eingeführt in der EJB 2.0-Spezifikation wurden die sogenannten Home-Methoden. Diese Geschäftsmethoden werden im Home-Interface deklariert und können auf dem Typ arbeiten, für den das Home-Interface die Factory darstellt. Da das Home-Objekt keine Identität besitzt, sind auch die Home-Methoden nicht an eine bestimmte Identität gebunden. Sie können daher auch nicht auf CMP- bzw. CMR-Felder zugreifen, aber durch Aufruf von Select-Methoden trotzdem an die persistenten Daten gelangen. Das folgende Beispiel deklariert eine Home-Methode, mit der die Postleitzahl eines Ortes ermittelt werden soll (um das Exception-Handling gekürzt). Diese wird zunächst mit beliebigem Namen im Home-Interface deklariert. In der Bean-Klasse trägt die Methode dann das Präfix ejbHome... und ruft die im vorherigen Abschnitt deklarierte Select-Methode ejbSelectZip() auf. Damit wird auch ein weiterer Grund für den Einsatz von Home-Methoden deutlich: Sie machen Clients eventuell vorhandene SelectMethoden verfügbar. public interface LocationEntityLocalHome extends EJBLocalHome { public Integer getZipFromLocation(Long id); } public abstract class LocationEntityBean implements EntityBean { public Integer ejbHomeGetZipFromLocation(Long id) { return ejbSelectZip(id); } } 3.6 Transaktionen Jede Unternehmensanwendung mit Enterprise JavaBeans ist ein transaktionales System und unterstützt sowohl lokale als auch verteilte Transaktionen. Die Details der Transaktionskontrolle werden dem Transaktionsdienst des EJB-Servers überlassen. Der Transakti- 46 3.6 Transaktionen onsdienst muß laut EJB-Spezifikation den Java Transaction Service (JTS) implementieren, womit eine Kommunikation nicht nur zu javabasierten Systemen, sondern auch zu anderen Systemen möglich ist, die den CORBA-Standard unterstützen (nach [BG02]). JTS unterstützt außerdem verteilte Transaktionen. Ein EJB-Container benutzt den Transaktionsdienst des EJB-Servers und muß laut Spezifikation eine JTA6 -basierte Schnittstelle zum Transaktionsdienst zur Verfügung stellen. Die Steuerung von Transaktionen kann bei Enterprise JavaBeans durch Clients, Beans oder den EJB-Container erfolgen. Man spricht dann von client-gesteuerten, bean-gesteuerten und container-gesteuerten Transaktionen [BG02]. Die Angabe der gewünschten Transaktionskontrolle erfolgt im <transaction-type>-Element des Deployment-Deskriptors, gültige Werte sind Container und Bean. <session> <transaction-type>Container</transaction-type> </session> 3.6.1 Container-gesteuerte Transaktionen Bei container-gesteuerten Transaktionen (container-managed transactions, CMT) übernimmt der EJB-Container das Transaktionsmanagement (container-managed transaction demarcation) [BG02]. Weder die Bean-Klasse noch der Client enthalten Logik zur Transaktionssteuerung. Sie wird dem Container überlassen, der bei jedem Methodenaufruf für die entsprechende Umgebung sorgt. Fängt der Container einen Methodenaufruf für eine Bean-Instanz ab, so entscheidet er anhand von Transaktionsattributen über den benötigten Transaktionskontext. Benötigt eine Methode zum Beispiel eine Transaktion, so wird eine solche bei Nichtvorhandensein vom Container gestartet und anschließend die gewünschte Methode aufgerufen. Nach dem Methodenaufruf kann der Container die Transaktion beenden und ein Two-PhaseCommit oder ein Rollback ausführen. Angaben über die Art der Transaktion für eine Methode werden im Deployment-Deskriptor unterhalb des Elements <container-transaction> gemacht. Es werden jeweils der Name einer Bean und einer Methode zusammen mit dem gewünschten Transaktionsattribut definiert. Soll die Angabe für alle Methoden einer Bean gelten, so kann als Methodenname der Platzhalter * benutzt werden. <assembly-descriptor> <container-transaction> <method> <ejb-name>LocationEJB</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> 6 JTA: Java Transaction API, siehe Anhang A.3 3 | Enterprise JavaBeans 47 </assembly-descriptor> Mit Hilfe eines Transaktionsattributes wird der Geltungsbereich einer Transaktion festgelegt. Folgende sechs Attribute stehen zur Verfügung: • NotSupported Eine Methode soll nicht innerhalb eines Transaktionskontextes laufen. Eine eventuell aktive Transaktion wird für die Dauer des Methodenaufrufs unterbrochen. Dieser Modus bietet sich unter anderem an, wenn innerhalb einer Methode auf nicht transaktionsfähige Ressourcen zugegriffen wird [BG02]. • Supports Eine Methode kann sowohl innerhalb eines Transaktionskontextes laufen als auch außerhalb. Daher sollte darauf geachtet werden, daß die Methode in jedem Fall konsistente Daten liefert. • Required Eine Methode muß innerhalb eines Transaktionskontextes laufen. Sollte bereits eine Transaktion offen sein, so wird diese benutzt, andernfalls wird vom Container eine neue Transaktion gestartet und am Ende des Methodenaufrufs beendet. • RequiresNew Unabhängig davon, ob bereits eine Transaktion läuft, muß der Container in diesem Modus in jedem Fall eine neue Transaktion für eine Methode erzeugen. Eine bereits laufende Transaktion wird für die Dauer des Methodenaufrufs suspendiert. • Mandatory Eine Methode muß in einer bereits laufenden Transaktion aufgerufen werden. Ist dies nicht der Fall, so wirft der Container eine Ausnahme. • Never Eine Methode darf nie innerhalb einer Transaktion aufgerufen werden; sollte dennoch eine Transaktion laufen, so wirft der Container eine Ausnahme. Dieses Attribut kann angewendet werden, wenn eine Applikation nicht-transaktional sein soll [BG02]. 3.6.2 Bean-gesteuerte Transaktionen Eine Enterprise JavaBean hat die Möglichkeit, eine Transaktion selbständig zu steuern. Hierzu kann sie zunächst eine Datenbank-Verbindung erzeugen. Die Kontrolle über eine Transaktion kann die Bean dann selbst übernehmen (lokale Transaktion) oder dem Server überlassen (globale Transaktion). Es gilt jedoch: Nur Session Beans und Message Driven Beans dürfen bean-gesteuerte Transaktionen benutzen. Während die Bean eine lokale Transaktion direkt über die erzeugte Verbindung kontrolliert, registriert sie bei einer globalen Transaktion die Verbindung beim Transaktionsdienst des Servers. Der Zugang zu diesem Dienst erfolgt über das SessionContextObjekt, das der Session Bean vom EJB-Container zur Verfügung gestellt wird. Von dort 48 3.7 Fazit kann die Bean eine UserTransaction holen und ihre Datenbank-Verbindung registrieren. Die weitere Transaktionskontrolle erfolgt dann über diese UserTransaction. Vorteile ergeben sich durch die Nutzung von bean-gesteuerten Transaktionen allerdings nicht. Lediglich eine Verbesserung der Performance kann bei der Benutzung von lokalen Transaktionen erreicht werden. Tatsächlich ist es jedoch so, daß gerade die serverseitige Transaktionssteuerung einer der Vorteile des EJB-Modells ist und daher auf beangesteuerte Transaktionen möglichst verzichtet werden sollte. 3.6.3 Client-gesteuerte Transaktionen Auch der Client hat die Möglichkeit, die Transaktionssteuerung zu übernehmen. Ähnlich wie bei lokalen Transaktionen wird das UserTransaction-Objekt vom Container benötigt. Dieses holt sich der Client über einen JNDI-Lookup. Anschließend kann eine Transaktion mit begin() gestartet und mit commit() oder rollback() beendet werden. Es dürfen allerdings nur solche Bean-Methoden aufgerufen werden, die CMT unterstützen und transaktional ablaufen können [BG02]. 3.7 Fazit EJBs sind hochgradig portabel. Daher können Anwendungen, die mit EJBs entwickelt wurden, in jedem Applikationsserver installiert werden, der die EJB-Spezifikation unterstützt. Das Komponentenmodell ermöglicht zudem die Entwicklung eigener Komponenten und unter Zukauf von anderen Komponenten die Erstellung vollständiger J2EEApplikationen. Dadurch kann Entwicklungszeit eingespart werden. Da EJB 2.0 die meisten relationalen Datenbanksysteme unterstützt, ist es möglich, Komponenten zu schreiben, die mit den meisten Datenbanken zusammenarbeiten. So können Entity Beans mit CMP 2.0 in den unterschiedlichsten Umgebungen installiert werden. Da CMP 2.0 speziell auf Plattformunabhängigkeit hin optimiert wurde, lassen sich Komponenten, die persistenten Speicher benötigen, leichter verkaufen. Allerdings ist der Einarbeitungsaufwand in CMP 2.0 vergleichsweise hoch. Die Container Managed Persistence hängt sehr stark von Deployment-Deskriptoren ab, da die meisten Angaben deklarativ statt programmatisch erfolgen. Hinzu kommt, daß die Plattformunabhängigkeit nur auf Kosten von Einschränkungen in der Funktionalität gewährleistet werden kann. So führt EJB-QL zu einer Abstraktion vom darunterliegenden Datenbanksystem, allerdings fehlen der Anfragesprache wichtige Features wie Sortierung, Beschränkung der Ergebnismenge und Aggregatfunktionen. 4 | Java Data Objects Bei Java Data Objects (JDO) handelt es sich um eine sehr junge Spezifikation von Sun, die als Standard für Objektpersistenz in Java-Anwendungen etabliert werden soll. Mit Hilfe von JDO werden Java-Objekte auf Datenbanken gemappt (abgebildet). Dabei soll JDO die Lücke zwischen dem Java-Objektmodell und dem relationalen SQL-Modell durch einen objektorientierten Persistenzmechanismus schließen und die Details der Zugriffstechniken auf die eigentliche Datenquelle vor dem Entwickler verbergen. Im Jahr 1999 begann eine Gruppe von Sun-Entwicklern mit der Entwicklung eines Standards für das objektrelationale Mapping in Java. Noch im selben Jahr wurde der Java Specification Request für JDO freigegeben. Es beteiligten sich weitere Firmen, und der erste Public Review Draft wurde bereits 2000 veröffentlicht. Im März 2002 wurde die JDO-Version 1.0 dann zum Standard erklärt. Um mit JDO arbeiten zu können, müssen folgende Komponenten vorhanden sein: • JDO-API Diese enthält ähnlich wie JDBC eine Sammlung von Interfaces und Klassen, welche ein implementierungsunabhängiges Arbeiten mit JDO ermöglichen soll. • JDO-Implementierung Auf dem Markt gibt es mehrere JDO-Implementierungen, teilweise kommerziell, teilweise frei, aber auch einige Open Source Projekte. Drei von diesen sollen im folgenden vorgestellt werden: Libelis LiDO, Triactive JDO (TJDO) und XORM. Die Implementierung soll laut JDO-Spezifikation austauschbar sein, ohne dabei CodeÄnderungen vornehmen zu müssen. • JDO-Metadaten Hierbei handelt es sich um eine standardisierte XML-Datei, in der für jede Klasse die persistent zu machenden Felder beschrieben werden. • JDBC-Treiber Da JDO wie die meisten O/R-Mapping-Tools intern auf der JDBC basieren, benötigt man den entsprechenden JDBC-Treiber für seine Datenbank. • Datenquelle Diese kann mehr oder weniger beliebig sein, solange sie von der benutzten JDOImplementierung unterstützt wird. Im Normalfall handelt es sich dabei um relatio- 50 4.1 Ziele nale Datenbanksysteme (RDBS), aber auch die Benutzung von objektorientierten Datenbanksystemen (OODBS) oder gar einfachen Dateien ist möglich. 4.1 Ziele JDO verfolgt im wesentlichen zwei Ziele: Zum einen soll der Java-Entwickler eine transparente Sicht auf persistente Daten haben. Er kann sich auf die Arbeit mit den Javaklassen konzentrieren und die Persistenzdetails der verwendeten JDO-Implementierung überlassen. Das bedeutet für den Entwickler die ununterbrochene Arbeit mit einem objektorientierten Klassenmodell, ohne zusätzlich ein relationales Modell für die eigentliche Speicherung entwickeln zu müssen. Dies erledigt JDO und abstrahiert somit die eigentliche Datenquelle vom Klassenmodell, womit auch ein Austausch der Datenquelle jederzeit möglich ist. Zum zweiten läßt sich JDO über die J2EE-Connector-Technologie problemlos in Applikationsserver integrieren und bietet so eine Alternative zu Enterprise JavaBeans und Container Managed Persistence, da vor allem letzteres von vielen Entwicklern als zu komplex angesehen wird. JDO bietet sich hier an, um alle Fragen der Persistenz direkt aus Session Beans heraus zu klären und so Entity Beans zu umgehen. JDO ermöglicht es, skalierbare Anwendungen zu erstellen, ohne dabei umfangreiche Code-Änderungen machen zu müssen, wie es beispielsweise bei JDBC-Anwendungen erforderlich wäre. 4.2 Konzepte Die JDO-Spezifikation definiert drei wesentliche Basiskonzepte, die in den folgenden Abschnitten kurz vorgestellt werden sollen. Diese Konzepte sind die Metadaten, das Bytecode Enhancement und die JDO Query Language. 4.2.1 Metadaten Um das Mapping zwischen Java-Objekten und Datenbankfeldern herzustellen, benötigt JDO Metadaten im XML-Format. Diese werden üblicherweise in einer XML-Datei namens metadata.jdo abgelegt und beschreiben, welche persistenten Felder eine Klasse enthält und welcher Art die Beziehungen zu anderen Klassen sind. Die Datei kann manuell oder durch ein Tool erzeugt werden. Die Metadaten werden während des Entwicklungsprozesses benötigt, um das Bytecode Enhancement (siehe 4.2.2) durchzuführen und optional das Datenbankschema zu erzeugen. Das Format ist von der JDO-Spezifikation vorgegeben und in der einzubindenden Dokumenttyp-Definition festgeschrieben. Sieht man sich die in Anhang A.7 aufgeführte DTD an, so wird man feststellen, daß diese sehr schlank ist und neben dem Wurzelelement nur sieben weitere Elemente enthält. Dies reicht aus, um alle relevanten Informationen abzubilden. Im einfachsten Fall, also ohne Extensionen, können die Mapping-Informationen für eine Klasse wie folgt aussehen: 4 | Java Data Objects 51 <?xml version="1.0"?> <!DOCTYPE jdo SYSTEM "jdo.dtd"> <jdo> <package name="info.artanis.locations.model"> <class name="State"> <field name="name" /> <field name="locations"> <collection element-type="Location" /> </field> </class> </package> </jdo> Alle Klassen eines Packages können unter einem <package>-Element angeordnet werden. Für jede persistente Klasse wird ein <class>-Element mit deren Name angegeben, ebenso für jeden Member der Klasse ein entsprechendes <field>-Element. Die Angabe der Datentypen entfällt, diese ermittelt JDO automatisch. Handelt es sich bei einem Member um eine Collection, so muß innerhalb des <field>-Elements ein <collection>-Element angegeben werden. Mit diesem Element wird der Typ der in der Collection enthaltenen Objekte festgelegt. Zusätzliche anbieterspezifische Daten können über das <extension>-Element eingefügt werden. Es erhält als Attribute den Anbieter (vendor-name), einen Schlüssel (key) und einen Wert (value). Solche Extensionen können überall in den Metadaten auftauchen. Eine JDO-Implementierung wertet dabei grundsätzlich nur die Extensionen aus, die mit dem eigenen Vendor-Namen übereinstimmen. So kann eine Metadatei von verschiedenen Implementierungen genutzt werden. Solche Extensionen werden zum Beispiel dann genutzt, wenn bereits ein Datenbankschema vorhanden ist und das Mapping entsprechend angepaßt werden muß. Vererbungshierarchien werden ebenfalls von JDO unterstützt. Hierzu wird ein zusätzliches Attribut im <class>-Element angegeben: <class name="ExtendingClass" persistence-capable-superclass="BaseClass" /> 4.2.2 Bytecode Enhancement Das sogenannte Bytecode Enhancement dient der Veränderung von bereits kompilierten Klassen auf Bytecode-Ebene, um Anpassungen für die Benutzung mit JDO vorzunehmen. Dafür wird von den meisten JDO-Implementierungen ein Bytecode Enhancer mitgeliefert. Dieser modifiziert die persistenten Klassen derart, daß sie einerseits das von der JDO-Spezifikation verlangte Interface PersistenceCapable implementieren und andererseits implementierungsspezifische Erweiterungen enthalten. Als Eingaben dienen ihm dabei die kompilierten Klassen und die XML-Metadaten. Erst nach dem Enhancement kann man Instanzen dieser Klassen mit JDO persistent machen. 52 4.3 Der Entwicklungsprozeß mit JDO Ein wichtiges Ziel des Enhancers ist es dabei auch, die Kompatibilität zwischen unterschiedlichen Datenbanken und unterschiedlichen JDO-Implementierungen zu gewährleisten. Das bedeutet beispielsweise, daß eine von Anbieter A erweiterte Klasse auch mit der JDO-Implementierung von Anbieter B zusammenarbeiten muß und umgekehrt (siehe [JDOSp]). Das Bytecode Enhancement ist ein kontroverses Thema [Mo02], da viele Entwickler davor zurückschrecken, ihre Klassen auf Bytecode-Ebene von einem Tool verändern zu lassen und nicht mehr über deren Inhalt Kenntnis zu haben. Aber auch die Integration in IDEs1 gestaltet sich schwierig. So kompiliert beispielsweise Eclipse bereits während der Eingabe den Quellcode, und vor dem Ausführen einer JDO-Anwendung muß jedesmal der Enhancer auf die persistenten Klassen angewendet werden. Die JDO-Spezifikation schreibt zwar nicht zwingend ein Bytecode Enhancement vor, um die notwendigen Änderungen vorzunehmen, jedoch sollte die manuelle Implementierung des Interfaces PersistenceCapable aufgrund des Umfangs nur von versierten Entwicklern vorgenommen werden. 4.2.3 JDO Query Language Für Objekt-Anfragen bietet JDO eine eigene Anfragesprache an, die JDO Query Language (JDOQL). Dabei handelt es sich um eine SQL-ähnliche, aber javazentrierte Anfragesprache, die in der JDO-Spezifikation standardisiert ist. Javazentriert bedeutet in diesem Zusammenhang, daß Parameter in Form von Objekten an eine Anfrage übergeben werden. Eine Anfrage stellt dabei einen Methodenaufruf dar. Intern wird eine solche Anfrage von der JDO-Implementierung natürlich in SQL übersetzt und an die Datenbank geschickt. JDOQL gewährleistet eine Unabhängigkeit von der darunterliegenden Datenquelle und somit eine gewisse Portabilität des Codes. Allerdings sind dadurch nicht alle Anfragen möglich, die mit SQL direkt machbar wären. Daher bieten die meisten JDO-Implementierungen Möglichkeiten an, um SQL-Anfragen direkt an die Datenbank abzusetzen. Die von JDOQL unterstützten Operatoren sind in Tabelle 4.1 aufgeführt. Der Operator LIKE für Zeichenketten sowie Aggregatfunktionen werden von JDOQL nicht unterstützt. Bei den meisten JDO-Implementierungen können jedoch Statements, die den LIKE-Operator verwenden, direkt abgesetzt werden. Dies führt jedoch dazu, daß die Unabhängigkeit von der verwendeten JDO-Implementierung verloren geht und bei einem Wechsel der Implementierung Eingriffe in den Code an genau diesen Stellen notwendig sind. 4.3 Der Entwicklungsprozeß mit JDO Durch das anzuwendende Bytecode Enhancement unterscheidet sich der Entwicklungsprozeß einer JDO-Anwendung von dem einer normalen Java-Anwendung. Abbildung 4.1 zeigt den prinzipiellen Ablauf, der hier kurz erklärt werden soll. 1 Integrierte Entwicklungsumgebungen 4 | Java Data Objects 53 == gleich != ungleich > größer als < >= kleiner als größer als oder gleich <= kleiner als oder gleich &, && |, || ! und oder Negation + Addition bzw. String-Konkatenation * Multiplikation / ˜ Divison bitweises Komplement . Vorzeichen Dereferenzierung von Membern Tabelle 4.1: Operatoren in JDOQL (nach [Ro03]) Zu Beginn werden alle Klassen des Datenmodells erzeugt. Dabei müssen zwar keine JDO-spezifischen Vorgaben beachtet werden, jedoch müssen die Klassen, die später mit JDO persistent gemacht werden sollen, der JavaBeans-Spezifikation genügen. Das heißt, neben einem parameterlosen Standardkonstruktor muß für jeden Member ein get-/ set-Methodenpaar existieren (siehe Anhang A.5). Abgesehen davon kann das Datenmodell beliebige Vererbungsstrukturen und Beziehungen zu anderen Objekten enthalten. Danach werden für alle persistent zu machenden Klassen die Metadaten erzeugt. Diese dienen dann zusammen mit den kompilierten Klassen als Eingabe für den Bytecode Enhancer. Dieser sorgt letztendlich dafür, daß die Klassen nach ihrer Modifikation von einer JDO-Anwendung benutzt werden können, um Daten persistent zu machen. Bei jeder Codeänderung an einer Klasse muß diese zuerst neu kompiliert und erweitert werden. Entwickelt man eine Anwendung von Grund auf, so ist außerdem das Datenbankschema zu erzeugen. Die Schema-Erzeugung erfolgt entweder mit Hilfe eines Tools, das vom Anbieter mitgeliefert wird (z.B. bei LiDO), oder beim ersten Ausführen der eigentlichen Anwendung (z.B. TJDO). Wie genau das erzeugte Schema aussieht, hängt zum einen von den Angaben in den Metadaten ab, zum anderen von der benutzten Implementierung. Werden beispielsweise keine besonderen Angaben in den Metadaten gemacht, so erzeugen die meisten Implementierungen für 1:n-Beziehungen eine zusätzliche Beziehungstabelle. Bei n:m-Beziehungen erzeugt LiDO sogar zwei Beziehungstabellen, je eine für jede Richtung. Mit Hilfe von Extensionen kann man jedoch das Schema in gewissem Maße anpassen. So können zum Beispiel 1:n-Beziehungen auf Fremdschlüsselbeziehun- 54 4.4 Objektidentität ! "" "" ) * ! "" # +* $ %' $ ( $ $ % &$ Abbildung 4.1: Der Entwicklungsprozeß mit JDO (nach [Mo02]) gen abgebildet werden. Das heißt, in der Tabelle mit den Daten der n-Seite existiert eine zusätzliche Spalte mit dem Fremdschlüssel, der das zugehörige Elternobjekt referenziert. So ist auch die Kardinalität von 1:n in jedem Fall gewährleistet. Bei JDO wird diese Art der Referenzierung als inverse Relation bezeichnet. Der letzte Schritt besteht in der Implementierung der eigentlichen Anwendung, die nun die erweiterten Klassen benutzt, um Daten persistent zu machen. Das Bytecode Enhancement und die Schema-Erzeugung können mit Hilfe des BuildTools Ant automatisiert werden. 4.4 Objektidentität JDO unterstützt drei unterschiedliche Arten der Objektidentität zur Identifizierung von persistenten Instanzen: Datastore Identity, Application Identity und Non-durable2 Identity. Das Objekt, welches die Identität kapselt, wird als Objekt-ID bezeichnet, dessen Klasse als Objekt-ID-Klasse [Ro03]. Die für eine Klasse gewünschte Art der Identität wird in den Metadaten über ein zusätzliches Attribut festgelegt. Wird es nicht angegeben, so gilt Datastore Identity. 2 nicht-dauerhaft 4 | Java Data Objects 55 <class name="State" identity-type="datastore"> [...] </class> 4.4.1 Datastore Identity Bei dieser Art der Identität übernimmt die JDO-Implementierung die Erzeugung einer Objekt-ID zum Zeitpunkt der Speicherung eines Objekts. Die Objekt-ID einer Instanz bleibt dem Entwickler verborgen, kann aber über die API ermittelt werden. Diese Art der Identität sollte benutzt werden, wenn noch kein Datenbankschema vorhanden ist. Mit der Schemaerzeugung erhält dann jede Tabelle automatisch eine zusätzliche Spalte für die ID. Weiterhin werden je nach JDO-Implementierung weitere Tabellen angelegt, die der Erzeugung von eindeutigen IDs dienen. Eine Primärschlüsselspalte in einem vorhandenen Schema kann nicht als ID für eine Klasse mit Datastore Identity benutzt werden. 4.4.2 Application Identity Bei Application Identity ist allein die Anwendung dafür verantwortlich, jedem Objekt eine ID zuzuordnen. Das heißt, die ID eines Objekts steht schon vor seiner Speicherung in der Datenbank fest. Hierfür ist ein zusätzlicher Member in den persistenten Klassen notwendig, der die Objekt-ID (den Primärschlüssel aus der Datenbank) hält. In den Metadaten erhält dieser Member ein zusätzliches Attribut. Außerdem muß der Typ der Objekt-ID-Klasse3 angegeben werden, bei dem einige Vorgaben zu beachten sind. <class name="State" identity-type="application" objectid-class="StatePK"> <field name="id" primary-key="true" /> [...] </class> Alle Member der Objekt-ID-Klasse müssen public und zudem primitive oder serialisierbare Datentypen sein. Die Klasse selbst muß das Interface Serializable implementieren sowie die Methoden equals(), hashCode() und toString() überschreiben. Außerdem muß neben dem parameterlosen Standardkonstruktor ein String-Konstruktor4 existieren. Die Methode toString() muß die ID in einer Form zurückgeben, in der sie als Übergabeparameter für den String-Konstruktor dienen kann. Für jeden in den Metadaten als Primärschlüssel ausgewiesenen Member muß ein Member gleichen Namens und Typs in der Objekt-ID-Klasse existieren. Diese Vorgaben sind notwendig, damit die Primärschlüsselklassen mit denen von EJBs austauschbar sind und somit die Integration von JDO in J2EE-Applikationsserver vereinfacht wird [Ro03]. 3 4 auch: Primärschlüsselklasse ein Konstruktor mit einem Stringparameter 56 4.5 Die JDO-API Anhang A.8 zeigt eine mögliche Primärschlüsselklasse für ganzzahlige Schlüssel vom Typ long. Application Identity sollte angewendet werden, wenn mit einem bereits vorhandenen Datenbankschema gearbeitet wird. 4.4.3 Non-durable Identity Dieser Identitätstyp wird angewendet, wenn eine eindeutige Identifizierung und somit Unterscheidung von Instanzen nicht notwendig ist. Das Attribut identity-type erhält dann den Wert non-durable. 4.5 Die JDO-API Die meisten JDO-Operationen laufen über den sogenannten Persistenzmanager, den die API in der Klasse PersistenceManager bereitstellt. Der Persistenzmanager ist mit genau einer Datenquelle assoziiert und wird mit Hilfe einer Factory-Klasse erzeugt, die von jeder JDO-Implementierung mitgeliefert wird. Die genaue Angabe erfolgt über Properties, dabei müssen neben der Factory-Klasse und dem Datenbanktreiber auch die Zugangsdaten zur Datenbank angegeben werden. Die Namen dieser Properties lauten wie folgt: javax.jdo.PersistenceManagerFactoryClass javax.jdo.option.ConnectionDriverName javax.jdo.option.ConnectionURL javax.jdo.option.ConnectionUserName javax.jdo.option.ConnectionPassword Zusätzlich zu den genannten kann jede JDO-Implementierung die Angabe weiterer Properties verlangen. Mit folgenden Zeilen kann dann der Persistenzmanager geholt und somit die Verbindung zur Datenbank hergestellt werden: Properties props = new Properties(); // Properties setzen... // die Factory-Klasse erzeugen PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory(props); // PersistenceManager holen PersistenceManager pm = pmf.getPersistenceManager(); // Aktionen ausführen pm.close(); 4 | Java Data Objects 57 JDO ist transaktionsbasiert, das heißt, alle Aktionen, die zu einer Änderung in der Datenbank führen sollen oder können, müssen innerhalb eines Transaktionskontextes ausgeführt werden. Als Muster eignet sich der folgende Codeabschnitt (nach [Ro03]): Transaction t = pm.currentTransaction(); t.begin(); try { // hier objekte abfragen, ändern, speichern, löschen } catch ( Exception e ) { // rollback ausführen, falls t noch aktiv ist if ( t.isActive() ) t.rollback(); } finally { // commit ausführen, falls t noch aktiv ist try { if ( t.isActive() ) t.commit(); } catch ( JDOUserException je ) { } } Eine Transaktion wird durch die Methode currentTransaction() vom Persistenzmanager geholt. Zudem kann angegeben werden, ob es sich um eine optimistische oder pessimistische Transaktion handeln soll. Die genaue Angabe erfolgt mit Hilfe der Methode setOptimistic(boolean optimistic) aus der Klasse Transaction. Pessimistische Transaktionen sind laut JDO-Spezifikation vorgeschrieben, müssen also von jeder JDO-Implementierung realisiert werden. Eine pessimistische Transaktion sperrt die von ihr verwendeten Daten der Datenquelle für die Dauer der gesamten Transaktion. Daher sollten nur kurzlebige Transaktionen als pessimistisch deklariert werden. Optimistische Transaktionen sind laut Spezifikation ein optionales Feature und sperren die Datenquelle während der Transaktion nur dann, wenn es für die Konsistenz der Daten notwendig ist. 4.5.1 Speichern und Löschen von Objekten Das Speichern und Löschen von Objekten geschieht, indem man dem Persistenzmanager das oder die gewünschten Objekte übergibt. Der Persistenzmanager stellt die Methode makePersistent() bereit, um ein Objekt persistent zu machen, die Methode makePersistentAll(), um eine Collection oder ein Array von Objekten persistent zu machen. JDO arbeitet mit Persistence by Reachability. Das bedeutet, alle Objekte, die vom Wurzelobjekt aus über Beziehungen erreichbar sind, werden ebenfalls vom transienten 58 4.5 Die JDO-API in den persistenten Zustand überführt. Hat man eine Menge von zu speichernden Objekten, sollte man makePersistentAll() vorziehen, da diese Methode performanter arbeitet, als beispielsweise ein Aufruf von makePersistent() innerhalb einer Schleife. Ähnlich verhält es sich mit dem Löschen eines Objekts. Auch hier stellt die API Methoden für das Löschen eines Objektes (deletePersistent()) oder mehrerer Objekte (deletePersistentAll()) zur Verfügung. Zu beachten ist jedoch, daß das zu löschende Objekt zunächst innerhalb der laufenden Transaktion geladen werden muß, bevor es gelöscht werden kann (siehe 4.5.2). Die meisten JDO-Implementierungen führen eine kaskadierende Löschaktion aus, die nicht nur das Objekt selbst, sondern alle damit in Verbindung stehenden Kindobjekte betrifft (Cascading Delete). pm.makePersistent(anObject); // ein zuvor geladenes Objekt löschen pm.deletePersistent(anotherObject); 4.5.2 Objekte laden und manipulieren Für das Laden von Objekten aus der Datenbank bietet JDO drei verschiedene Möglichkeiten an. Alle drei Wege haben gemeinsam, daß sie nicht wie z.B. JDBC eine tabellenförmige Ergebnismenge zurückliefern, sondern Objekte oder Collections von Objekten. Das heißt, das Umwandeln der Ergebnismenge in Objekte entfällt vollständig, und die Arbeit kann sofort an den gelieferten Objekten erfolgen. Ein Objekt direkt holen Mit dem Befehl PersistenceManager#getObjectById(Object oid, boolean validate) kann man ein Objekt direkt holen, wenn seine ID bekannt ist. So könnte die ID zum Beispiel schon zu einem früheren Zeitpunkt mit getObjectId(Object o) geholt worden sein und nun als Parameter eingesetzt werden. Der Parameter validate gibt an, ob die Existenz einer Instanz mit der übergebenen ID überprüft werden soll. Object o = pm.getObjectById(oid, true); Extents Ein sogenannter Extent kann genutzt werden, um alle gespeicherten Instanzen einer Klasse zu erhalten. Der Extent dient dabei als Basis für alle Objektanfragen und stellt eine Art Kollektion aller Objekte einer Klasse dar. Geholt wird ein Extent mit Hilfe der Methode PersistenceManager#getExtent(). Als Parameter wird die Klasse der gewünschten Objekte übergeben und festgelegt, ob Subklassen der Instanzen ebenfalls geholt werden sollen. Anschließend kann mit einem Iterator die Ergebnismenge durchlaufen werden. Danach sollte der Extent mit der Methode closeAll() wieder geschlossen werden. Mit folgenden Anweisungen können beispielsweise alle in der Datenbank enthaltenen Instanzen der Klasse State ermittelt werden: 4 | Java Data Objects 59 Extent ext = pm.getExtent(State.class, true); Iterator it = ext.iterator(); // [...] ext.closeAll(); Anfragen mit JDOQL Um die Ergebnismenge einzuschränken, bietet JDO die bereits unter 4.2.3 vorgestellte JDO Query Language (JDOQL) an. JDOQL setzt sich im wesentlichen aus zwei Teilen zusammen: einer API, um die Anfragen zu verwalten sowie der Anfragesprache selbst. Die API von JDOQL verbirgt sich hinter dem Query-Interface. Instanzen dieses Interfaces können vom Persistenzmanager geholt werden, als Ausgangspunkt können Klassen, Collections oder Extents dienen. Anschließend können Filterregeln festgelegt werden, nach denen bestimmte Objekte ausgewählt werden sollen. Das folgende Beispiel soll alle Instanzen der Klasse Location ausgeben, deren Postleitzahl größer als 99000 ist. Extent ext = pm.getExtent(Location.class, false); String filter = "zip > aZip"; String parameter = "int aZip"; Query q = pm.newQuery(ext, filter); q.declareParameters(parameter); q.setOrdering("name ascending"); Collection c = (Collection)q.execute(new Integer(99000)); // [...] ext.closeAll(); Es wird zunächst der Extent mit allen Instanzen der Klasse Location geholt, dann ein Filter vereinbart, der festlegt, daß der Member zip gleich dem übergebenen Parameter aZip sein soll. Letzterer wird als int deklariert. Mit der Methode newQuery() kann nun ein Query-Objekt vom Persistenzmanager geholt werden. Als Parameter werden der Extent sowie der Filter übergeben. Dem Query muß dann der Parameter bekanntgegeben werden. Zusätzlich wird noch eine aufsteigende Sortierung nach Name festgelegt. Zuletzt wird mit der Methode execute() die Anfrage ausgeführt, als Parameter erhält sie einen Integer mit der gewünschten Postleitzahl. Möchte man die Auswahl durch mehr als einen Parameter einschränken, so ändern sich Filter und Parameter wie folgt: String filter = "zip > aZip & latitude > aLat"; String parameter = "int aZip, double aLat"; Die Methode execute() bekäme dann einen Integer- sowie einen Double-Parameter übergeben. 60 4.6 JDO-Implementierungen Sieht man sich die Art und Weise an, wie die Einschränkung der Ergebnismenge erfolgt, so wird klar, daß JDO das Prinzip der Kapselung verletzt, da im Filter direkt die Namen der (privaten) Member angegeben werden. Grund hierfür ist, daß zur Zeit keine Methodenaufrufe innerhalb einer JDOQL-Anfrage möglich sind. Das zweite Beispiel soll zeigen, wie eine Anfrage über Subklassen und Collections funktioniert. Es sollen alle Bundesländer ermittelt werden, in denen sich Orte mit mehr als 500.000 Einwohner befinden. Extent ext = pm.getExtent(State.class, false); String locVar = "info.artanis.locations.model.Location loc"; String filter = "locations.contains(loc) & loc.size.minimum > value"; String parameter = "int value"; Query q = pm.newQuery(ext, filter); q.declareParameters(parameter); q.declareVariables(locVar); Collection c = (Collection)q.execute(new Integer(500000)); In diesem Beispiel wurde das Konzept der Query-Variablen benutzt, um alle Bundesländer nach bestimmten Orten zu durchsuchen. Der Filter besteht hier aus zwei Bedingungen: Der Member locations muß den Ort enthalten, der die übergebene Bedingung (Einwohnerzahl) erfüllt. Als Variable wird eine Instanz der Klasse Location festgelegt und mit declareVariable() beim Query-Objekt bekanntgemacht. Um ein Objekt zu manipulieren, sind keine besonderen Vorkehrungen notwendig. Das geladene Objekt kann über die von ihm zur Verfügung gestellten Methoden beliebig verändert werden. Sobald das Commit der laufenden Transaktion erfolgt, werden alle Änderungen automatisch in die Datenbank geschrieben. 4.6 JDO-Implementierungen Da es sich bei JDO um eine Spezifikation und bei der API um eine Sammlung von Interfaces handelt, muß - ähnlich wie bei JDBC - eine Implementierung erworben werden, um mit JDO in einer Applikation arbeiten zu können. Auf dem Markt gibt es neben kommerziellen JDO-Implementierungen (z.B. IntelliBO von Signsoft, Jrelay von Object Industries sowie LiDO von Libelis) auch einige Open Source Projekte wie zum Beispiel TJDO und XORM (beide Sourceforge). Auch einige Hersteller von objektorientierten Datenbanken bieten für ihre Systeme einen Zugriff über die JDO-API an, hier sind enJin und FastObjects von Versant/Poet zu nennen. Da es sich um einen Standard handelt, der in Zukunft an größerer Wichtigkeit gewinnen wird, statten auch einige andere Anbieter ihre Tools mit dieser standardisierten API aus oder planen dies für eine der nächsten Versionen. Hierzu zählt unter anderem das Jakarta-Projekt OJB (siehe Kapitel 5.1). 4 | Java Data Objects 61 Die JBoss Group integriert ab der Version 4 ihres Applikationsservers JBoss eine JDOImplementierung namens JBossDO als Alternative zu den Entity Beans. Das Besondere an JBossDO ist, daß das Enhancement der persistenten Klassen erst ausgeführt wird, wenn das Deployment der Applikation im Server erfolgt. Drei der genannten JDO-Implementierungen sollen in den folgenden Abschnitten näher vorgestellt werden: LiDO, TJDO und XORM. Dabei soll neben der kurzen Beschreibung der Features besonders auf Unterschiede aufmerksam gemacht werden, die trotz des vorhandenen Standards beim Wechsel der Implementierung immer ein Problem sein können. 4.6.1 Libelis LiDO Die Firma Libelis bietet ihre kommerzielle JDO-Implementierung unter dem Namen LiDO an. Zur Zeit steht LiDO in vier Versionen zur Verfügung: als Community Edition, Standard Edition, Professional Edition und Enterprise Edition. Die Community Edition ist für den nicht-kommerziellen Einsatz frei verfügbar, hat allerdings auch nicht den vollen Funktionsumfang der anderen Editionen. Dies betrifft vor allem das Fehlen von zusätzlichen Tools, wie dem Project Manager als graphische Oberfläche für die Erzeugung der Metadaten. LiDO unterstützt als Datenquellen relationale und objektorientierte Datenbanksysteme, aber auch einfache Dateien. LiDO zeichnet sich daneben vor allem durch performante Caching-Mechanismen, Connection Pooling und leicht konfigurierbare Metadaten aus. So kann LiDO problemlos mit bereits vorhandenen DatenbankSchemata zusammenarbeiten, solche allerdings auch selbst aus vorhandenen Metadaten und Javaklassen erzeugen. Unterstützt werden auch alle Arten von Objektidentitäten sowie Beziehungen zwischen Objekten. LiDO erwartet die JDO-Metadaten in Form einer einzigen Datei, in der alle persistenten Klassen beschrieben sind. LiDO-spezifische Extensionen werden darin mit dem VendorNamen libelis gekennzeichnet. Wird Datastore Identity angewendet, so kann man die Erzeugung des Datenbankschemas beeinflussen. LiDO legt für jede Klasse eine Tabelle mit einer Primärschlüsselspalte namens LIDOID an. Deren Name kann zwar nicht geändert werden, dafür können jedoch die Namen aller anderen Felder individuell über die Extensionen angepaßt werden (Schlüsselwort sql-name). 1:n-Beziehungen können entweder über eine Fremdschlüsselbeziehung oder eine Relationentabelle realisiert werden. Für n:m-Beziehungen legt LiDO immer zwei Relationentabellen an, da die Referenzen immer eindeutig sein müssen und keine Collection sein dürfen. Eine Fremdschlüsselbeziehung wird mit dem Schlüsselwort sql-reverse realisiert. Weiterhin anzugeben ist der Member des KindElements, der die Referenz auf das Elternobjekt hält. <class name="State"> <field name="shortName"> <extension vendor-name="libelis" key="sql-name" 62 4.6 JDO-Implementierungen value="shortname" /> </field> <field name="locations"> <collection element-type="Location"> <extension vendor-name="libelis" key="sql-reverse" value="javaField:state" /> </collection> </field> [...] </class> Für das Bytecode Enhancement liefert LiDO einen eigenen Class Enhancer mit, der entweder über die Konsole oder über einen Ant-Task aufgerufen werden kann. Er erwartet als Argumente den Pfad zu den kompilierten Klassen und die zu verwendende Datei mit den Metadaten: <enhance targetPath="bin" metadata="metadata.jdo" /> Auch für das Erzeugen des Datenbank-Schemas liefert LiDO ein Tool mit, das über die Konsole oder einen Ant-Task ausgeführt werden kann. Als Argumente werden der Pfad zu den erweiterten Klassen, die Metadaten sowie eine Properties-Datei mit den Datenbank-Zugangsdaten (siehe Abschnitt 4.5) benötigt. Der zugehörige Ant-Task sieht wie folgt aus: <define-schema targetPath="bin" properties="lido.properties" metadata="metadata.jdo" sql="schema.sql" /> Über das Attribut sql kann die Ausgabe in eine Datei umgeleitet werden. Wird es weggelassen, so schreibt LiDO das Schema direkt in die Datenbank. LiDO erzeugt mit dem Task für jede Klasse eine Tabelle sowie für jede Nicht-Fremdschlüssel-Beziehung eine Beziehungstabelle; weiterhin werden zwei Tabellen für die Erzeugung von Primärschlüsseln angelegt. Bei Anfragen an die Datenbank besteht die Möglichkeit der Performance-Anpassung durch die sogenannten LiDOHints. Das betrifft Einstellungen wie die Anzahl der abzufragenden Elemente, die zu ladenden Attribute oder Cursordefinitionen. Bei den zu ladenden Attributen handelt es sich um die sogenannte Fetch Group. Standardmäßig wird bei Ergebnismengen nur die ID geladen, erst beim eigentlichen Zugriff auf ein Objekt werden dessen Daten vollständig geladen. Sollen jedoch sofort alle Daten der Objekte geladen werden, so kann dies wie folgt erreicht werden: 4 | Java Data Objects 63 Query q = pm.newQuery(aClass); q.declareParameters("String LIDOHINTS"); Collection res = (Collection)q.execute("load=dfg"); Der der Methode execute() übergebene String ist die Option, die Default Fetch Group zu laden. Diese umfaßt für ein Objekt dessen primitive Member, aber nicht die Referenzen auf andere Objekte. Auch das Ausführen von SQL-Anweisungen ist mit LiDO problemlos möglich. Der folgende Query wendet das in der JDO-Spezifikation fehlende LIKE an: Query q = pm.newQuery("sql", "where name like ’"+aPattern+"’ order by name"); q.setClass(aClass); Sowohl für die LiDOHints als auch für das Absetzen von SQL-Befehlen gilt natürlich, daß die Unabhängigkeit von der JDO-Implementierung verloren geht. Das heißt, bei einem Wechsel der JDO-Implementierung müssen derartige Code-Fragmente geändert oder sogar entfernt werden. 4.6.2 Triactive JDO Triactive JDO (TJDO) ist ein Open Source Projekt bei Sourceforge und stellt eine Implementierung der JDO-Spezifikation 1.0 bereit. Das Tool unterstützt die meisten JDOFeatures vollständig (u. a. die Anfragesprache JDOQL). Allerdings kann TJDO kein Mapping zu einem bereits vorhandenen Datenbankschema herstellen. Stattdessen werden alle notwendigen Tabellen automatisch erzeugt, dies muß jedoch über den PropertyEintrag com.triactive.jdo.autoCreateTables true explizit festgelegt werden. TJDO verlangt weiterhin für jede Klasse eine eigene Mappingdatei, es ist also nicht möglich, alle Mapping-Informationen in nur einer Datei zusammenzufassen. Zudem muß die Mappingdatei im gleichen Verzeichnis liegen, wie die erweiterte Klasse und in ihrem Namen dem Muster klassenname.jdo entsprechen. Die Extensionen von TJDO haben den Vendor-Name triactive. Eine notwendige Extension für String-Member ist die Angabe der Länge. <field name="name"> <extension vendor-name="triactive" key="length" value="max 128" /> </field> Bei Beziehungen bietet TJDO ebenso wie die meisten anderen JDO-Implementierungen die Möglichkeit, Fremdschlüsselbeziehungen für die Referenzierung zu nutzen (inverse 64 4.6 JDO-Implementierungen Relation), allerdings beherrscht TJDO nur bidirektionale Beziehungen. Bei TJDO dienen die Schlüssel collection-field und owner-field des <extension>-Elements dazu, eine inverse Relation zu realisieren. Mit dem Schlüssel owner-field wird der Name des Attributs im Kindobjekt angegeben, welches die Referenz auf das Elternobjekt enthält. Die Angabe erfolgt unterhalb des <collection>-Elements der Elternklasse. Über den Schlüssel collection-field wird der Name des Collection-Objektes im Elternobjekt angegeben, welches alle Kindobjekte enthält. Die Extension wird unterhalb des <field>-Elements plaziert, das zu dem Attribut gehört, welches das Elternobjekt referenziert. Beide Angaben sind erforderlich und müssen einander entsprechen, damit die Konsistenz (korrekte Referenzierung) gewahrt bleibt. <class name="State"> <field name="locations"> <collection element-type="Location"> <extension vendor-name="triactive" key="owner-field" value="state" /> </collection> </field> [...] </class> <class name="Location"> <field name="state"> <extension vendor-name="triactive" key="collection-field" value="locations" /> </field> [...] </class> Für das Bytecode Enhancement bietet TJDO keinen eigenen Enhancer an, sondern lediglich einen Wrapper für den Enhancer der Referenzimplementierung von Sun. Aufgerufen wird dieser mit folgendem Befehl: java -cp "tjdo.jar;jdori.jar;xercesImpl.jar;xmlParserAPIs.jar;." com.triactive.jdo.enhance.SunReferenceEnhancer info/artanis/locations/model/*.jdo Cascading Deletes werden von TJDO nicht korrekt ausgeführt. So werden zum Beispiel beim Löschen eines Eintrags nicht alle dazugehörigen Einträge aus anderen Tabellen entfernt, sondern lediglich deren Fremdschlüsselspalte auf NULL gesetzt. 4.6.3 XORM Auch XORM ist eine JDO-Implementierung, die bei Sourceforge gehostet wird, hält sich aber zumindest in einem Punkt nicht an die Spezifikation: Es findet kein Bytecode Enhancement statt. Vielmehr definiert der Entwickler statt konkreter Klassen nur Interfaces 4 | Java Data Objects 65 oder abstrakte Klassen. Aus denen erzeugt XORM erst zur Laufzeit konkrete Klassen, deren Daten dann persistent gemacht werden können. Neben den JDO-Metadaten benötigt XORM eine weitere XML-Datei, in der alle für die Anwendung benötigten Tabellen beschrieben sind. XORM kann zur Zeit nur mit vorhandenen Datenbank-Schemata arbeiten und bietet ein Tool an, um die XML-Schema-Datei zu erzeugen. Dieses kann zum Beispiel mit Ant aufgerufen werden: <java fork="true" classname="org.xorm.tools.generator.DBToXML" output="db.xml"> <arg value="xorm.properties" /> <classpath> <pathelement location="bin" /> <fileset dir="lib"> <include name="**/*.jar" /> </fileset> </classpath> </java> Die Datei xorm.properties enthält alle im Abschnitt 4.5 genannten Angaben. Zur Beachtung: Alle Klassen aus dem Package org.xorm.tools liegen der Version Beta 5 nur als Quellcodes bei. Weiterhin verlangt XORM, daß die erzeugte XML-Schema-Datei innerhalb eines Java-Archivs zu liegen hat. Der Pfad dorthin muß der Anwendung als Property zur Verfügung gestellt werden: org.xorm.datastore.database=/info/artanis/locations/model/db.xml Auch die JDO-Metadaten kann XORM automatisch erzeugen, hierfür steht das Tool GenerateJDO zur Verfügung. Die Datei mit den Metadaten muß nach dem Muster packagename.jdo benannt werden und sich in der gleichen Verzeichnisebene wie packagename befinden. Das Tool kann aus Ant heraus wie folgt aufgerufen werden: <java classname="org.xorm.tools.generator.GenerateJDO" output="model.jdo"> <arg value="info.artanis.locations.model" /> <arg value="bin" /> <classpath> <pathelement location="bin" /> <fileset dir="lib"> <include name="**/*.jar" /> </fileset> </classpath> </java> Die erzeugte XML-Datei muß vom Entwickler noch vervollständigt werden. Dabei handelt es sich hauptsächlich um die Angabe der Tabellen- und Spaltennamen, die mit den 66 4.6 JDO-Implementierungen Namen in der vorhin erzeugten XML-Datei übereinstimmen müssen. XORM hat gegenüber TJDO den Vorteil, daß auch unidirektionale 1:n-Beziehungen über Fremdschlüssel realisiert werden können. Dabei muß lediglich die entsprechende Fremdschlüsselspalte einer Tabelle angegeben werden, um das Elternobjekt zu referenzieren. Um die Referenzen von einem Elternobjekt zu seinen Kindobjekten herzustellen, benötigt XORM zwei <extension>-Elemente innerhalb des <collection>-Elements. Das erste gibt an, in welcher Tabelle die Kindelemente zu finden sind, das zweite die Fremdschlüsselspalte der entsprechenden Tabelle, die das Elternobjekt referenziert (Ausschnitt aus den Metadaten der Klassen State und Location): <class name="State"> <extension vendor-name="XORM" key="table" value="states" /> <field name="locations"> <collection element-type="Location"> <extension vendor-name="XORM" key="table" value="locations" /> <extension vendor-name="XORM" key="source" value="state_id" /> </collection> </field> [...] </class> <class name="Location"> <extension vendor-name="XORM" key="table" value="locations" /> <field name="state"> <extension vendor-name="XORM" key="column" value="state_id" /> </field> [...] </class> Da XORM erst zur Laufzeit konkrete Klassen erzeugt, ergeben sich daraus auch Änderungen im Quellcode. Eine Instanz kann nicht länger per new erzeugt werden, sondern muß von XORM mit Hilfe der Methode newInstance() geholt werden: State s = (State)XORM.newInstance(pm, State.class); s.setName("Mecklenburg-Vorpommern"); s.setCapital("Schwerin"); Alle anderen Aufrufe erfolgen entsprechend der JDO-API. Bei Löschoperationen führt XORM automatisch ein Cascading Delete aller Kindobjekte des zu löschenden Objekts durch. 4 | Java Data Objects 67 XORM unterstützt allerdings keine Vererbungsstrukturen, folglich hat das Attribut persistence-capable-superclass keine Wirkung. Stattdessen erzeugt XORM für jede Klasse eine eigene Tabelle mit allen nötigen Spalten. 4.7 JDO und XDoclet Dieser Abschnitt soll die Benutzung von XDoclet in Verbindung mit JDO (speziell LiDO) beschreiben. Dabei soll von den drei Klassen der Beispielanwendung ausgegangen werden, welche im folgenden um XDoclet-Kommentare erweitert werden, um anschließend mit Hilfe eines Ant-Tasks die JDO-Metadaten zu erzeugen. Bei XDoclet handelt es sich um einen Code-Generator, der das attributorientierte Programmieren in Java ermöglicht. Zu diesem Zweck können in den Quelltext spezielle Javadoc-Tags eingefügt werden. XDoclet parst den Quelltext und kann mit Hilfe dieser Tags zusätzliche Dateien und sogar Java-Klassen generieren. Ursprünglich wurde XDoclet mit dem Ziel entwickelt, die Entwicklung von EJBs zu vereinfachen. Seitdem hat sich XDoclet jedoch zu einem umfangreicheren Werkzeug entwickelt und liefert eine große Anzahl weiterer Module für die unterschiedlichsten Aufgaben mit. Einige dieser Module dienen der Erzeugung von JDO-Metadaten. Hierfür stehen eine Reihe von Tags sowie ein Ant-Task zur Verfügung. Die persistenten Klassen und deren Attribute werden mit Hilfe des Namespaces jdo beschrieben. XDoclet unterstützt zur Zeit LiDO, TJDO und KodoJDO mit eigenen Modulen. Die persistenten Klassen werden mit dem Tag @jdo.persistence-capable gekennzeichnet, jeder Member mit dem Tag @jdo.field. Damit sind die wesentlichen Angaben bereits vorhanden. Um den Typ eines Collection-Members näher zu beschreiben, stehen die Parameter collection-type und element-type zur Verfügung. Angaben über die Eigenschaften und Beziehungen auf Datenbankebene können mit den Tags @sql.field und @sql.relation realisiert werden. So kann mit @sql.field der Spaltenname in der Tabelle festgelegt werden. Der Tag @sql.relation ermöglicht unter anderem die Definition von Beziehungen. Über den Parameter style wird die Art der Beziehung als Fremdschlüssel-Beziehung (foreign-key) oder Relationentabelle (relation-table) festgelegt. Handelt es sich um eine Fremdschlüssel-Beziehung, so gibt der Parameter related-field den referenzierenden Member der Kindklasse an. Bei einer Relationentabelle kann der Name dieser Tabelle über den Parameter table-name angegeben werden. Um Extensionen einzufügen, stehen die Tags @jdo.package-vendor-extension, @jdo.class-vendor-extension und @jdo.field-vendor-extension zur Verfügung. Die jeweilige Bezeichnung spezifiziert, wo die Extension später eingesetzt wird. Als Parameter anzugeben sind vendor-name, key und value für die entsprechenden Attribute im XML-Element. Der folgende Codeblock zeigt einen Ausschnitt aus einer um XDoclet-Kommentare erweiterten Datenmodell-Klasse. Das gesamte Listing für alle drei Klassen befindet sich in Anhang A.9. 68 4.8 Fazit /* * @jdo.persistence-capable */ public class State implements Serializable { /** @jdo.field */ private String name; /** * @jdo.field * collection-type="collection" * element-type="Location" */ private Set locations; [...] } Mit Hilfe des Ant-Tasks jdodoclet kann nun die Erzeugung der Metadaten aus den XDoclet-Kommentaren erfolgen. Hierzu wird zunächst der Task in Ant geladen und anschließend ausgeführt: <taskdef name="jdodoclet" classname="xdoclet.modules.jdo.JdoDocletTask" classpathref="project.classpath" /> <jdodoclet destdir="${srcdir}"> <lido /> <jdometadata /> <fileset dir="${srcdir}"> <include name="model/*.java" /> </fileset> </jdodoclet> Als Attribut anzugeben ist das Zielverzeichnis für die erzeugten Dateien. Die Subtasks geben an, für welche JDO-Implementierung die Daten erzeugt werden sollen (hier: LiDO) und welche Dateien bearbeitet werden sollen. XDoclet erzeugt daraufhin für jede persistente Klasse eine Metadatei, die dann im weiteren Verlauf für das Enhancement, die Schemaerzeugung und die Verwendung in der Applikation zur Verfügung steht. 4.8 Fazit Bei JDO handelt es sich um ein Java-Persistenzframework, das portable und herstellerunabhängige Persistenz für Java-Objekte verspricht. Die Spezifikation abstrahiert das Datenmodell vollständig vom darunterliegenden Datenspeicher und ermöglicht transparente Persistenz aus Applikationen heraus. Der Aufwand, mit dem persistente Klassen erzeugt werden, ist relativ gering, gerade auch im Vergleich mit EJB. Die API, um 4 | Java Data Objects 69 Objekte persistent zu machen, ist einfach zu bedienen, sehr übersichtlich und dennoch umfassend in ihrer Funktionalität. Das Bytecode Enhancement ist eine umstrittene, aber akzeptable Lösung, da Alternativen wie Reflection zu langsam wären. Sun hat jedoch durch die Spezifikation festgelegt, daß der von unterschiedlichen JDO-Implementierungen erzeugte Bytecode untereinander kompatibel sein muß. JDOQL macht einen noch sehr unreifen Eindruck. So ist es eine stringbasierte Anfragesprache, was gerade im objektorientierten Umfeld sehr ungewöhnlich ist. Eine spezielle API für die Erzeugung von Anfragen wäre geeigneter. Zwar lassen sich dynamische Abfragen erzeugen, allerdings ist ein aufwendiges Parsen nötig, um SQL-Anfragen zu erzeugen. Zudem fehlen wichtige Operatoren und vor allem Aggregatfunktionen. Hier zeigen andere Werkzeuge bessere Möglichkeiten. Mit den nächsten Versionen wird sich JDO jedoch als Standard für den Zugriff auf persistente Java-Objekte durchsetzen können. 5 | Objektrelationale Mapper Neben Werkzeugen, die den im vorangegangenen Kapitel vorgestellten Standard JDO implementieren, gibt es eine Reihe von proprietären Tools, die eine eigene MappingStrategie bei der Abbildung von Java-Objekten auf relationale Strukturen verfolgen. Wie bereits in Abschnitt 2.2 erklärt wurde, dienen objektrelationale Mappingtools (kurz O/R-Mapper) als Brücke zwischen dem objektorientierten Programmierparadigma und der relationalen Datenbankwelt und bilden die Daten von persistenten Objekten (nicht aber die Objekte selbst) auf entsprechende Spalten in einer Datenbanktabelle ab. Abbildung 5.1 verdeutlicht dieses Prinzip. Abbildung 5.1: Allgemeine Architektur eines O/R-Mappers Die Menge der Werkzeuge läßt sich prinzipiell in zwei Gruppen einteilen: in Schemageneratoren, die ausgehend von vorhandenen Java-Klassen ein Datenbankschema erzeugen, und Codegeneratoren, die aus einem vorhandenen Datenbankschema eine komplette Persistenzschicht erzeugen. Die Schemageneratoren funktionieren nach dem gleichen Prinzip wie Enterprise JavaBeans und JDO-Implementierungen: Sie benötigen Metadaten im XML-Format, um die Abbildung von Objektattributen auf Datenbankfelder und die Beziehungen zu anderen Objekten zu beschreiben. Diese müssen dann auch zur Laufzeit der Applikation verfügbar sein. Allerdings sind diese XML-Metadaten bei weitem nicht so umfangreich wie der 72 5.1 Apache ObJectRelational Bridge Deployment-Deskriptor von CMP-Entity Beans. Außerdem können problemlos arbiträre Klassen abgebildet werden, und auch ein Bytecode Enhancement findet nicht statt. Einige Schemageneratoren bringen neben der obligatorischen eigenen API auch Implementierungen von Standard-APIs wie ODMG oder JDO mit, diese sind allerdings der proprietären API in Umfang und Funktionalität weit unterlegen. Codegeneratoren sind nicht von XML-Metadaten abhängig. Sie lesen meist nur das Datenbankschema ein und erzeugen daraus sofort die Persistenzschicht. Diese basiert in den meisten Fällen auf dem Entwurfsmuster der Data Access Objects (DAO). Ähnlich wie JDO sind die proprietären O/R-Mapper nicht auf eine J2EE-Umgebung angewiesen, sondern können auch in normalen Java-Applikationen benutzt werden. Einige der Tools können allerdings als alternative Persistenzschicht in J2EE-Applikationsservern eingesetzt werden (z.B. Hibernate und TopLink). Im weiteren Verlauf sollen vier Tools aus der Gruppe der Schemageneratoren vorgestellt werden: Apache ObJectRelational Bridge, Exolab Castor, Hibernate und TopLink. Hibernate und TopLink sind die Tools mit dem größten Verbreitungsgrad und bieten auch die Möglichkeit der Code-Generierung. 5.1 Apache ObJectRelational Bridge Um die transparente Persistenz von Java-Objekten zu realisieren, bietet das Apache DB Project die ObJectRelationalBridge (OJB) als objektrelationales Mapping-Tool an. OJB wird mit drei unterschiedlichen APIs ausgeliefert: einer ODMG 3.0-konformen API, einer noch nicht vollständig implementierten JDO-API sowie der PersistenceBroker-API. Letztere stellt den eigentlichen Kern dar, auf dem die beiden anderen APIs basieren. OJB kann sowohl in normalen Anwendungen als auch innerhalb von J2EE-Applikationsservern eingesetzt werden; ein JNDI-Lookup auf Datenquellen ist möglich. Weitere Features sind unter anderem Objekt-Caching, Transaktionslevel, optimistisches und pessimistisches Locking sowie ein Sequence-Manager für die automatische Erzeugung von Schlüsseln. Auch OJB verwendet für das eigentliche Mapping eine Reihe von XML-Dateien. Diese sind Teil des sogenannten Dynamic MetaData Layer, der sogar zur Laufzeit verändert werden kann, um beispielsweise das Verhalten des Persistenzkerns anzupassen. OJB setzt prinzipiell keine speziell implementierten Interfaces voraus, allerdings sind folgende Vorgaben zu beachten: Zum einen muß ein Member vorhanden sein, der den Primärschlüssel aus der Datenbank enthält, zum anderen muß für Referenzen auf ein anderes persistentes Objekt ein zusätzlicher Member für dessen ID existieren. Letzteren benötigt OJB, um die Referenz zum entsprechenden Objekt herstellen zu können. public class Location { private Long stateId; private State state; } 5 | Objektrelationale Mapper 5.1.1 73 XML-Metadaten Die Metadaten setzen sich aus mehreren XML-Dateien zusammen, von denen der Entwickler mindestens zwei anpassen muß. Grundlage ist die Datei repository.xml, die alle weiteren Dateien einbindet. Wichtig sind dabei die repository_database.xml, welche die Zugangsdaten für die Datenbank enthält sowie die repository_user.xml mit den eigentlichen Mapping-Informationen. Weiterhin muß die Datei repository_ internal.xml verfügbar sein, die Mapping-Informationen für OJB-interne Tabellen enthält. Diese neun Tabellen müssen zudem erzeugt werden (entweder manuell oder mit Hilfe der Build-Datei von OJB). Die Datei repository_database.xml enthält alle von der Applikation benutzten Datenbankverbindungen; die für jede dieser Verbindungen notwendigen Daten werden in einem <jdbc-connection-descriptor>-Element angegeben. Weiterhin muß eine der Verbindungen als default gekennzeichnet werden. Zusätzlich zu den üblichen Zugangsinformationen wird der zu verwendende Sequence-Manager angegeben. Hier stehen verschiedene High/Low-Generatoren sowie datenbankgestützte Sequenzen zur Verfügung. Außerdem ist es möglich, eigene Generatoren einzusetzen. <jdbc-connection-descriptor jcd-alias="default" default-connection="true" platform="MySQL" jdbc-level="2.0" driver="com.mysql.jdbc.Driver" protocol="jdbc" subprotocol="mysql" dbalias="/localhost:3306/jboss_db" username="jboss" password="jboss" eager-release="false" batch-mode="false" useAutoCommit="1" ignoreAutoCommitExceptions="false"> <sequence-manager className="SequenceManagerHighLowImpl"> <attribute attribute-name="grabSize" attribute-value="65536"/> <attribute attribute-name="globalSequenceId" attribute-value="false"/> <attribute attribute-name="globalSequenceStart" attribute-value="1000"/> <attribute attribute-name="autoNaming" attribute-value="true"/> 74 5.1 Apache ObJectRelational Bridge </sequence-manager> </jdbc-connection-descriptor > In der repository_user.xml hinterlegt der Entwickler alle für seine Anwendung nötigen Mapping-Informationen. Für Klassen und deren einfache sowie Collection-Member stehen entsprechende Deskriptor-Elemente zur Verfügung. Für jede Klasse werden der vollständige Name sowie die Zieltabelle angegeben, für jeden Member der Name, die Tabellenspalte sowie der SQL-Datentyp. Der Member, der den Primärschlüssel aus der Datenbank enthält, wird mit dem Attribut primarykey gekennzeichnet; das Attribut autoincrement gibt an, ob OJB automatisch IDs für neue Objekte erzeugen soll. Referenzen werden mit dem Element <reference-descriptor> beschrieben und enthalten im Kindelement <foreignkey> den Namen des Members, der die ID des referenzierten Objekts enthält. Dies wurde bereits weiter oben angesprochen: OJB kann den Fremdschlüssel nicht direkt aus einer Tabellenspalte beziehen, sondern benötigt einen Extra-Member. Für Collections gibt das Element <inverse-foreignkey> den Member des Kindobjekts an, der den Fremdschlüssel des Elternobjekts enthält. Für Collections und Referenzen kann bei Verwendung der PersistenceBroker-API die Kaskadierung von Operationen über die Attribute auto-update (Speichern, Aktualisieren) und auto-delete (Löschen) festgelegt werden. Wird eine der beiden anderen APIs benutzt, so dürfen die Attribute nicht angegeben werden, die Kaskadierung der entsprechenden Operationen erfolgt dort automatisch. <class-descriptor class="State" table="states"> <field-descriptor name="id" column="id" jdbc-type="BIGINT" primarykey="true" autoincrement="true" /> <field-descriptor name="name" column="name" jdbc-type="VARCHAR" /> <collection-descriptor name="locations" element-class-ref="Location" auto-update="true" auto-delete="true"> <inverse-foreignkey field-ref="stateId" /> </collection-descriptor> [...] </class-descriptor> <class-descriptor class="Location" table="locations"> [...] <field-descriptor name="stateId" column="state_id" jdbc-type="BIGINT" /> <reference-descriptor name="state" class-ref="State"> 5 | Objektrelationale Mapper 75 <foreignkey field-ref="stateId" /> </reference-descriptor> </class-descriptor> 5.1.2 Die PersistenceBroker-API Bei der PersistenceBroker-API handelt es sich um die Kern-API von OJB, auf welche die beiden Standard-APIs aufsetzen. Hauptkomponente dieser API ist das PersistenceBroker-Interface, welches alle notwendigen Operationen für das Speichern, Löschen und Holen von Objekten bereitstellt. Ein PersistenceBroker wird mit Hilfe einer FactoryKlasse verfügbar: PersistenceBroker broker = PersistenceBrokerFactory.defaultPersistenceBroker(); broker.beginTransaction(); // [...] broker.commitTransaction(); Der PersistenceBroker arbeitet vollständig transaktionsbasiert. Eine Transaktion wird mit beginTransaction() begonnen und mit commitTransaction() beendet. Für ein Rollback steht die Methode abortTransaction() zur Verfügung. Laden von Objekten Das Laden von Objekten erfolgt über das sogenannte query by criteria. Dabei wird eine Anfrage an die Datenbank nicht in Form eines Strings erzeugt, sondern nach objektorientierten Prinzipien. Eine Anfrage dieser Form besteht üblicherweise aus der Klasse der abzufragenden Objekte, einer Liste von Auswahlkriterien, einem DISTINCT-Flag sowie einer Sortierreihenfolge. Anfragen werden über das Query-Interface erzeugt, Instanzen werden von einer Factory-Klasse geholt. Auswahlkriterien werden in Criteria-Objekten gekapselt. Dabei stehen für fast jeden SQL-Operator und viele SQL-Funktionen entsprechende Methoden zur Verfügung. Auch die Abfrage auf null ist möglich. Nach Zusammenstellung aller Kriterien werden diese dem Query-Objekt übergeben und die Anfrage über den PersistenceBroker an die Datenbank abgesetzt. Im folgenden Beispiel besteht die Ergebnismenge aus vollständig geladenen Objekten inklusive aller Referenzen ungeachtet der Tatsache, ob im weiteren Programmverlauf auch wirklich alle Objekte der Ergebnismenge genutzt werden. Hier bietet OJB jedoch eine Optimierung an: Ist der Entwickler nicht sicher, später alle Objekte zu verwenden, so kann alternativ die Methode getIteratorByQuery() verwendet werden. Diese liefert einen Iterator zurück und lädt die Objekte der Ergebnismenge erst dann, wenn wirklich auf sie zugegriffen wird. Das folgende Beispiel lädt alle Location-Objekte, deren Name auf das übergebene Muster endet und die im zuvor geladenen Bundesland liegen: 76 5.1 Apache ObJectRelational Bridge // state ist ein zuvor geladenes State-Objekt Criteria crit = new Criteria(); crit.addLike("name", "%see"); crit.addEqualTo("state.id", state.getId()); Query q = QueryFactory.newQuery(Location.class, crit); Collection c = broker.getCollectionByQuery(q); Alternativ kann auch query by example angewendet werden: Hierbei wird ein Objekt vom gewünschten Typ erzeugt und die Attribute, nach denen abgefragt werden soll, mit entsprechenden Werten gesetzt. OJB liefert dann all die Objekte zurück, die den gesetzten Werten entsprechen. Location l = new Location(); l.setName("Schwerin"); Query q = QueryFactory.newQueryByExample(l); Dieses Prinzip wird auch angewendet, um ein Objekt anhand seiner ID aus der Datenbank zu laden: State s = new State(); s.setId(42); Identity id = new Identity(s, broker); s = (State)broker.getObjectByIdentity(id); Speichern und Löschen von Objekten Um ein Objekt in der Datenbank persistent zu machen, genügt der Aufruf der Methode PersistenceBroker#store() mit dem zu speichernden Objekt als Parameter. Diese Methode muß auch aufgerufen werden, wenn ein zuvor geladenes Objekt verändert wurde: Erst mit dem Aufruf wird die Aktualisierung in der Datenbank ausgeführt. Für das Löschen von Objekten stehen zwei Möglichkeiten zur Verfügung: Soll genau ein Objekt gelöscht werden, so kann die Methode PersistenceBroker#delete() verwendet werden. Sollen mehrere Objekte nach einem bestimmten Auswahlkriterium gelöscht werden, so steht die Methode PersistenceBroker#deleteByQuery() zur Verfügung, die alle Objekte aus der Datenbank löscht, die dem übergebenen Query entsprechen. OJB überzeugt durch das Vorhandensein von drei unterschiedlichen APIs und Query By Criteria. Damit wird auch die Anfragesprache zumindest bei der PersistenceBroker-API auf ein objektorientiertes Niveau angehoben. Von Nachteil ist der hohe Vorbereitungsaufwand, bis eine Anwendung ihre Daten mit OJB persistent machen kann (u.a. neun zusätzliche Tabellen in der Datenbank). 5 | Objektrelationale Mapper 5.2 77 Exolab Castor Exolab Castor ist ein Open Source Data Binding Framework für Java und unterstützt neben dem Mapping von Java-Objekten auf relationale Datenbanken (CastorJDO) auch die Abbildung auf XML-Dateien (CastorXML). Obwohl der Name auf eine JDO-Implementierung hinweist, ist CastorJDO nicht kompatibel zur JDO-Spezifikation [JDOSp] von Sun. Castor wurde speziell darauf optimiert, die Menge der Anfragen an die Datenbank möglichst gering zu halten. Ermöglicht wird dies durch Techniken wie LRU-Cache, optimistisches und pessimistisches Sperren, Transaktionen mit 2-Phase-Commit und DeadlockErkennung. Auch Castor bietet eine eigene Anfragesprache an: OQL. Auch diese Sprache ähnelt SQL in gewissem Maße, allerdings werden alle Operationen auf Java-Objekten ausgeführt und nicht auf Datenbank-Tabellen. Neben den üblichen logischen Ausdrücken unterstützt OQL auch Aggregatfunktionen, mathematische Ausdrücke sowie objektorientierte Ausdrücke wie Casts und Pfadausdrücke. 5.2.1 Konfiguration Die Konfiguration von Castor gibt an, welches Datenbanksystem benutzt wird, auf welche Art darauf zugegriffen werden soll und wo sich die Mapping-Informationen befinden. Die Mapping-Informationen werden aus externen Dateien eingelesen, wodurch eine Verteilung auf beliebig viele Mappingdateien möglich ist. Alle notwendigen Daten werden in XML-Form gespeichert und vor Erzeugung einer Database-Instanz eingelesen. Übergeben wird dabei der Name der zu verwendenden Datenbank sowie der Name der Konfigurationsdatei. JDO jdo = new JDO(); jdo.setDatabaseName("jboss_db"); jdo.setConfiguration("database.xml"); jdo.setClassLoader(getClass().getClassLoader()); Database db = jdo.getDatabase(); Der Datenbankname muß dem in der Konfigurationsdatei angegebenen Namen entsprechen. Daraus könnte geschlossen werden, daß Castor mehrere unterschiedliche Datenbankverbindungen zuließe, es ist in der XML-Datei jedoch nur ein <database>Element erlaubt. Dieses enthält als Attribute neben einem eindeutigen Namen einen Alias für das verwendete Datenbanksystem. Darunter folgen die Zugangsinformationen für die verwendete Datenbank sowie der Pfad zur Mapping-Datei. <database name="jboss_db" engine="mysql"> <!-- Datenbank-Informationen --> <mapping href="mapping.xml" /> </database> 78 5.2 Exolab Castor Castor bietet drei verschiedene Wege an, um auf eine relationale Datenbank zuzugreifen: • JDBC-URL Hierfür steht das <driver>-Element zur Verfügung. Angegeben werden der Name des zu verwendenden JDBC-Treibers sowie die URL zur Datenbank. Als weitere Parameter folgen Benutzername und Paßwort für den Datenbankzugang. <driver url="jdbc:mysql://localhost:3306/jboss_db" class-name="org.gjt.mm.mysql.Driver"> <param name="user" value="jboss" /> <param name="password" value="jboss" /> </driver> • JDBC-DataSource JDBC bietet in der Version 2 einen Datenbankzugang über eine DataSource an. Ein JDBC-Treiber kann das Interface javax.sql.DataSource implementieren, um einen solchen Zugriff zu ermöglichen. Castor kann mit Hilfe des <data-source>Elements auf die angegebene Datenquelle zugreifen. Auch hier sind neben dem zu verwendenden Treiber alle anderen notwendigen Informationen wie Servername, Port, Datenbankname sowie Zugangsdaten anzugeben. <data-source class-name="com.mysql.jdbc.jdbc2.optional.MySQLDataSource"> <params server-name="localhost" port-number="3306" database-name="jboss_db" user="jboss" password="jboss" /> </data-source> • JNDI Die dritte Möglichkeit ist der Zugriff auf eine über JNDI registrierte Datenquelle. Damit ist der Einsatz von Castor im J2EE-Umfeld möglich. Mit Hilfe des <jndi>Elements wird der JNDI-Name der Datenquelle angegeben, weitere Angaben sind nicht notwendig. <jndi name="java:/MySqlDS" /> 5.2.2 Mapping-Informationen Auch Castor benutzt eine Mapping-Datei im XML-Format, um die Abbildung von JavaObjekten auf relationale Datenbanktabellen zu beschreiben. Die Mapping-Datei von Castor kann entweder per Hand oder durch Einsatz eines Drittanbieter-Tools wie CastorOil oder JDOMapper (siehe Anhang A.10) erzeugt werden und hat folgende Grundstruktur: 5 | Objektrelationale Mapper 79 <mapping> <key-generator name="..."> [...] </key-generator> <class name="..."> <map-to ... /> <field name="..."> <sql ... /> </field> [...] </class> </mapping> Jede Javaklasse wird durch ein <class>-Element beschrieben, welches neben einem <map-to>-Element eine Menge von <field>-Elementen für die Member enthält. Das <class>-Element enthält neben dem vollständigen Namen der Klasse den für die eindeutige Identifizierung verwendeten Member sowie den für die Erzeugung von IDs verwendeten Key-Generator. Das Element <map-to> gibt den Namen der Datenbanktabelle an, auf welche die Objekte der entsprechenden Javaklasse abgebildet werden sollen. Jedes <field>-Element beschreibt einen Member der Klasse genauer. Neben dem Namen kann der Datentyp angegeben werden; entfällt die Angabe, so ermittelt Castor den Datentyp mit Hilfe der Reflection API. Außerdem ist die Angabe der get-/setMethoden für einen Member möglich, falls diese in ihrer Benennung von der JavaBeansSpezifikation abweichen. Jedes <field>-Element enthält ein <sql>-Element, welches die Datenbankspalte angibt, auf die der Member abgebildet werden soll. Handelt es sich bei einem Member um eine Collection, so kann mit Hilfe des Attributs collection deren Datentyp näher spezifiziert werden. Zur Verfügung stehen die Werte array, arraylist, collection, hashtable, map, set und vector. Für Beziehungen sind keine speziellen Angaben notwendig. Jedoch ist zu beachten, daß Castor zur Zeit nur mit bidirektionalen Beziehungen umgehen kann. Für unidirektionale Beziehungen ist das Verhalten von Castor nicht vorhersagbar. Es folgt ein Ausschnitt aus den Mapping-Informationen der Klassen State und Location. Die Identifizierung eines Objekts erfolgt in beiden Fällen über den Member id, der Member locations der Klasse State ist als Set deklariert. Das Laden der Objekte erfolgt per Lazy Initialization. <class name="info.artanis.locations.model.State" identity="id" key-generator="HIGH-LOW"> <map-to table="states" /> <field name="id"> <sql name="id" /> </field> 80 5.2 Exolab Castor <field name="locations" type="info.artanis.locations.model.Location" collection="set" lazy="true"> <sql many-key="state_id"/> </field> </class> <class name="info.artanis.locations.model.Location" identity="id" key-generator="HIGH-LOW"> <map-to table="locations" /> <field name="id"> <sql name="id" /> </field> <field name="state" type="info.artanis.locations.model.State"> <sql name="state_id" /> </field> </class> Das Attribut key-generator verweist auf den zu verwendenden Key-Generator, mit dessen Hilfe IDs für neue Instanzen erzeugt werden sollen. Castor bietet insgesamt fünf Generatoren an: HIGH-LOW, IDENTITY, MAX, SEQUENCE sowie UUID. Für den verwendeten Generator kann in der Mapping-Datei ein <key-generator>-Element angegeben werden, womit zusätzliche Parameter für den Key-Generator festgelegt werden können. Das folgende Beispiel zeigt die Konfiguration eines HIGH-LOW-Generators. Angegeben sind die zu verwendende Datenbanktabelle, deren Spaltennamen sowie die Grab-Size (siehe Anhang A.4). Castor legt in dieser Tabelle jeweils ein Schlüssel-/WertPaar an, bestehend aus Tabellenname und einem Integer-Wert für die Berechnung des nächstfolgenden Schlüssels. Wird der Parameter global angegeben, so werden global eindeutige IDs erzeugt, nicht für jede Tabelle separate. Castor legt dann nur einen Eintrag in der Tabelle mit dem Schlüssel <GLOBAL> an. <key-generator name="HIGH-LOW"> <param name="table" value="castor_unique_keys"/> <param name="key-column" value="tablename"/> <param name="value-column" value="next_hi"/> <param name="grab-size" value="65536"/> <param name="global" value="true" /> </key-generator> 5.2.3 Abhängige und unabhängige Objekte Castor unterscheidet zwei Arten von Objekten: abhängige Objekte und unabhängige Objekte. 5 | Objektrelationale Mapper 81 Ein unabhängiges Objekt kann laut Castor beliebig erzeugt, verändert und gelöscht werden, es ist in seinem Lebenszyklus unabhängig von anderen Objekten. Ohne zusätzliche Angaben in den Mapping-Informationen ist jedes Objekt ein unabhängiges Objekt. Abhängige Objekte hängen in ihrem Lebensyklus von einem anderen Objekt ab, das heißt, sie können nicht ohne Existenz eines Elternobjekts in der Datenbank erzeugt, verändert oder gelöscht werden. Derartige Objekte werden in den Mapping-Informationen über das zusätzliche Attribut depends im <class>-Element kenntlich gemacht: <class name="info.artanis.locations.model.Location" identity="id" key-generator="HIGH-LOW" depends="info.artanis.locations.model.State"> Folgende Dinge sind bei derart definierten Abhängigkeiten zu beachten: Die Klasse, die im depends-Attribut angegeben ist, muß in der Mapping-Datei vor der abhängigen Klasse definiert werden. Außerdem ist es nicht möglich, auf Objekte einer abhängigen Klasse direkt zuzugreifen, das heißt, es muß immer der Weg über das Elternobjekt gegangen werden. Castor „unterstützt“ dieses Vorgehen, indem Mapping-Informationen von abhängigen Klassen quasi vor dem Entwickler „versteckt“ werden, er also keine Objekte abhängiger Klassen direkt persistent machen oder laden kann. Bei abhängigen Objekten ist die Kaskadierung von Operationen wie Speichern, Löschen und Aktualisieren gewährleistet, bei unabhängigen Objekten muß der Entwickler selbst dafür sorgen, daß referenzierte Objekte entsprechend gehandhabt werden. Dies sei kurz am verwendeten Beispiel der Ortsdatenbank erklärt: Prinzipiell wäre angebracht, die Klasse Location von der Klasse State abhängig zu machen, da ein Ort nicht ohne ein Bundesland existieren kann. Ein Location-Objekt muß dann immer erzeugt und zu einem existierenden State-Objekt hinzugefügt werden, welches dann in der Datenbank aktualisiert werden kann. Das Laden erfolgt ebenso nur über das zugehörige State-Objekt. Probleme gibt es erst, wenn man auf Location-Objekte direkt zugreifen möchte, beispielsweise all jene Objekte laden möchte, die in ihrem Namen einem bestimmten Textmuster entsprechen. Dies ist bei einem abhängigen Objekt nicht möglich, da Castor die dazugehörigen Mapping-Informationen nicht finden kann. Hier muß der Entwickler einen Mittelweg finden und genau abwägen, wie die Struktur des Datenmodells aussehen muß. In der Beispiel-Implementierung wurden alle Objekte als unabhängig deklariert und beim Löschen eines State-Objekts werden zunächst alle dazugehörigen Location-Objekte gelöscht, bevor das State-Objekt selbst gelöscht wird. 5.2.4 Die Castor-API Castor arbeitet vollständig transaktionsbasiert. Sowohl lesende als auch schreibende Zugriffe auf die Datenbank müssen innerhalb eines Transaktionskontextes ausgeführt werden. Für die Transaktionssteuerung stehen die Methoden begin(), commit() und rollback() aus der Klasse Database zur Verfügung. Eine entsprechende Implementierung kann wie folgt aussehen: try { 82 5.2 Exolab Castor db.begin(); // Objekte laden, manipulieren, speichern, löschen ... } catch ( Exception e1 ) { try { if ( db.isActive() db.rollback(); } catch ( Exception e2 ) } finally { try { if ( db.isActive() db.commit(); db.close(); } catch ( Exception e3 ) } ) { } ) { } Ein transientes Objekt wird mit der Methode Database#create() persistent gemacht. Sollte das Objekt abhängige Objekte enthalten, so werden auch diese gespeichert (Persistence by Reachability). Für das Löschen eines Objekts steht die Methode Database#remove() zur Verfügung. Das zu löschende Objekt muß zuvor aus der Datenbank geladen werden. Dies kann beispielsweise mit der Methode Database#load() geschehen, sofern die Objekt-ID bekannt ist. Auch für das Löschen gilt: Eine Kaskadierung auf Kindobjekte findet nur dann statt, wenn die Kindobjekte als abhängige Objekte deklariert wurden. Änderungen an geladenen Objekten werden automatisch mit dem Commit in die Datenbank übernommen. Castor unterstützt aber auch mehrschichtige Architekturen, in denen ein Objekt in einer Transaktion geladen und erst in einer späteren Transaktion aktualisiert wird. Hierfür kann die Methode Database#update(Object) auf das entsprechende Objekt angewendet werden. Allerdings muß ein Objekt das Interface org.exolab.castor.jdo.TimeStampable implementieren, damit eine Aktualisierung mit dieser Methode erfolgen kann. Anfragen mit OQL Um eine Anfrage an die Datenbank abzusetzen, muß ein OQLQuery-Objekt von der Datenbank geholt werden. Optional kann der Anfragestring bereits der Factory-Methode Database#getOQLQuery() übergeben werden. Das Objekt wird dann benutzt, um die Anfrage zu erzeugen, alle gewünschten Parameter zu setzen und schließlich die Ergebnismenge zu holen. Der Anfragestring folgt in seiner Syntax SQL-üblichen Regeln, 5 | Objektrelationale Mapper 83 abgesehen davon, daß nicht auf Datenbanktabellen, sondern auf Java-Objekten operiert wird. Als Platzhalter für Parameter dient das Dollarzeichen, gefolgt von einer Zahl. Jeder Parameter wird über OQLQuery#bind() and das OQLQuery-Objekt gebunden. Dabei müssen Reihenfolge und Datentyp der angegebenen Parameter beachtet werden. Zuletzt wird die Ergebnismenge durch Aufruf der Methode OQLQuery#execute() geholt. Diese liegt dann als QueryResults-Objekt vor, welches wie eine Enumeration durchlaufen werden kann. Das folgende Beispiel soll alle Location-Objekte aus der Datenbank holen, deren Name dem übergebenen Textmuster entspricht: String aPattern = "%see"; OQLQuery q = db.getOQLQuery("SELECT l "+ "FROM info.artanis.locations.model.Location l "+ "WHERE l.name like $1"); q.bind(aPattern); QueryResults res = q.execute(); Sollen alle Instanzen einer Klasse ermittelt werden, so kann eine WHERE-Klausel entfallen. Das OQLQuery-Objekt kann allerdings nicht nur mit OQL-Anfragen umgehen, sondern auch SQL-Anfragen direkt an die Datenbank absetzen und Stored Procedures in der Datenbank aufrufen. Bei Castor überzeugen vor allem die einfache API sowie das übersichtliche Format der Mapping-Dateien. Negativ aufgefallen ist das Prinzip der abhängigen Objekte. Zwar werden damit kaskadierende Operationen ermöglicht, jedoch mit dem Nachteil, daß man nie direkt auf derartige Objekte zugreifen kann (z.B. über Anfragen), sondern immer über das Elternobjekt gehen muß. 5.3 Hibernate Hibernate ist der bekannteste und am häufigsten eingesetzte Open Source O/R-Mapper. Das Tool zeichnet sich durch hohe Performance und eine umfassende Unterstützung der wichtigsten Datenbanksysteme aus. Neben einer Anwendung in normalen JavaApplikationen ist die Integration in J2EE-Applikationsserver über JCA oder JMX möglich. Hibernate stellt neben einer eigenen API auch eine ODMG 3-konforme API zur Verfügung, die allerdings einen nicht sehr großen Funktionsumfang hat. Hibernate bietet dem Entwickler eine gute Unterstützung bei der Erstellung einer Persistenzschicht an. So werden Tools mitgeliefert, die ein Forward oder Reverse Engineering ermöglichen, so daß der Entwickler nur noch optimierend eingreifen muß. Hibernate kann alle Klassen persistent machen, deren Aufbau der JavaBeans-Spezifikation entspricht und unterstützt 1:1-, 1:n- und n:m-Beziehungen sowohl unidirektional 84 5.3 Hibernate als auch bidirektional. Auch ternäre Beziehungen stellen für Hibernate kein Problem dar. Jede persistent zu machende Klasse kann optional die Interfaces Lifecycle und Validatable implementieren. Lifecycle bietet eine Reihe von Callbackmethoden an, die nach dem Laden und Speichern bzw. vor dem Aktualisieren und Löschen aufgerufen werden. Durch die Implementierung von Validatable kann der Zustand vor der Persistierung eines Objekts geprüft werden. Für das Absetzen von Anfragen an die Datenbank bietet Hibernate eine umfangreiche API sowie eine eigene Anfragesprache an, die Hibernate Query Language, kurz HQL. Sie ähnelt in ihrer Syntax SQL, ist aber vollständig objektorientiert. So kann HQL mit Polymorphismus und Assoziationen umgehen, außerdem ist der Zugriff auf Member über Objektpfade möglich. Auch die von SQL bekannten Aggregatfunktionen wie avg, min, max oder count können innerhalb von HQL-Anfragen benutzt werden. 5.3.1 Mapping-Informationen Auch Hibernate benötigt für jede persistente Klasse Mapping-Informationen, die in einer XML-Datei definiert werden müssen. Für jede Klasse wird ein <class>-Element mit dem Namen der persistenten Klasse sowie der Zieltabelle in der Datenbank angegeben. Darunter folgt ein <id>-Element, das den Primärschlüssel der persistenten Klasse definiert. Dieses Element ist für jede persistente Klasse notwendig. Darin werden als Attribute mindestens der Datentyp des Schlüssels sowie die Zielspalte der Tabelle angegeben. Wird zusätzlich das Attribut name angegeben, so wird der Wert auf den angegebenen Member der Klasse abgebildet. Für jede ID muß außerdem eine Generatorklasse angegeben werden, welche beim Speichern in die Datenbank einen entsprechenden Schlüssel erzeugt. Hibernate liefert bereits sieben solche Generatorklassen (z.B. High-Low- und UUID-Algorithmen) mit; eigene Generatorklassen können ebenfalls verwendet werden. Durch die Angabe von assigned wird die Erzeugung eines Schlüssels der Anwendung überlassen, durch native der verwendeten Datenbank. Nach dem <id>-Element folgt für jeden einfachen Member ein <property>-Element mit der Angabe des Member-Namens, des Datentyps und der Zielspalte. Wird der Datentyp nicht angegeben, so versucht Hibernate, diesen mit Hilfe von Reflection zu ermitteln. Für Beziehungen stehen die Elemente <one-to-one>, <many-to-one>, <oneto-many> sowie <many-to-many> zur Verfügung, für Collections bietet Hibernate abhängig von der Art der Collection die Elemente <set>, <list>, <map> und <bag> an. Die anzugebenden Attribute sind äquivalent: der Name des Members, die Zieltabelle und Angaben darüber, ob Operationen auf Kindelemente kaskadiert werden sollen, ob Lazy Initialization1 verwendet werden soll und ob es sich um eine inverse (bidirektionale) Beziehung handelt. Das folgende Beispiel zeigt einen Auszug aus dem Mapping der Klassen State und Location. Die 1:n-Beziehung zwischen beiden Klassen wird durch ein <set>-Element auf der 1-Seite definiert. Zusätzlich werden der Name der Fremdschlüsselspalte sowie 1 Lazy Initialization bedeutet, daß ein Objekt erst geladen wird, wenn die Anwendung darauf zugreift. 5 | Objektrelationale Mapper 85 der Datentyp der in der Collection enthaltenen Objekte angegeben. Auf der n-Seite wird lediglich ein <many-to-one>-Element für den Member definiert, der die Referenz auf das Elternobjekt hält. Durch die Angabe des Attributs inverse auf der 1-Seite wird die Beziehung als bidirektional gekennzeichnet. Außerdem gibt das Attribut cascade an, welche Operationen auf referenzierte Objekte ausgedehnt werden sollen. Gültige Werte sind none (Standard: keine), save-update (nur Speichern und Aktualisieren), delete (nur Löschen) und all (alle Operationen kaskadieren). <class name="info.artanis.locations.model.State" table="states"> <id name="id" type="long" column="id"> <generator class="hilo" /> </id> <property name="name" column="name" type="string"/> <set name="locations" table="locations" lazy="true" inverse="true" cascade="all"> <key column="state_id" /> <one-to-many class="info.artanis.locations.model.Location" /> </set> [...] </class> <class name="info.artanis.locations.model.Location" table="locations"> [...] <property name="name" column="name" type="string"/> <property name="zip" column="zip" type="int"/> <many-to-one name="state" column="state_id" class="info.artanis.locations.model.State"/> [...] </class> Hibernate bietet sowohl Forward Engineering als auch Reverse Engineering an, um einen Access Layer zu generieren. Allerdings muß der Entwickler in beiden Fällen noch eingreifen, um einige Optimierungen vorzunehmen. Für das Reverse Engineering bietet Hibernate ein graphisches Tool an. In der Oberfläche kann man die Tabellen auswählen, zu denen die Mapping-Informationen generiert werden sollen, weiterhin, ob für jede Klasse eine eigene Mapping-Datei oder eine gemeinsame Mapping-Datei generiert werden soll. Nach Auswahl eines Generators für die Primärschlüssel und eines Packagenamens werden die Mapping-Informationen vom Tool erzeugt. Es werden allerdings keine Beziehungen zwischen den Objekten generiert, sondern nur für jede Tabellenspalte ein Member mit einfachem Datentyp. Das heißt, an dieser Stelle muß der Entwickler die Beziehungen manuell herstellen und auch die Mapping-Datei ändern. Danach können mit Hilfe des Tools CodeGenerator die Javaklassen erzeugt werden. 86 5.3 Hibernate Das Forward Engineering wird durch die beiden Tools MapGenerator und SchemaExport realisiert. MapGenerator erzeugt eine Mapping-Datei aus vorhandenen Javaklassen, aber auch hier muß der Entwickler noch nachbessern. Danach kann mit SchemaExport das Datenbankschema aus den generierten Mapping-Informationen erzeugt werden. 5.3.2 Die Hibernate-API Bevor Objekte mit Hibernate persistent gemacht werden können, müssen zunächst alle notwendigen Mapping-Dateien und Properties in eine Configuration-Instanz geladen werden. Die Properties geben unter anderem an, welches Datenbanksystem benutzt wird, außerdem alle Daten für die zu erstellende Verbindung. Hibernate bietet im wesentlichen drei Wege an, eine Datenbankverbindung bereitzustellen: durch den Benutzer, durch Hibernate selbst oder über JNDI. Um beispielsweise eine Verbindung zu einer HSQL-Datenbank über JNDI zu holen, müssen folgende zwei Properties angegeben werden: hibernate.dialect cirrus.hibernate.sql.HSQLDialect hibernate.connection.datasource java:/HSqlDS Die erste Zeile gibt an, welcher „Dialekt“ verwendet wird, das heißt, auf welche Art Datenbanksystem zugegriffen wird. Damit hat Hibernate die Möglichkeit, gewisse Optimierungen hinsichtlich des Datenbankzugriffs vorzunehmen. Die zweite Zeile gibt den JNDI-Namen der zu verwendenden Datenquelle an. Nach dem Laden der Einstellungen steht eine Factory-Klasse zur Verfügung, von der die Anwendung die zum Datenbankzugriff nötige Session holen kann. Grundsätzlich ist zu empfehlen, nur eine solche SessionFactory in einer Anwendung zu halten und für jeden Datenbankzugriff eine Sitzung zu öffnen. Properties props = new Properties(); props.load(new FileInputStream("hibernate.properties")); Configuration cfg = new Configuration(); cfg.addFile("mapping.xml"); cfg.setProperties(props); SessionFactory sf = cfg.buildSessionFactory(); Session session = sf.openSession(); // Objekt laden, speichern oder manipulieren session.flush(); session.close(); Alle Operationen, die persistente Daten ändern, müssen durch den Aufruf der Methode Session#flush() abgeschlossen werden. Sie sorgt dafür, daß der Zustand der Datenbank mit dem Zustand des Speichers synchronisiert wird. Nach Abschluß des Datenbankzugriffs sollte die Sitzung über Session#close() wieder geschlossen werden. 5 | Objektrelationale Mapper 87 Objekte laden Ist die ID eines Objektes bekannt, so kann dieses über die Methode Session#load() geladen werden. Als Übergabeparameter erwartet sie den Datentyp des zu ladenden Objekts sowie dessen ID: Location l = (Location)session.load(Location.class, anId); Hibernate bietet die Methoden Session#find() sowie Session#iterate() an, um ein Objekt zu finden oder eine Menge von Objekten anhand bestimmter Kriterien auszuwählen und zu laden. Die Methode iterate() hat Performancevorteile, wenn nicht alle geladenen Objekte benötigt werden, oder wenn ein Teil der zu ladenden Objekte bereits im Cache liegt. Als Übergabeparameter erhalten beide Methoden eine Anfrage in der Hibernate Query Language (HQL) sowie eine beliebige Menge an Parametern als Auswahlkriterien. Als Platzhalter für die Parameter dient das Fragezeichen. Double latitude = new Double(52.5); List res = session.find( "from info.artanis.locations.model.Location as location " + "where location.latitude > ?", latitude, Hibernate.DOUBLE); Alternativ zu diesen beiden Methoden bietet Hibernate das Query-Interface an, mit dem der Entwickler die Möglichkeit hat, die Ergebnismenge einer Anfrage weiter einzuschränken. So kann beispielsweise die Ergebnismenge einer Anfrage mit den Methoden setFirstResult() und setMaxResults() begrenzt werden. Außerdem können benannte Parameter verwendet werden. Eine Instanz des Query-Interfaces wird von der aktuellen Session mit Hilfe der Methode Session#createQuery() geholt: Query q = session.createQuery( "from location " + "in class info.artanis.locations.model.Location " + "where location.name like :pattern"); q.setString("pattern", aPattern); list = q.list(); Der Anfrage-String wird bereits der Methode createQuery() übergeben, danach müssen über entsprechende set-Methoden die Parameter an das Query-Objekt übergeben werden. Hierbei gibt es für jeden Datentyp eine solche set-Methode; als Parameter dienen die Position des zu ersetzenden Platzhalters in der Anfrage (oder dessen Name bei benannten Parametern) sowie der einzusetzende Wert. Es ist also nicht notwendig, die Parameterwerte in der Reihenfolge dem Query-Objekt zu übergeben, in der sie im Anfrage-String auftauchen. Die Ergebnisse werden schließlich mit einer der Methoden list(), iterate() oder scroll() geholt. Auch hier hat iterate() die bereits genannten Performancevorteile. 88 5.3 Hibernate Objekte speichern oder löschen Ein Aufruf der Methode Session#save() genügt, um ein transientes Objekt persistent zu machen, der Rückgabewert ist die von Hibernate erzeugte ID des Objekts in der Datenbank: State s = new State("BB", "Brandenburg", "Potsdam"); Location l = new Location("Falkensee", 52.5667, 13.1, 14612); s.addLocation(l); session.save(s); Wurde in den Mapping-Informationen das Attribut cascade=“save-update“ oder cascade=“all“ angegeben, so wird der gesamte Objektbaum persistent gemacht. Für das obige Beispiel gilt also, daß auch das hinzugefügte Location-Objekt persistent gemacht wird (Persistence by Reachability). Hibernate bietet die Möglichkeit, ein Objekt in einer Session zu laden und nach dem Beenden dieser das Objekt in einer zweiten Session in der Datenbank zu aktualisieren. Dadurch ist Hibernate besonders für den Einsatz in mehrschichtigen Anwendungen, wie zum Beispiel Webanwendungen, geeignet. Für dieses Feature stehen dem Entwickler die Methoden Session#update() und Session#saveOrUpdate() zur Verfügung. Der Methode update() wird neben dem zu speichernden Objekt die ID des Objekts übergeben. Alternativ kann saveOrUpdate() verwendet werden; hierbei speichert oder aktualisiert Hibernate das übergebene Objekt abhängig vom Wert der Objekt-ID. Als Grundlage dient das in den Mapping-Informationen angegebene Attribut unsavedvalue. Hat die Objekt-ID den im Attribut angegebenen Wert, so wird das Objekt in der Datenbank gespeichert, andernfalls wird das Objekt mit der entsprechenden ID aktualisiert. In Webanwendungen sollte diese Methode den Methoden save() und update() vorgezogen werden. Für das Löschen eines Objekts steht die Methode Session#delete() zur Verfügung. Es kann ein zuvor in der Session geladenes Objekt gelöscht werden oder das zu löschende Objekt über eine HQL-Anfrage spezifiziert werden. Object o = session.load(State.class, anId); session.delete(o); Auch für diese Methode gilt: Wenn das Attribut cascade den Wert all oder delete hat, dann werden alle Kindobjekte ebenfalls gelöscht. 5.3.3 SessionFactory und Session Laut Hibernate-Dokumentation ist eine SessionFactory ein schwergewichtiges, threadsicheres Objekt, welches von allen Threads einer Anwendung geteilt werden sollte. Eine Session hingegen ist nicht threadsicher und sollte von einem Geschäftprozeß erzeugt, benutzt und anschließend wieder freigegeben werden [H2RD]. 5 | Objektrelationale Mapper 89 Eine Klasse, die mit Hibernate arbeitet, könnte also eine SessionFactory als Instanzvariable halten und in jeder Methode, die auf der Datenbank operiert, eine Session holen, Anfragen an diese absetzen und am Ende der Methode die Session wieder schließen. Allerdings sind bei diesem Vorgehen folgende Dinge zu beachten: Hat der Entwickler in der Mapping-Datei Lazy Loading beispielsweise für eine Collection festgelegt, so ist es nach dem Schließen der Session nicht möglich, auf deren Elemente zuzugreifen. Hibernate quittiert dies mit einer Exception, da keine Session geöffnet ist. Ist die Anwendung jedoch auf einen solchen Zugriff angewiesen, so bleiben zwei Alternativen: Zum einen kann das Lazy Loading abgeschaltet werden. Dies bedeutet allerdings, daß bei jedem Datenbankzugriff alle Objekte eines Objektbaums geladen werden, was zu Lasten der Performance geht. Die Alternative wäre, nicht die SessionFactory, sondern die Session selbst als Instanzvariable verfügbar zu machen. Hierbei ist jedoch zu beachten, daß die Verbindung zur Datenbank während der gesamten Lebenszeit der Instanz bestehen bleibt. Hibernate bietet dem Entwickler eine umfangreiche Palette von Werkzeugen an, um den Mapper bei unterschiedlichsten Voraussetzungen einsetzen zu können. Außerdem positiv hervorzuheben sind die einfach zu bedienende proprietäre API mit der umfangreichen Anfragesprache sowie die verfügbare ODMG-API. Die Mapping-Informationen sind leicht verständlich und mit Tool-Unterstützung (durch Hibernate selbst oder XDoclet) problemlos erstellbar. 5.4 Oracle9iAS TopLink TopLink ist ein urspünglich von Webgain entwickeltes Persistenz-Framework, welches Java-Applikationen den Zugriff auf relationale und nicht-relationale Datenquellen ermöglicht. Mittlerweile wurde der Mapper von Oracle übernommen und gehört nun zu den Tools des Oracle9i Application Servers. Oracle stellt unter der Development License eine voll funktionsfähige Version von TopLink für Testzwecke zur Verfügung. Für den kommerziellen Einsatz und den Einsatz in Produktivumgebungen belaufen sich die Lizenz-Kosten allerdings auf 5.000 US-Dollar pro CPU. TopLink unterstützt zur Zeit zwölf verschiedene Datenbanksysteme, kann aber auch auf weitere über natives JDBC zugreifen und dort neben einfachen Java-Objekten auch Enterprise JavaBeans abbilden. TopLink zeichnet sich durch hohe Performance, Caching und Lazy Loading (hier: Indirection) aus. 5.4.1 Deskriptoren und Mapping TopLink benutzt sogenannte Deskriptoren im XML-Format, um die Abbildung von Objektattributen auf Datenbankfelder zu realisieren und Beziehungen zu anderen Objekten zu beschreiben. Ein solcher Deskriptor liegt für jede persistente Klasse vor und enthält laut [OTS02] folgende Informationen: 90 5.4 Oracle9iAS TopLink • den Namen der beschriebenen Java-Klasse sowie den Namen der Datenbanktabelle, in der Instanzen dieser Klasse gespeichert werden sollen, • den Primärschlüssel der Tabelle, • die Beschreibung der Attribute und Beziehungen eines Objektes (das sogenannte Mapping), • zusätzliche Eigenschaften, um das Verhalten des Deskriptors anzupassen. Das Mapping beschreibt, wie die Daten eines Attributs gespeichert bzw. geladen werden sollen, TopLink unterscheidet hierbei zwischen zwei Arten: dem direct mapping und dem relationship mapping (siehe [OTS02]). Während das direct mapping ein Attribut direkt auf eine Tabellenspalte abbildet, beschreibt das relationship mapping Beziehungen zwischen Objekten. TopLink unterstützt 1:1-, 1:n- und n:m-Beziehungen sowohl unidirektional als auch bidirektional. TopLink ermöglicht das Mapping einer Klasse über mehrere Tabellen ebenso wie das Mapping von Aggregatobjekten. Bei letzterem handelt es sich um eine 1:1-Beziehung zwischen zwei Objekten, deren Daten allerdings in der gleichen Tabelle gespeichert werden. 5.4.2 Mapping Workbench Für die komfortable Erzeugung der Deskriptoren und Mappings liefert TopLink ein separates Tool mit graphischer Oberfläche namens Mapping Workbench mit. Im Rahmen dieser Arbeit soll jedoch nicht die gesamte Mächtigkeit dieser Oberfläche vorgestellt werden, sondern nur eine kurze Einführung erfolgen. Zunächst wird über File-Menü ein neues Projekt angelegt. Dabei sind ein Projektname, das verwendete Datenbanksystem sowie ein Zielverzeichnis anzugeben. Da zu jedem Projekt spezielle Verzeichnisse gehören, ist es empfehlenswert, jedes Projekt in einem eigenen Verzeichnis zu speichern (nach [OTT02]). Jedes Projekt hat einen eigenen Classpath, in den alle später verwendeten persistenten Klassen aufgenommen werden müssen. Dies geschieht im General-Tab des Projekts über den Button Add Entry. Anschließend werden die persistenten Klassen über den Menüpunkt Selected -> Add/Refresh Classes in das Projekt aufgenommen. TopLink zeigt die ausgewählten Klassen im Projektbaum an, erzeugt für jede einen Deskriptor und markiert alle Attribute als unmapped. Die Tabellen, auf die die Abbildung erfolgen soll, können entweder aus der Datenbank importiert oder im Mapping Workbench direkt erzeugt und von dort in die Datenbank exportiert werden. Dazu ist es zunächst nötig, eine Verbindung zur Datenbank herzustellen. TopLink bietet die Möglichkeit, mehrere Logins zu verwenden. Hinzugefügt werden diese über die Eigenschaften der Datenbankverbindung. Über das Kontextmenü erfolgen Login/Logout sowie der Import/Export von Tabellen. Die Tabellen werden ebenfalls im Projektbaum angezeigt. Nachdem alle nötigen Tabellen dem Projekt hinzugefügt wurden, können nun die Klassen einer bestimmten Tabelle zugeordnet werden. Dies geschieht im Tab Descriptor Info 5 | Objektrelationale Mapper 91 Abbildung 5.2: TopLink Mapping Workbench der jeweiligen Klasse. Anschließend können alle Attribute auf die entsprechenden Spalten abgebildet werden. Dies erfolgt entweder über das Kontextmenü jedes Attributs oder mit Hilfe des Automap-Tools, welches versucht, anhand gleichlautender Namen automatisch eine Abbildung der Attribute auf entsprechende Datenbankfelder herzustellen. Für die Abbildung von Beziehungen sind Fremdschlüssel sowie Referenzen notwendig. Diese können über den Tab Table References des entsprechenden Attributs ausgewählt werden. Handelt es sich um eine bidirektionale Beziehung, so kann der Partner im General-Tab des Attributs festgelegt werden, dies muß beiderseitig geschehen. Eine Beziehung kann außerdem als private owned gekennzeichnet werden, wodurch Operationen auf das Kindelement kaskadiert werden. Für die Erzeugung von Primärschlüsselwerten nutzt TopLink Sequenzen. Diese können entweder nativ von der Datenbank (z.B. Oracle, Sybase) oder über eine spezielle Sequenztabelle zur Verfügung gestellt werden. Diese Tabelle besteht aus einer Spalte für den Sequenznamen sowie einer Spalte für den Sequenzzähler. Die Angabe der Sequenztabelle erfolgt im Sequencing-Tab des Projekts sowie für jede Klasse im Tab Descriptor Info. Dort muß neben dem Namen der Sequenz die Zieltabelle sowie die Zielspalte angegeben werden. Für jede im Projekt angegebene Sequenz muß ein entsprechender Eintrag in der Sequenztabelle existieren. 92 5.4 Oracle9iAS TopLink Wenn alle Mappings vollständig sind, können die Informationen entweder als Deployment-XML oder als eine von Project abgeleitete Java-Klasse exportiert werden (über das Menü File). Eine Java-Applikation kann dann die Deployment-XML in ein Projekt einlesen oder direkt eine Instanz der exportierten Projektklasse erzeugen, um mit TopLink zu arbeiten. 5.4.3 Session und Unit of Work Nach dem Erzeugen der Deskriptoren und dem Export als Deployment-XML oder eigenständige Java-Klasse müssen diese für eine Session registriert werden. Eine solche Session repräsentiert den Dialog einer Applikation mit der Datenbank und kapselt laut [OTS02] folgende Informationen: • das Projekt, das Datenbank-Login sowie die Konfiguration der Session, • die Deskriptoren der persistenten Klassen, • Identity Maps für das Caching und die Überwachung der persistenten Objekte, • den Database Accessor, der die Low-Level-Kommunikation zwischen der Session und der Datenbank mittels JDBC regelt. Die Applikation nutzt die Session, um nach einem Login die gewünschten Lese- und Schreiboperationen auf der Datenbank auszuführen. Dabei entspricht die Lebenszeit der Session üblicherweise der Lebenszeit der Applikation. TopLink bietet zwei Arten von Sessions an: Die DatabaseSession für Datenbank-Zugriffe aus einfachen Applikationen heraus sowie eine ServerSession für mehrschichtige Applikationen wie beispielsweise Webanwendungen. Während die DatabaseSession den exklusiven Zugriff eines Benutzers auf eine Datenbank ermöglicht, kann die ServerSession von mehreren Benutzern oder Clients gleichzeitig benutzt werden. Dabei wird eine gemeinsam verwendete Nur-Lese-Verbindung zur Verfügung gestellt, Schreiboperationen laufen über eine für einen Client exklusive Unit of Work. Um ein Session-Objekt in der Applikation verfügbar zu machen, werden zunächst die Deployment-Informationen in eine Project-Instanz eingelesen. Wurde das Mapping als Deployment-XML aus dem Mapping Workbench exportiert, so kann diese Datei mit Hilfe des XMLProjectReaders eingelesen werden. Wurde hingegen eine Java-Klasse exportiert, so genügt eine Instantiierung dieser Klasse. Sobald das Projekt zur Verfügung steht, kann von diesem eine Session geholt werden: Project p = XMLProjectReader.read("project.xml"); DatabaseSession session = p.createDatabaseSession(); session.login(); // Lese-/Schreiboperationen auf der Datenbank session.logout(); 5 | Objektrelationale Mapper 93 Der eigentliche Datenbankzugriff geschieht nach einem Login bei der Datenbank über login(), beendet wird die Datenbankverbindung über logout(). Die Session kann nun über executeQuery() Objekte abfragen, mit readObject() Objekte lesen oder mit writeObject() Objekte speichern. Allerdings sollte auf das Schreiben von Objekten über die Session zugunsten der Anwendung einer sogenannten Unit of Work verzichtet werden, welche eine Transaktion auf Objektebene repräsentiert: Sie vereinigt Datenbank-Transaktionen mit Änderungen an Java-Objekten. Normalerweise wird eine Session genutzt, um Instanzen einer angegebenen Klasse aus der Datenbank zu lesen. Instanzen, die geändert werden sollen, werden dann bei einer Unit of Work registriert, welche die Änderungen überwacht und beim Commit in die Datenbank schreibt. Laut [OTS02] ergibt sich dadurch eine maximale Performance für die meisten Applikationen: Die Lese-Performance wird durch die Nutzung der Session optimiert, da diese keine Objekte auf Änderungen hin überwacht bzw. überwachen muß, denn dies obliegt allein einer Unit of Work. Die Schreib-Performance wird optimiert, da die Unit of Work nur die Objekte überwacht, die geändert werden. Außerdem werden nur die Daten in die Datenbank geschrieben, die sich tatsächlich geändert haben. Für das Einlesen von Objekten stehen eine Reihe von Klassen zur Verfügung. So wird ein einzelnes Objekt mit Hilfe eines ReadObjectQuery geladen, eine Collection von Objekten der gleichen Klasse mit Hilfe eines ReadAllQuery. Auch das Einlesen über direkte SQL-Anfragen ist möglich. Bei der Erzeugung eines Queries wird zunächst die Klasse angegeben, deren Instanzen geladen werden sollen. Anschließend besteht die Möglichkeit, über Expressions die Auswahl weiter einzuschränken. Eine Expression ist eine objektorientierte Repräsentation einer SQL-WHERE-Klausel und wird mit Hilfe des ExpressionBuilders erzeugt. Über die Methode get() wird zunächst ein Attribut bestimmt, für das anschließend über verschiedene weitere Methoden Auswahlkriterien festgelegt werden können. Jede dieser verfügbaren Methoden entspricht einem SQLOperator, bildet diesen also objektorientiert ab. ReadAllQuery q = new ReadAllQuery(State.class); Collection col = (Collection)session.executeQuery(q); TopLink ermöglicht das partielle Einlesen von Daten, so daß nur die benötigten Attribute eines Objekts oder einer Menge von Objekten geladen werden. Hierbei werden dem Query-Objekt alle zu ladenden Attribute über addPartialAttribute() angegeben: Long id = new Long(23); ReadObjectQuery q = new ReadObjectQuery(Location.class); ExpressionBuilder builder = new ExpressionBuilder(); q.setSelectionCriteria( builder.get("id").equal(id) ); q.addPartialAttribute("zip"); q.dontMaintainCache(); Location l = (Location)this.session.executeQuery(q); 94 5.4 Oracle9iAS TopLink Um ein neues Objekt zu speichern oder ein zuvor geladenes Objekt zu verändern, muß dieses zunächst bei einer Unit of Work registriert werden. Diese liefert dann einen Klon des Objekts zurück, auf dem die gewünschten Änderungen vorgenommen werden müssen. Der Aufruf von commit() überträgt dann alle Änderungen an dem Klon und seinen als private owned gekennzeichneten Kindobjekten auf das Originalobjekt und speichert die aktualisierten Werte in der Datenbank. Abbildung 5.3 verdeutlicht dieses Prinzip: Abbildung 5.3: Unit of Work (nach [OTT02]) Das folgende Beispiel lädt ein State-Objekt und führt Änderungen auf dem Objekt nach Registrierung bei einer Unit of Work aus: Long id = new Long(42); ReadObjectQuery q = new ReadObjectQuery(State.class); ExpressionBuilder builder = new ExpressionBuilder(); q.setSelectionCriteria( builder.get("id").equal(id) ); State s = (State)session.executeQuery(q); UnitOfWork uow = session.acquireUnitOfWork(); State registeredState = (State)uow.registerObject(s); // auf Klon des registrierten Objekts arbeiten registeredState.setName("Brandenburg"); uow.commit(); Ebenso wie das Ändern von Objekten sollte auch das Löschen per delete() erst nach Registrierung bei einer Unit of Work geschehen; die Methode löscht neben dem Objekt selbst auch alle referenzierten Objekte, die als private owned gekennzeichnet sind. Beim Marktführer TopLink läßt der Mapping Workbench als graphische Benutzeroberfläche für die Erzeugung des Mappings in Bedienungskomfort und Funktionsumfang keinerlei Wünsche offen. Allerdings ist die API für die Verwendung von TopLink in Applikationen aufgrund ihrer Konzepte sehr gewöhnungsbedürftig. 5 | Objektrelationale Mapper 5.5 95 Weitere O/R-Mapper Neben den hier vorgestellten O/R-Mappern existieren noch eine Reihe weiterer O/RMapping-Tools, von denen einige hier kurz vorgestellt werden sollen. Abra Abra ist ein Persistenz-Tool, mit dem sowohl Java-Klassen als auch deren Mappings definiert werden können. Diese Definition geschieht über einfache XML-Dateien, mit deren Hilfe das Datenbank-Schema und die persistenten Klassen erzeugt werden. Das Tool kann auch abstrakte Views erstellen, womit die Menge der zwischen den Schichten ausgetauschten Daten eingeschränkt werden kann. Cayenne Cayenne ist ein als Open Source verfügbares objektrelationales Persistenz-Framework, das die gängigsten Datenbanksysteme unterstützt. Es handelt sich um einen Codegenerator, der aus einer bestehenden Datenbank eine objektorientierte Abstraktion des Schemas erstellen kann. Zu diesem Zweck liefert Cayenne den CayenneModeler als graphische Oberfläche mit, über die das Reverse Engineering von Datenbanken und die Erzeugung von Java-Klassen für die persistenten Objekte möglich ist. Neben Transaktionen auf Objektebene unterstützt Cayenne auch atomare Transaktionen von mehreren Objekten sowie Stored Procedures. CocoBase CocoBase ist ein kommerzieller O/R-Mapper, der für Applikationen auf allen drei JavaPlattformen eingesetzt werden kann. Der Mapper bietet eine graphische Oberfläche (den CocoAdmin) und kann - ähnlich wie Hibernate - sowohl für das Forward als auch für das Reverse Engineering sowie zwischen existierenden Java-Klassen und DB-Schemata eingesetzt werden. Als Codegenerator kann das Tool template-basiert einfache JavaKlassen, Entity Beans, Session Beans und Servlets erstellen und diese anschließend auch in einen Applikationsserver installieren. Dabei können auch serverspezifische Optimierungen eingebunden werden. DataBind DataBind ist ein einfacher O/R-Mapper, der Reflection benutzt, um Java-Objekte persistent zu machen. Dabei ist das Tool nicht auf relationale Datenbanken beschränkt, sondern kann auch andere Arten von Datenquellen (zum Beispiel XML-Dateien) benutzen. Der Zugriff auf eine Datenquelle erfolgt über einen sogenannten Binder. DataBind liefert bereits einen Binder für relationale Datenbanken mit, für beliebige andere Datenquellen können jedoch eigene Binder implementiert werden. 96 5.6 Fazit DBGen DbGen ist ein Codegenerator, der Java-Klassen nach dem DAO-Entwurfsmuster erzeugt. Das Tool liefert eine graphische Benutzeroberfläche mit, in der notwendige Tabellen entweder erzeugt und als DDL-Skript exportiert oder aus einem bestehenden DatenbankSchema importiert werden können. Firestorm/DAO Firestorm/DAO von CodeFutures ist ein GUI-basierter Codegenerator, der ursprünglich DAO-basierte Java-Klassen für ein relationales Datenbank-Schema erzeugt hat. CodeFutures liefert Firestorm/DAO 2.0 in drei Versionen mit unterschiedlichem Funktionsumfang aus: als Standard Edition, die eine DAO-basierte Persistenzschicht erzeugt, als Enterprise Edition, die zusätzlich CMP Entity Beans mitsamt Session Fassade und Deployment-Deskriptor erstellen kann, sowie als Architect Edition, welche die Möglichkeit bietet, eigene Module für die Codegenerierung zu integrieren. Das notwendige Datenbank-Schema kann entweder über JDBC aus einer Datenbank oder aus einem DDL-Skript importiert werden. Die Erzeugung weiterer Tabellen über die graphische Oberfläche von Firestorm/DAO ist ebenfalls möglich. Jaxor Framework Jaxor ist ein einfaches Framework, das XML-Metadaten benutzt, um eine transparente Persistenzschicht zu erzeugen. Alle persistenten Klassen benötigen eine gemeinsame Basisklasse (die Base Entity) und können bei Bedarf einen Listener implementieren, um über Änderungen an Attributen informiert zu werden und beispielsweise ein Update in der Datenbank auszulösen. Außerdem stehen jeder persistenten Klasse die Metadaten zur Laufzeit zur Verfügung. Auch Jaxor arbeitet transaktionsbasiert, ähnlich wie bei TopLink müssen Änderungen bei einer Jaxor Session registriert werden. Productivity Environment For Java (PE:J) PE:J ist eine kommerzielle Entwicklungsumgebung, mit der ein kompletter Entwicklungszyklus durch Integration von UML-Werkzeugen, Rapid Application Development (RAD), JDO und Applikationsservern realisiert werden kann. Das Tool generiert automatisch komplette J2SE- oder J2EE-Applikationen, wobei letztere auch gleich im ZielApplikationsserver installiert werden können. PE:J ermöglicht das Forward Engineering durch UML-Diagramme oder Java-Klassen und das Reverse Engineering von vorhandenen Datenbank-Schemata. 5.6 Fazit Dieses Kapitel hat gezeigt, daß es auf dem Markt bereits eine Vielzahl proprietärer O/RMapper gibt, die dem Entwickler für das objektrelationale Mapping seiner Datenmodell- 5 | Objektrelationale Mapper 97 Klassen zur Verfügung stehen. Sie bieten sich als Alternativen an, wenn keine EJB-Plattform zur Verfügung steht oder ein Projekt nicht mit JDO realisiert werden kann oder soll. Je nach Ausgangssituation eines Projekts sind natürlich unterschiedliche Mapper einsetzbar. So bieten sich Schemageneratoren genau dann an, wenn bereits die Klassen des Datenmodells vorliegen, aber hinsichtlich der Datenbankstruktur noch keine Aussagen getroffen wurden. So kann der Mapper die Erzeugung des Schemas übernehmen. Bei vorliegendem Datenbankschema, aber noch nicht existierenden Klassen können hingegen Codegeneratoren eingesetzt werden, die aus einem importierten Datenbank-Schema eine vollständige, meist DAO-basierte Persistenzschicht erzeugen. Ein weiteres Auswahlkriterium ist die Bedienerfreundlichkeit des Mappers. Das schließt sowohl eine eventuelle graphische Oberfläche als auch den Funktionsumfang und die Bedienbarkeit der API ein. Speziell Schemageneratoren liefern eine eigene API mit, über die die Kommunikation mit der Persistenzschicht abläuft. Hier sind natürlich die Werkzeuge im Vorteil, die eine Standard-API vorweisen können. So wird der Einarbeitungsaufwand verringert und auch die Austauschbarkeit der Persistenzschicht in gewissem Maße gewährleistet. In den meisten Fällen gilt jedoch, daß umfangreiche Änderungen am Quelltext stattfinden müssen, wenn die Persistenzschicht ausgetauscht werden soll. Man legt sich also mit der Wahl eines hier vorgestellten Mappers meist während der gesamten Projektlebenszeit auf diesen fest. Nicht zuletzt spielt natürlich der Anschaffungspreis eines Mappers eine Rolle. Es gibt neben den teilweise sehr teuren kommerziellen Produkten bereits eine Reihe von Alternativen im Open Source-Sektor. Sie reichen zwar noch nicht ganz an den Funktionsumfang von beispielsweise TopLink heran, können aber trotzdem auch in größeren Projekten eingesetzt werden. 6 | Objektorientierte Datenbanken Bei objektorientierten Systemen handelt es sich um Datenbanksysteme mit einem objektorientierten Datenmodell. Ziel solcher Systeme ist die problemlose Speicherung von vollständigen Objekten und somit die Vermeidung des Impedance Mismatch. Die Entwicklung objektorientierter Datenbanken begann Mitte der 1980er Jahre. Erste kommerzielle Systeme gibt es seit 1987 auf dem Markt. Bis heute existieren jedoch nur einige wenige Systeme, die sich zudem noch nicht gegen die etablierten relationalen Datenbanken durchsetzen konnten. Das liegt hauptsächlich daran, daß zu Beginn der Entwicklung noch kein Standard für objektorientierte Datenbanken vorlag. Ein solcher Standard wurde erst 1989 mit dem Database Manifesto [AB+89] von Atkinson veröffentlicht. In der Folge existiert bis heute keine einheitliche Schnittstelle zu objektorientierten Systemen, so wie es von den relationalen Datenbanken her bekannt ist. Im Bereich Java existieren die von der ODMG standardisierte Schnittstelle (siehe Abschnitt 6.4.1) sowie seit kurzem JDO als allgemeine Java-Schnittstelle für den Zugriff auf persistente Objekte (siehe Kapitel 4). Laut Database Manifesto soll eine objektorientierte Datenbank sowohl die Regeln der Objektorientierung als auch die Grundsätze von Datenbanken erfüllen. Im Bereich der Objektorientierung bedeutet dies, daß eine Datenbank komplexe Objekte durch Klassen und Typen darstellen soll. Durch Vererbung und Polymorphismus sollen innerhalb der Datenbank Klassenhierarchien aufgebaut werden können. Auch das Prinzip der Kapselung soll eingehalten werden. Als vollständiges Datenbanksystem muß eine objektorientierte Datenbank zudem Erweiterbarkeit, Persistenz und Synchronisation gewährleisten. Auch Transaktionen müssen unterstützt werden. Das Vorhandensein einer Abfragesprache ist ebenfalls im Manifesto vorgeschrieben. Um Objekte in der Datenbank zu identifizieren, schreibt das Manifesto die Verwendung eines zustands- und speicherortunabhängigen Objektidentifikators (OID) vor. Dieser wird beim Speichern eines Objekts von der Datenbank systemweit eindeutig generiert. Er ist während der gesamten Lebenszeit des Objekts von dessen Zustand unabhängig und zudem unveränderlich. Damit sind Objekte mit gleichen Eigenschaften aber unterschiedlicher Identität möglich, ohne komplexe Schlüssel erzeugen zu müssen. Der Objektidentifikator hat keinerlei Anwendungssemantik und bleibt dem Entwickler verborgen. Als optionale Features kann eine objektorientierte Datenbank zudem Versionierung und Mehrfachvererbung unterstützen. 100 6.1 Schwächen relationaler Systeme Es erheben nicht alle auf dem Markt verfügbaren objektorientierten Systeme den Anspruch, vollständige objektorientierte Datenbanksysteme zu sein. Prinzipiell lassen sich daher drei Entwicklungslinien erkennen: • Erweiterung vorhandener Sprachen Hier werden sprachspezifische Erweiterungen angeboten, um Objekte der jeweiligen Sprachen persistent zu machen. Zu dieser Gruppe gehören die Objektdatenbanken. • evolutionäre Linie Hier werden die etablierten relationalen Datenbanken um objektorientierte Elemente erweitert. Man spricht dann von objektrelationalen Datenbanken. • revolutionäre Linie Hierbei handelt es sich um komplette Neuentwicklungen von Datenbanksystemen, welche die genannten Grundsätze der Objektorientierung und der Datenbanktheorie vereinen. Es handelt sich somit um echte objektorientierte Datenbanksysteme (OODBS). Im Verlauf des Kapitels sollen die drei Entwicklungslinien näher vorgestellt werden. Zunächst sollen jedoch die Schwächen relationaler Systeme genannt werden, welche zur Entwicklung objektorientierter Datenbanken geführt haben. 6.1 Schwächen relationaler Systeme Relationale Datenbanksysteme werden den Anforderungen moderner Non-StandardSoftware nur bedingt gerecht. So arbeiten die meisten derartigen Anwendungen objektorientiert und zudem mit sehr komplexen Objekten. Als Beispiele seien hier die Bereiche Architektur, CAD1 und Medizin genannt. Die größten Nachteile relationaler Systeme sind in diesem Zusammenhang: • Segmentierung Je komplexer die Datenstrukturen und Anwendungsobjekte sind, auf desto mehr Relationen müssen sie in der Datenbank verteilt werden. Zum einen hat dies eine unnatürliche Modellierung zur Folge. Zum anderen steigt auch der Aufwand, derartige Objekte über SQL abzufragen. Dies ist meistens nur über aufwendige Verbunde (Joins) möglich. • künstliche Schlüsselattribute Zur Identifikation von Tupeln in einer Tabelle werden eindeutige Schlüssel vergeben. Da hierfür Attribute der Tabelle herangezogen werden, sind die Schlüssel wertebezogen (identity through content). Der Nachteil ist jedoch, daß Tupel 1 CAD: Computer Aided Design 6 | Objektorientierte Datenbanken 101 mit gleichen Schlüsselwerten, aber unterschiedlicher Identität auftauchen können. Dann muß der Schlüssel über weitere Attribute ausgedehnt werden, um die Eindeutigkeit zu gewährleisten. Alternativ können künstliche Schlüsselattribute eingeführt werden. Diese haben jedoch keinerlei Anwendungssemantik. Zudem darf der Wert des Schlüssel während der Lebenszeit eines Tupels nicht verändert werden, da sonst alle Referenzen auf dieses Tupel ungültig werden (Integritätsverletzungen). • fehlendes Verhalten Relationale Datenbanken sind nicht dazu in der Lage, das anwendungsspezifische Verhalten von Objekten zu speichern. • keine Vererbung Auch das Speichern von Vererbungshierarchien in relationalen Datenbanken ist nicht möglich. • externe Programmierschnittstelle Für die Manipulation von Objekten ist eine Programmierschnittstelle nötig, mit der die Abfragesprache der Datenbank in eine Programmiersprache eingebettet werden kann (Impedance Mismatch, siehe Abschnitt 2.2). 6.2 Objektrelationale Datenbanken Seit Mitte der 1990er Jahre bemühen sich die führenden Hersteller relationaler DBMS2 , objektorientierte Konzepte in relationale Datenbanken zu integrieren. Ergebnis sind die objektrelationalen Datenbanken. Sie erweitern relationale Datenbanken um objektorientierte Konzepte wie Objektidentität, benutzerdefinierte Datentypen und Typhierarchien. Durch die Erweiterungen ist es möglich, benutzerdefinierte Objekttypen mit eigenen Attributen und Methoden zu erzeugen. Zudem können komplexe Objekte durch spezielle Typkonstruktoren erzeugt werden. Dazu gehören Tupel, Mengen, Multimengen, Listen und Arrays. Unterstützt wird auch das Konzept der Vererbung. Es läßt sich sowohl auf Typen als auch auf Tabellen anwenden. Die Wertebereiche von Attributen wurden angepaßt. Es sind nun neben elementaren Attributen auch objektwertige Attribute möglich. Beziehungen zu objektwertigen Attributen werden über Referenzen auf den Objektidentifikator hergestellt. Dieser identifiziert ein Objekt in einer Objekttabelle. Als neue Datentypen wurden unter anderem Large Objects (LOBs) eingeführt. Mit ihnen können umfangreiche Binärdaten (BLOBs) oder Textdaten (CLOBs) in der Datenbank gespeichert werden. Beispiele für objektrelationale Datenbanken sind IBM DB2, Oracle9i und Intersystems Caché. 2 DBMS: Datenbankmanagementsystem 102 6.2.1 6.2 Objektrelationale Datenbanken SQL99 Die Anfragesprache SQL wurde in der SQL99-Norm um neue Schlüsselwörter und Features erweitert, um die objektorientierten Erweiterungen auch nutzen zu können. So kann über das Schlüsselwort CREATE TYPE ein neuer benutzerdefinierter Datentyp erzeugt werden. CREATE TYPE name zip state } Location AS { VARCHAR(64), INTEGER, REF(State) Die Erzeugung von Methoden ist ebenfalls möglich. Die Implementierung erfolgt meist in der dem Datenbanksystem spezifischen Sprache, die auch für die Erzeugung von Stored Procedures oder Triggern verwendet wird. Derart erzeugte Datentypen können sowohl als objektwertiges Attribut als auch als Tabellentyp für Objekttabellen verwendet werden. CREATE TABLE locations OF Location (REF IS oid SYSTEM GENERATED) Der Objektidentifikator erhält im obigen Beispiel den Namen oid und wird vom System beim Einfügen eines Objekts in die Tabelle generiert. Jeder benutzerdefinierte Datentyp erhält automatisch einen Default-Konstruktor. Mit Hilfe dieses Konstruktors können dann neue Objekte erzeugt und in eine Objekttabelle eingefügt werden. INSERT INTO locations VALUES ( ’Falkensee’, 14612, State(’Brandenburg’) ) Auch Pfadausdrücke sind in SQL99 möglich. Damit entfallen komplizierte Joins über mehrere Tabellen. Die Verknüpfung findet stattdessen über Referenzen statt. Unter der Annahme, daß der Datentyp State aus obigem Beispiel ein Attribut name hat, ist folgende Anfrage möglich: SELECT * FROM locations l WHERE l.state.name=’Brandenburg’ Sonstige Erweiterungen der SELECT-Klausel umfassen Typkonvertierungen, Methodenaufrufe und Konvertierungsfunktionen. Die FROM-Klausel wurde unter anderem um die Möglichkeit der Variablendeklaration erweitert, außerdem sind innere Tabellen als Wertebereiche möglich. In der WHERE-Klausel können ebenfalls Methoden aufgerufen sowie Typkonstruktoren angewendet werden. Auch der Einsatz von Konvertierungsfunktionen ist möglich. Für tiefergehende Informationen sei auf entsprechende Literatur sowie den Standard [ISO99] verwiesen. 6 | Objektorientierte Datenbanken 6.2.2 103 Beispiel: Intersystems Caché Bei Intersystems Caché handelt es sich laut Hersteller um ein „postrelationales“ Datenbanksystem. Es wurde speziell für das Rapid Development von Datenbank- und WebApplikationen optimiert. Caché ist für die Plattformen Windows, Linux und Unix verfügbar und kann sowohl im Embedded-Bereich als auch als Standalone-Server oder verteiltes Datenbanksystem eingesetzt werden. Durch die Integration von Caché Server Pages (CSP) ist auch der Einsatz als vollständiger Applikationsserver möglich. Um die Interoperabilität zu gewährleisten, ist der Zugriff auf eine Caché-Datenbank unter anderem über Java (Caché-API, JDBC, EJB), C++, ODBC, ActiveX, .NET sowie CORBA möglich. Zudem unterstützt Caché XML und Web Services. Der Hersteller liefert zum eigentlichen Datenbanksystem eine Reihe von Werkzeugen für die Entwicklung und Administration mit. Die primäre Entwicklungsumgebung ist das Caché Studio. Mit dieser IDE können CachéKlassen und CSPs erstellt und kompiliert werden. Außerdem stehen ein Class Inspector und ein Debugger zur Verfügung. Die Erzeugung der Klassen kann entweder über den integrierten Texteditor geschehen oder vollständig interaktiv über Dialoge und Assistenten. Als weitere Werkzeuge stehen eine Kommandozeilen-Anwendung für den direkten Zugriff auf die Datenbank-Engine, ein SQL-Manager sowie ein Explorer für die Anzeige der in der Datenbank enthaltenen Daten zur Verfügung. Die Administration eines CachéSystems kann über ein Kontrollmenü und den Konfigurationsmanager erledigt werden. Caché-Architektur Abbildung 6.1 zeigt die Architektur eines Caché-Systems mit den unterschiedlichen Zugriffsmöglichkeiten. Kern des Systems ist die Datenbank-Engine, welche Dienste wie Persistenz, Journaling (Überwachung), Backup und Recovery anbietet. Die Daten werden in mehrdimensionalen Arrays gespeichert. Die darüberliegende Unified Data Architecture von Caché ermöglicht den gleichzeitigen objektorientierten und relationalen Zugriff auf die Daten. Zwar werden alle Objekte in Form von Klassen gespeichert, jedoch wird bei Erzeugung einer neuen Klasse automatisch eine relationale Tabelle für den Zugriff über SQL erzeugt. Der umgekehrte Fall funktioniert analog: Wird eine Tabelle über SQL erzeugt, so wird gleichzeitig eine Caché-Klasse erstellt. Es findet also eine automatische Synchronisation zwischen objektorientierter und relationaler Darstellung statt. Das Objektmodell von Caché integriert neben Klassen, Attributen, Methoden auch Indizes sowie Integritätsbedingungen (Constraints). Bei Beziehungen wird die referentielle Integrität sichergestellt. Der objektorientierte Zugriff geschieht über Caché Objects. Dabei handelt es sich um eine Reihe von Servern für die entsprechend angebotenen Zugriffsmöglichkeiten. Parallel dazu bietet Caché den relationalen Zugriff über JDBC und ODBC an. Die Anfragesprache SQL wurde gegenüber SQL92 um LOBs, Stored Procedures, abstrakte Datentypen 104 6.2 Objektrelationale Datenbanken Abbildung 6.1: Caché-Architektur sowie den Referenzoperator -> erweitert. Caché und Java Für den Java-Zugriff auf eine Caché-Datenbank stehen drei unterschiedliche Möglichkeiten zur Verfügung: • Caché Java Binding Das Caché Java Binding bezeichnet die Caché-spezifische Java-API für den Datenbank-Zugriff aus einer Java-Applikation heraus. Dazu gehört auch der Java Class Compiler als Erweiterung des Caché Class Compilers. Er erzeugt für jede CachéKlasse eine Java-Klasse, die als Proxy fungiert. Diese Proxy-Klassen kommunizieren zur Laufzeit der Applikation mit den korrespondierenden Caché-Klassen auf dem Caché-Server. • Caché EJB Binding Ähnlich wie das Caché Java Binding umfaßt das EJB Binding eine Erweiterung des Class Compilers. In diesem Falle wird jedoch für eine Caché-Klasse eine passende Entity Bean mit Bean Managed Persistence und einem zugehörigen DeploymentDeskriptor erzeugt. • Caché JDBC-Treiber Der JDBC-Treiber ermöglicht einer Anwendung den Zugriff auf eine Caché-Datenbank mit Hilfe der standardisierten JDBC-API. Der Treiber unterstützt die JDBCVersion 2.0. 6 | Objektorientierte Datenbanken 105 Applikationen können gleichzeitig über das Java Binding und den JDBC-Treiber auf eine Datenbank zugreifen. Alle notwendigen Klassen für die drei Zugriffsarten liegen im Package com.intersys, welches sich in einem einzubindenden Java-Archiv im devVerzeichnis der Caché-Installation befindet. Nachfolgend soll die Benutzung des Java Bindings beschrieben werden. Um über einen Java-Client auf eine Caché-Datenbank zuzugreifen, müssen zunächst alle notwendigen Caché- und Java-Klassen erzeugt werden. Die Caché-Klassen können am einfachsten mit Hilfe des Caché Studios und den dortigen Assistenten erzeugt werden. Persistente Klassen werden vom Obertyp %Persistent3 abgeleitet. Diese Klassen haben dann die Fähigkeit, ihren Zustand eigenständig mit dem in der Datenbank abzugleichen. Attribute werden mit dem Schlüsselwort Property eingeleitet, gefolgt vom Namen und dem Datentyp. Beziehungen beginnen mit dem Schlüsselwort Relationship. Wird der Assistent für die Erzeugung einer Beziehung benutzt, so wird gleichzeitig ein referenzierendes Attribut in der entsprechenden Klasse erzeugt. Eine Methode wird mit dem Schlüsselwort Method eingeleitet, gefolgt von einer Parameterliste und dem Rückgabetyp. Optional kann die Programmiersprache angegeben werden, in der die Implementierung der Methode vorliegt. Möglich sind hier Caché-ObjectScript und -Basic oder Java. Verschiedene Methoden einer Klasse können mit unterschiedlichen Sprachen implementiert werden. Sowohl für Klassen als auch für Attribute können die Namen der entsprechenden relationalen Tabelle bzw. Tabellenspalten angegeben werden. Das folgende Beispiel zeigt die Klassen-Definition für die Klassen State und Location, die ein Attribut name besitzen und in einer 1:n-Beziehung zueinander stehen. Class User.State Extends %Persistent [ClassType=persistent, ProcedureBlock, SqlTableName=states] { Property name As %String [ SqlFieldName = name ]; Relationship locations As User.Location [ Cardinality = many, Inverse = state ]; Projection java As %Projection.Java; } Class User.Location Extends %Persistent [ClassType=persistent, ProcedureBlock, SqlTableName=locs] { Property name As %String [ SqlFieldName = name ]; Relationship state As User.State [ Cardinality = one, Inverse = locations ]; Projection java As %Projection.Java; } 3 Alle Caché-spezifischen Klassen beginnen mit %. 106 6.2 Objektrelationale Datenbanken Da auf diese Klassen von einem Java-Client aus zugegriffen werden soll, muß eine JavaProjektion angegeben werden. Dies geschieht über das Schlüsselwort Projection, gefolgt von einem Namen und dem Typ Projection.Java. Solche Projektionen sind für Java, EJBs, und C++ möglich. Durch die Angabe einer Projektion wird der Class Compiler von Caché dazu veranlaßt, bei der Kompilierung der Caché-Klasse eine entsprechende Java- oder C++-Klasse bzw. eine Entity Bean zu erzeugen. Eine Projektion beschreibt die Transformation einer Caché-Klasse in eine programmiersprachenspezifische Klasse. Im Falle der Java-Projektion werden die in der Caché-Klasse angegebenen Namen für Klassen, Attribute und Methoden unverändert übernommen. Die Datentypen werden geeignet ersetzt. Für jedes Attribut wird ein get-/set-Methodenpaar erzeugt. Für Beziehungen wird ein zusätzliches get-/set-Methodenpaar eingefügt, welches den Zugriff auf die ID des referenzierten Objekts ermöglicht. Neben den in der Caché-Klasse definierten Methoden kommen durch die Ableitung von der Klasse Persistent weitere Methoden hinzu. Dazu gehört unter anderem die statische Methode _open(), die ein Objekt aus der Datenbank lädt. Sie erwartet als Parameter eine Datenbank-Verbindung sowie die ID des zu ladenden Objekts. Die Methode _save() speichert das Objekt in der Datenbank. Mit der Methode delete() kann ein Objekt aus der Datenbank gelöscht werden. Um auf eine Caché-Datenbank zuzugreifen, muß zunächst eine Verbindung hergestellt werden. Dies geschieht auch beim Java Binding über eine URL gemäß der JDBC-Spezifikation. Für die Verwaltung von Verbindungen steht die Klasse CacheDatabase zur Verfügung. Über die statische Methode getDatabase() kann unter Angabe der URL sowie eines Benutzernamens und Paßworts eine Verbindung geholt werden. Das folgende Beispiel erstellt eine Verbindung, erzeugt ein Objekt und lädt ein anderes Objekt. String url = "jdbc:Cache://localhost:1972/SAMPLES"; String user = "_SYSTEM"; String passwd = "sys"; Database db = CacheDatabase.getDatabase(url, user, passwd); State s = new State(db); s.setname("Brandenburg"); s._save(); State s2 = (State)State._open(db, new Id(1)); System.out.println(s2.getname()); db.closeObject( s.getOref() ); db.closeObject( s2.getOref() ); db.close(); 6 | Objektorientierte Datenbanken 107 Wird ein Objekt aus der Datenbank geladen, dann holt es sich alle Attributwerte aus der Caché-Datenbank und macht sie dem Java-Client verfügbar. Dies schließt jedoch referenzierte Objekte aus. Diese werden erst beim Zugriff durch den Java-Client nachgeladen. Dieses Prinzip des Lazy Loadings heißt bei Caché „Swizzling“. Wird eine in der Klassen-Definition implementierte Methode aufgerufen, so synchronisiert das Objekt zunächst seinen Zustand mit der Datenbank und führt anschließend die Methode aus. Alle Objekte, mit denen gearbeitet wurde, sollten mit der Methode closeObject() aus der Klasse Database explizit geschlossen werden. Damit wird deren Integrität in der Datenbank gewährleistet. Allerdings werden alle Referenzen auf ein solches Objekt im Java-Client ungültig. 6.3 Objektdatenbanken Bei Objektdatenbanken handelt es sich um Datenbanksysteme, die stark in eine bestehende objektorientierte Sprache eingebunden sind. Sie wurden mit dem Ziel entwickelt, Objekte in den persistenten Speicher zu schreiben und sie in Bezug auf Zugriff und Aktualisierung effizient zu verwalten. Solche Systeme stellen also nichts weiter als ein Verwaltungssystem für Objekte dar, die von einer bestimmten objektorientierten Sprache wie Java, C++ oder Smalltalk erzeugt wurden. Zudem sind diese Systeme meist auf den Einzelnutzerbetrieb ausgerichtet. Beispiele für Objektdatenbanken sind ObjectStore von der ObjectStore Division, Ozone, ObjectDB sowie db4o. 6.3.1 Beispiel: db4o Bei db4o handelt es sich um eine native Objektdatenbank, die für die Plattformen Java und .NET verfügbar ist. Sie zeichnet sich durch eine sehr einfache Bedienung und eine hohe Geschwindigkeit aus. Das vollständige System ist nur gut 200 KB groß und kann über das Internet bezogen werden. Es steht eine laufzeitbegrenzte Testversion zur Verfügung. Darüber hinaus kostet die Mitgliedschaft bei db4o 100 US-Dollar pro Jahr, allerdings hat man dann Zugriff auf uneingeschränkte Versionen, kostenlose Updates und kann db4o zudem in Produktivumgebungen einsetzen. Features Die Objektdatenbank ist als In-Process-Datenbank und als Client/Server-Datenbank einsetzbar. Die persistenten Objekte können in beliebig benannten Dateien gespeichert werden, üblicherweise begrenzt man sich pro Datenbank auf genau eine Datenbank-Datei. Zusätzliche Meta-Informationen in Form von Properties- oder XML-Dateien sind nicht erforderlich. Die Daten liegen in proprietärem Format vor. Es ist zudem möglich, die Datenbank-Datei zu verschlüsseln und mit einem Paßwort zu schützen. 108 6.3 Objektdatenbanken In der Datenbank können beliebige Objekte gespeichert werden. Es werden alle Sprachkonstrukte der jeweiligen Plattform unterstützt. Das Klassenschema wird durch Reflection ermittelt. Sogar spätere Änderungen am Schema können erkannt und übernommen werden (neue Attribute, umbenannte Klassen). Anfragen werden über Query by Example oder das S.O.D.A.-Query-Interface an die Datenbank abgesetzt. Query by Example bedeutet, daß einfach ein Objekt der abzufragenden Klasse als Muster erzeugt wird. Darin werden alle Attribute gesetzt, nach deren Werten die Auswahl erfolgen soll. Alle in der Datenbank enthaltenen Objekte, die diesem Muster entsprechen, werden dann bei der Abfrage zurückgeliefert. S.O.D.A. steht für Simple Object Database Access und stellt eine API dar, um mit Objektdatenbanken zu kommunizieren. Die API ist Open Source und in der aktuellen Version nur für Abfragen, nicht jedoch für Updates geeignet. Alle abzufragenden Objekte werden in einen Graph eingefügt. Die Angabe von Bedingungen erfolgt in vollständig objektorientierter Art und Weise. Für den Einsatz in Webanwendungen liefert db4o eine Servlet-API mit. Dabei handelt es sich um eine Reihe von Servlets, die der Distribution als Quelltext beiliegen. Die API sorgt dafür, daß den Clients der Zugang zu einer Datenbank gewährt wird. Außerdem kann die Art des Zugriffs festgelegt werden: Entweder über eine gemeinsame Transaktion für alle Clients (shared transaction) oder pro Client eine Transaktion (session transaction). Die Klasse Db4oServlet bietet alle nötigen Methoden an, um db4o in einer Servlet-Umgebung einsetzen zu können. Verwendung von db4o Eine Datenbank wird über die Factory-Klasse Db4o geöffnet. Da es sich nur um eine Datei handelt, genügt die Angabe des Pfades zu dieser Datei als Parameter für die Methode openFile(). Sollte die Datenbank nicht vorhanden sein, so wird sie angelegt. ObjectContainer db = Db4o.openFile("locations.db4o"); Alle Zugriffe auf die Datenbank zur Manipulation und Abfrage von Objekten laufen über den Objekt-Container. Bei Programmende muß der Container mit der Methode close() wieder geschlossen werden. Mit der Methode set() können Objekte in der Datenbank gespeichert oder aktualisiert werden. Um ein Objekt aus der Datenbank zu löschen, muß dieses zunächst geladen werden. Anschließend kann es mit der Methode delete() gelöscht werden. Für Anfragen über Query by Example steht die Methode get() zur Verfügung. Ihr wird als Parameter das Muster-Objekt übergeben. Der Rückgabewert ist immer ein ObjectSet, das dann ähnlich wie ein Iterator durchlaufen werden kann. Location l = new Location(); 6 | Objektorientierte Datenbanken 109 l.setName("Berlin"); ObjectSet result = db.get(l); while ( result.hasNext() ) db.delete( result.next() ); Für komplexe Anfragen steht die S.O.D.A.-API zur Verfügung. Zunächst muß jedoch ein Query-Objekt vom Objekt-Container geholt werden. Anschließend können Bedingungen in Form von Constraints definiert werden. Das folgende Beispiel soll alle Instanzen der Klasse Location ermitteln, deren Name dem übergebenen Muster entspricht: Query query = this.db.query(); query.constrain(Location.class); Query qName = query.descend("name"); Constraint cName = qName.constrain( pattern ); cName.contains(); ObjectSet res = query.execute(); Nachdem das Query-Objekt geholt wurde, wird die Ergebnismenge auf Instanzen der Klasse Location begrenzt. Mit der Methode descend() kann dann zu dem Attribut navigiert werden, das abgefragt werden soll. An dieser Stelle sind auch komplexe Pfadausdrücke wie zum Beispiel query.descend("state").descend("name") möglich. Das Muster, auf das verglichen werden soll, wird als Constraint definiert. Die Methode contains() legt anschließend die Art des Vergleichs fest. Hier stehen entsprechende logische Operationen wie and(), or() sowie Vergleichsoperationen wie greater(), equal() usw. zur Verfügung. Auch der Vergleich von Zeichenketten über like() ist vorgesehen, allerdings funktionert diese Methode nicht. Daher wurde im obigen Beispiel contains() benutzt, was prinzipiell das gleiche Ergebnis liefern sollte. Zuletzt wird die Anfrage ausgeführt, das Ergebnis ist auch hier ein ObjectSet mit allen gefundenen Objekten. Zusätzliche Konfiguration von db4o Für komplexere Datenmodelle und Performance-Optimierungen ermöglicht db4o die Konfiguration verschiedener Einstellungen. Für diesen Zweck steht ein Configuration-Objekt zur Verfügung, das von der Factory-Klasse Db4o geholt werden kann. Beim Laden von Objekten werden alle Referenzen bis zu einer Tiefe von fünf aktiviert. Das heißt, es ist anschließend eine Navigation durch den Objektbaum bis zu dieser Tiefe möglich. Dieses Verhalten kann über die Methode activationDepth() beeinflußt werden. Kaskadierende Operationen für referenzierte Objekte müssen explizit festgelegt werden. In der Standardeinstellung werden nur die einfachen Member eines Objekts gespeichert oder aktualisiert, nicht jedoch referenzierte Objekte. 110 6.4 Objektorientierte Datenbanksysteme objectClass( className ).cascadeOnDelete(); objectClass( className ).cascadeOnUpdate(); Auch die Tiefe, bis zu der Aktualisierungen ausgeführt werden sollen, kann konfiguriert werden: objectClass( className ).updateDepth(int depth); 6.4 Objektorientierte Datenbanksysteme Bei den objektorientierten Datenbanksystemen handelt es sich um vollständige Datenbanksysteme, die ein objektorientiertes Datenmodell integrieren. Sie unterstützen das objektorientierte Paradigma, womit eine bessere Modellierung des Schemas möglich ist. In einem objektorientierten Datenbanksystem werden nicht nur Daten gespeichert, sondern auch die Verhaltens- und Strukturinformationen eines Objekts. Das heißt, das anwendungsspezifische Verhalten wird Bestandteil der Datenbank und umständliche Transformationen zwischen Datenbank und eingesetzter Programmiersprache entfallen (O/R-Mapping). Damit fällt auch der Impedance Mismatch weg und der Zugriff auf komplette Objekte ist schneller. Zudem sind die einem Objekt zugeordneten Methoden direkt in der Datenbank ausführbar. Für den standardisierten Zugriff auf OODBS steht bisher nur ODMG 3 zur Verfügung. Da dieser Standard jedoch erst im Jahr 2000 verabschiedet wurde, bietet jede objektorientierte Datenbank eine eigene API an. Erst nach Verabschiedung des aktuellen ODMGStandards haben einige Hersteller eine entsprechende API integriert, in den meisten Fällen allerdings noch nicht vollständig. Speziell für Java wird dieser Standard jedoch in absehbarer Zeit von JDO abgelöst. Zu den objektorientierten Datenbanksystemen gehören O2, Objectivity und Orient. 6.4.1 Der ODMG-Standard Die Object Data Management Group (ODMG) hat sich 1991 aus mehreren Firmen des Datenbanksektors zusammengeschlossen. Mitglieder waren unter anderem GemStone Systems, Poet und Versant. Ziel der ODMG war es, eine Standardisierung von Objektmodell und deskriptiver Definitions- und Anfragesprache für objektorientierte Datenbanksysteme zu erreichen. Die Version 1.0 des Standards wurde bereits im Jahr 1993 verabschiedet, vier Jahre später folgte Version 2.0. Die letzte und aktuelle Version 3.0 ist seit dem Jahr 2000 Standard. Dieser Standard umfaßt ein Objektmodell, die Definitionssprache ODL, die Anfragesprache OQL sowie eine festgelegte Sprachanbindung für objektorientierte Programmiersprachen. Kurz nach der Verabschiedung der dritten Version des Standards hat sich die ODMG aufgelöst. Die Ergebnisse wurden zudem dem Java Community Process zur Verfügung gestellt, um die Entwicklung von JDO zu ermöglichen. 6 | Objektorientierte Datenbanken 111 Die ODMG-Anwendungsarchitektur (Abb. 6.2) sieht vor, die Klassen des Datenmodells in der standardisierten Definitionssprache zu erstellen. Diese werden dann durch einen Präprozessor zum einen in der Datenbank abgelegt und zum anderen als Quellcode für die gewünschte Programmiersprache erzeugt (Java, C++, Smalltalk). Die Applikation selbst arbeitet mit Objekten dieser Klasse und kann sie durch die Verwendung der ODMG-API in der Datenbank persistent machen. Durch diese Architektur sind die in der Datenbank gespeicherten Objekte unabhängig von der verwendeten Programmiersprache und können so von verschiedenen Clients verwendet werden. & ' ! "#$ % Abbildung 6.2: Die ODMG-Anwendungsarchitektur Object Definition Language Die Typen des Datenbankschemas werden in der Object Definition Language (ODL) beschrieben. Die Syntax basiert auf der CORBA-IDL. ODL ermöglicht die Definition von Klassen (class) und Interfaces (interface). Bei Vererbung wird ähnlich wie in Java das Schlüsselwort extends gefolgt vom Namen der Basisklasse angegeben. Für jeden Typ können Attribute, Beziehungen und Operationen festgelegt werden. Attribute werden mit dem Schlüsselwort attribute eingeleitet, gefolgt vom Datentyp und Namen des Attributs. attribute type attname; Beziehungen zu anderen Typen werden mit dem Schlüsselwort relationship eingeleitet. Danach folgen der Datentyp sowie der Name der Beziehung. ODMG unterstützt Beziehungen der Kardinalitäten 1:1, 1:n und n:m. Auch mehrstellige Beziehungen sind möglich, benötigen aber einen eigenen Objekttyp. Um die Konsistenz einer Beziehung zu sichern, muß zusätzlich das Schlüsselwort inverse angegeben werden. Es folgen der Name des Typs sowie die darin enthaltene zugehörige Beziehung. Damit wird auch festgelegt, daß es sich um eine bidirektionale Beziehung handelt. 112 6.4 Objektorientierte Datenbanksysteme relationship type relname [inverse type::relname]; Handelt es sich um eine 1:n- oder n:m-Beziehung, so muß zusätzlich zum Datentyp das Schlüsselwort set angegeben werden. relationship set<type> relname [inverse type::relname]; Die Signatur einer Methode enthält neben dem Methodennamen einen Rückgabewert sowie eine Liste von Übergabeparametern. Die Syntax entspricht im wesentlichen der Java-Syntax, lediglich Übergabeparameter werden mit dem Schlüsselwort in eingeleitet. rettype methodname(in type paramname [, ...]); Das folgende Beispiel definiert die Klassen State und Location. Beide enthalten sowohl Attribute als auch Methoden. Außerdem stehen beide Klassen in einer bidirektionalen Beziehung zueinander. class State { attribute string name; relationship set<Location> locs inverse Location::state; void addLocation(in Location loc); boolean removeLocation(in Location loc); }; class Location { attribute string name; attribute short zip; relationship State state inverse State::locs; }; Object Query Language Die Object Query Language (OQL) dient als Anfragesprache für objektorientierte Datenbanksysteme. Sie ist an SQL angelehnt, verfügt allerdings über zusätzliche Features wie die Operation auf Collections, den Aufruf von Methoden sowie die Nutzung von Pfadausdrücken. Ein UPDATE-Befehl existiert in OQL jedoch nicht, da Änderungen an Objekten im Sinne der Objektorientierung über entsprechende Methoden der Objekte stattfinden. Geschachtelte Anfragen sind mit OQL ebenfalls möglich. Die SELECT-Klausel gibt an, welche Daten ermittelt werden sollen. In der FROM-Klausel werden die Datentypen angegeben, außerdem ein Name für die Objekte, über den die Pfadausdrücke realisiert werden. In der WHERE-Klausel werden schließlich die Auswahlbedingungen angegeben. 6 | Objektorientierte Datenbanken 113 select l from l in Location where l.name = "Falkensee" Eine OQL-Anfrage ist vollständig objektorientiert, das heißt auch der Rückgabewert ist immer ein Objekt oder eine Menge von Objekten. Eine Strukturierung von Rückgabewerten ist mit Hilfe des Schlüsselwortes struct möglich. Die folgende Anfrage soll die Attribute name und capital aller State-Objekte in der Datenbank zurückgeben: select struct(n: s.name, c: s.capital) from s in State Sprachanbindung Die Sprachanbindung findet über eine Bibliothek statt. Bei Java liegt diese in Form eines Java-Archivs vor, das vom Datenbankanbieter zur Verfügung gestellt wird. Darin ist eine anbieterspezifische Implementierung aller ODMG-Interfaces enthalten. Ausgangspunkt bei ODMG ist das Interface org.odmg.Implementation. Dieses Interface stellt Factory-Methoden bereit, um Datenbank-, Transaktions- und Query-Objekte zu holen. Zunächst müssen ein Implementation- und von dort ein DatabaseObjekt geholt werden. Anschließend kann die Datenbank geöffnet werden. Neben dem Namen der Datenbank muß der Modus angegeben werden, in dem sie geöffnet werden soll. Gültige Werte sind OPEN_READ_WRITE, OPEN_READ_ONLY und OPEN_EXCLUSIVE. // Implementation holen Implementation impl = ... Database db = impl.newDatabase(); db.open("dbname", Database.OPEN_READ_WRITE); Transaction tx = impl.newTransaction(); tx.begin(); // [...] tx.commit(); db.close(); ODMG verlangt die Ausführung aller Operationen innerhalb eines Transaktionskontextes. Hierfür steht das Interface org.odmg.Transaction zur Verfügung. Gestartet wird eine Transaktion mit begin(), beendet mit commit() oder abort(). Alle Änderungen an geladenen Objekten werden mit dem Commit automatisch in die Datenbank übernommen. Um ein neues Objekt zu erzeugen, genügen folgende Zeilen innerhalb einer laufenden Transaktion: State s = new State("SN", "Sachsen", "Dresden"); tx.lock(s, Transaction.WRITE); 114 6.4 Objektorientierte Datenbanksysteme Mit lock() erhält die Transaktion das zu sperrende Objekt sowie den Sperrmodus. Damit kann die laufende Transaktion alle notwendigen Schritte unternehmen, um Änderungen in die Datenbank zu schreiben. Mögliche Werte für den Sperrmodus sind WRITE, READ und UPGRADE. Alternativ kann Database#makePersistent() auf das zu speichernde Objekt angewendet werden. OQL-Anfragen werden über ein OQLQuery-Objekt abgesetzt, das nach Beginn einer Transaktion geholt wird. Das folgende Beispiel holt alle Instanzen der Klasse State aus der Datenbank: OQLQuery q = impl.newOQLQuery(); q.create("select s from s in State"); DList list = (DList)q.execute(); Die Bindung von Parametern erfolgt über die Methode OQLQuery#bind(). Im Anfragestring werden diese durch Platzhalter der Form $n gekennzeichnet, die Zählung beginnt bei 1. Um ein Objekt zu ändern, wird dieses zunächst wie gezeigt geladen und mit lock() gesperrt. Anschließend kann das Objekt mit den zur Verfügung stehenden Methoden bearbeitet werden. Sobald commit() aufgerufen wird, werden alle Änderungen in die Datenbank geschrieben. Das Löschen eines zuvor geladenen Objektes geschieht einfach durch den Aufruf der Methode db.deletePersistent(anObject). 6.4.2 Beispiel: Objectivity Objectivity/DB von Objectivity, Inc. ist ein vollständig objektorientiertes Datenbanksystem. Es kann als Standalone-Server oder innerhalb von Echtzeitanwendungen im Embedded-Bereich eingesetzt werden. Das System zeichnet sich durch hohe Performance, Skalierbarkeit und Interoperabilität aus. Auch Schema-Evolution mit zugehöriger Objekt-Konvertierung ist mit Objectivity möglich. Die aktuelle Version 8.0 wird mit einer Reihe von administrativen Werkzeugen für die Schema-Erzeugung, das Backup und die Replikation ausgeliefert. Ebenfalls enthalten ist ein Eclipse-Plugin. Dieses Plugin ermöglicht die Erzeugung von Klassen in der Datenbank und den Export als Java- oder C++-Quellcode. Die Plattformunabhängigkeit von Objectivity bezieht sich sowohl auf das für den oder die Server verwendete Betriebssystem, als auch auf verwendete Programmiersprachen. So ist es möglich, einen Objectivity-Datenbankserver auf verschiedene heterogene Rechnerarchitekturen zu verteilen. Der Server sorgt dafür, daß einer Anwendung immer eine logische Sicht des Servers zur Verfügung steht. Diese Sicht wird über eine Federated Database realisiert, in der verschiedene Datenbanken und Objekt-Container existieren können. So können logisch zusammengehörige Objekte in einem gemeinsamen Container gespeichert werden. Das Datenbanksystem speichert diese Objekte auch physikalisch zusammen, was zu einem verringerten I/O- und Kommunikationsaufwand 6 | Objektorientierte Datenbanken 115 führt. Die in der Datenbank gespeicherten Objekte können in Java-, C++- oder SmalltalkApplikationen verwendet werden. Auch der Zugriff über ODBC und SQL99 ist möglich. Objectivity unterstützt CMP 2.0, speziell den Applikationsserver BEA Weblogic. Auch eine Schnittstelle zur JTA wird angeboten, womit die Datenbank als Ressourcen-Manager innerhalb von J2EE-Applikationsservern verwendet werden kann (siehe Anhang A.3). Objectivity/DB for Java Die Java-Schnittstelle von Objectivity trägt den Namen Objectivity/DB for Java und unterstützt alle Sprachkonstrukte und Typen von Java 2. Neue Klassen können entweder mit dem genannten Eclipse-Plugin direkt in der Datenbank erzeugt und exportiert werden oder manuell erstellt werden. Wird erstmalig ein Objekt einer Klasse in der Datenbank gespeichert, so wird die Klasse implizit in das Schema der Datenbank aufgenommen. Alternativ kann auch die explizite Aufnahme über API-Befehle erfolgen. Persistente Klassen müssen von der Objectivity-Basisklasse ooObj abgeleitet werden. Diese stellt grundlegende Funktionalitäten zum Speichern, Suchen und Laden von Objekten zur Verfügung. public class Location extends ooObj { private String name; public String getName() { fetch(); return name; } public void setName(String value) { markModified(); name = value; } } Ein Objekt wird als persistent markiert, wenn es eine Referenz zu einem bereits persistenten Objekt erhält oder über die entsprechende API-Methode gespeichert wird. Die eigentliche Speicherung in der Datenbank erfolgt erst mit dem Abschluß einer laufenden Transaktion. Objekte können im Namensraum der Federated Database, einer Datenbank oder eines Containers gespeichert werden. Beim Laden von Objekten holt Objectivity zunächst eine Objektreferenz. Erst mit dem Zugriff auf die Methoden des Objekts werden dessen Daten geladen. Daher sollten alle get-Methoden eines Objekts zunächst die geerbte Methode fetch() ausführen, um die Daten aus der Datenbank zu laden. 116 6.5 Fazit Um mit den Objekten in einer verbundenen Federated Database zu arbeiten, muß zunächst eine Session geöffnet werden. Sie repräsentiert eine Transaktion und ist multithreading-fähig. Das heißt, eine Anwendung kann eine Session in allen Threads teilen. Die Abarbeitung seitens Objectivity erfolgt jedoch immer seriell. Für echte parallele Datenbankzugriffe benötigt jeder Thread seine eigene Session. Connection con = Connection.open("db.boot", oo.openReadWrite); Session s = new Session(); s.setOpenMode(oo.openReadWrite); s.begin(); // DB holen ooDBObj db = s.getFD().lookupDB("myDB"); // Datenbank-Operationen... s.commit(); con.close(); Die Klasse ooFDObj bietet alle notwendigen Methoden an, um die Datenbanken der Federated Database zu verwalten. So können über entsprechende Befehle Datenbanken erzeugt, gesucht und gelöscht werden. Eine Datenbank wird durch die Klasse ooDBObj repräsentiert und enthält Methoden, um Objekte zu speichern, zu finden und zu löschen. Objekte können unter anderem mit dem Befehl bind() gespeichert werden. Neben dem Objekt selbst ist auch ein eindeutiger Name für die Identifizierung anzugeben. Dieser dient dazu, das Objekt später mit der Methode lookup() aus der Datenbank zu laden. State s = new State("BB", "Brandenburg", "Potsdam"); db.bind(s, "BB"); Für das Laden mehrerer Instanzen einer Klasse steht die Methode scan() zur Verfügung. Sie erwartet mindestens den Namen der Klasse, deren Instanzen geladen werden sollen. Optional kann eine Zeichenkette mit Auswahlkriterien übergeben werden. Der Rückgabewert der Methode ist ein Iterator. String select = "zip>14000 && zip<15000"; Iterator it = db.scan(Location.class.getName(), select); Mit dem Befehl unbind() kann ein Objekt aus der Datenbank gelöscht werden. 6.5 Fazit Objektorientierte Datenbanksysteme gelten als der nächste logische Schritt der Datenbankentwicklung, um den Anforderungen objektorientierter Anwendungen für die Da- 6 | Objektorientierte Datenbanken 117 tenspeicherung gerecht zu werden. Allerdings sind die objektorientierten Systeme in ihrer Entwicklung lange nicht so weit fortgeschritten wie die etablierten relationalen Systeme. So gelten alle bisher verfügbaren Systeme als nicht ausgereift, weil vor allem die Erfahrung auf diesem Gebiet der Datenbanken fehlt. Durch die völlig neuen Konzepte ist die Migration zu objektorientierten Systemen sehr schwierig. Daraus und durch die hohe Verbreitung der relationalen Systeme folgt der geringe Markanteil. Einzig die objektrelationalen Datenbanken werden sich auf absehbare Zeit durchsetzen. Sie vereinen die bekannten relationalen Konzepte mit den Entwicklungen der Objektorientierung. Allerdings geht dadurch die Einfachheit in der Bedienung und Verwendung verloren. Objektrelationale Datenbanken erfüllen übrigens nicht das Ziel, den Impedance Mismatch zu vermeiden, da der Zugriff über SQL unverändert bestehen bleibt. 7 | Performance Gerade im Bereich von Webapplikationen spielt die Performance eine wichtige Rolle. Dies gilt zwar für alle Schichten einer Applikation, jedoch im besonderen für die Persistenzschicht. Sie ist dafür verantwortlich, die auf dynamischen Webseiten anzuzeigenden Daten möglichst schnell zu liefern. Leidet die Persistenzschicht an Performanceschwächen oder fällt sie gar aus, entsteht meist ein nicht zu unterschätzender Schaden. Dies gilt insbesondere für kommerzielle Webapplikationen. In den vorangegangenen Kapiteln wurden speziell die Unterschiede der vorgestellten Werkzeuge in der Bedienbarkeit seitens des Entwicklers - also in der Benutzung der API - deutlich. Allerdings spielt im Produktiveinsatz auch die Performance des Access Layers eine große Rolle. Daher sollen in diesem Kapitel ausgewählte Werkzeuge im Hinblick auf ihre Performance untersucht und verglichen werden. Zu diesem Zweck wurden Performance-Messungen in drei unterschiedlichen Testfällen durchgeführt: Speichern von Objekten, vollständiges Laden und Laden mit Lazy Loading. Das Laden wurde deshalb unterschieden, weil die meisten Tools zur Steigerung der Performance nicht sofort ein komplettes Objekt laden, sondern erst beim Zugriff auf ein Objekt dessen Daten aus der Datenbank nachladen. Die Realisierung des Lazy Loadings wird bei den verschiedenen Werkzeugen unterschiedlich gehandhabt. So lädt Hibernate standardmäßig alle einfachen Attribute eines Objekts sofort, Referenzen zu anderen persistenten Objekten jedoch erst, wenn auf diese Objekte zugegriffen wird. Ähnlich arbeiten auch OJB und TopLink. Alle anderen Werkzeuge laden bei einer Anfrage nur die IDs der gefundenen Objekte. Alle anderen Daten werden erst beim Zugriff auf die entsprechenden Attribute nachgeladen. Natürlich ist klar, daß dem Laden von Daten in einer Webapplikation ein größerer Stellenwert zukommt, als dem Speichern. Das liegt zum einen daran, daß viele Applikationen reine Lese-Operationen für die Anzeige von Daten ausführen. Zum anderen hält sich die Menge von erzeugten Daten einer Webapplikation meist in Grenzen. Es wurden insgesamt acht Werkzeuge ausgewählt und in den Messungen verglichen. Die Messungen wurden zudem mit unterschiedlicher Anzahl von Objekten durchgeführt. Als Testsystem kam ein AMD Athlon XP 1800+ mit 512 MB Arbeitsspeicher zum Einsatz. Die Tests wurden auf einer Windows 2000-Plattform mit installiertem JDK 1.4.2 und dem Applikationsserver JBoss 3.2.1 durchgeführt. Als Datenquelle wurde die von JBoss mitgelieferte HSQL-Datenbank benutzt. 120 Ab Seite 121 sind die Meßergebnisse der einzelnen Testszenarien sowohl tabellarisch als auch graphisch dargestellt. Die Tabelle zeigt für die jeweilige Objektanzahl den Mittelwert aus fünf Messungen und gibt außerdem die Komplexität an. Zwar kann per Definition der Komplexitätsfaktor wegfallen, er wurde jedoch zur besseren Vergleichbarkeit mit aufgeführt. Fazit Bis auf TopLink zeichnen sich alle Werkzeuge beim Lazy Loading durch hohe Geschwindigkeiten auch beim Laden einer Vielzahl von Objekten aus. Erst beim vollständigen Laden von Objekten zeigen sich deutliche Unterschiede zwischen den einzelnen Werkzeugen. So brechen Castor, EJB und XORM vollständig ein und verschlechtern ihre Performance auf eine Komplexität von O(n2 ). EJBs gelten im allgemeinen als „Performancekiller“. So verwenden die Applikationsserver immer mehrere - zumeist komplexe Anfragen. Optimierungen sind bei jedem Applikationsserver möglich, allerdings geht damit die Portabilität der Komponenten verloren. Positiv aufgefallen ist, daß alle Werkzeuge das Speichern mit einer Komplexität von O(n) meistern. Lediglich in den Faktoren unterscheiden sich die Tools. Prinzipiell war das Ergebnis in dieser Form zu erwarten, da beim Speichern von Objekten weitaus weniger Optimierungsmöglichkeiten bestehen als beim Laden. 7 | Performance 121 100 1000 5000 10000 50000 100000 Komplexität Castor db4o EJB/JBossCMP 0,01 0,01 0,29 0,02 0,01 0,32 0,08 0,06 0,59 0,13 0,12 0,73 1,10 0,51 1,77 2,43 0,92 3,79 0,02n 0,01n 0,03n Hibernate LiDO OJB 0,02 0,01 1,69 0,07 0,01 1,96 0,24 0,11 1,97 0,40 0,23 2,05 1,95 0,82 2,85 4,16 1,38 3,70 0,04n 0,01n 0,02n TopLink 0,02 0,09 0,32 0,69 6,61 22,48 0,1n XORM 0,09 0,09 0,09 0,09 0,09 0,09 0,09 Tabelle 7.1: Meßwerte für das Laden von Objekten (Lazy Loading) Abbildung 7.1: Graphische Darstellung der Meßwerte für das Lazy Loading 122 100 1000 5000 10000 50000 100000 Komplexität Castor db4o EJB/JBossCMP 0,05 0,01 2,65 0,22 0,04 80,57 2,66 0,28 - 18,58 0,56 - 478,00 2,72 - 1857,14 5,12 - 0,0002n2 0,05n 0,2n2 Hibernate LiDO OJB 0,02 0,02 1,69 0,07 0,07 1,96 0,24 0,22 1,97 0,40 0,65 2,05 1,95 4,12 2,85 4,16 8,09 3,70 0,04n 0,08n 0,02n TopLink 0,02 0,09 0,32 0,69 6,61 22,48 0,1n XORM 0,09 0,16 5,94 26,13 63,49 289,52 0,000025n2 Tabelle 7.2: Meßwerte für das vollständige Laden von Objekten Abbildung 7.2: Graphische Darstellung der Meßwerte für das vollständige Laden 7 | Performance 123 100 1000 5000 10000 50000 100000 Komplexität Castor db4o EJB/JBossCMP 0,61 0,02 0,30 4,47 0,12 1,79 22,42 0,49 8,46 44,44 0,99 17,75 225,20 5,62 85,26 10,55 170,84 4,5n 0,1n 1,75n Hibernate LiDO OJB 0,22 0,14 0,14 1,04 0,84 0,74 4,91 3,82 3,65 9,60 7,49 7,31 50,07 39,06 36,46 99,78 78,78 72,16 n 0,8n 0,75n TopLink 0,77 5,33 24,93 48,60 255,78 - 5n XORM 0,30 1,85 9,11 18,25 92,28 175,04 2n Tabelle 7.3: Meßwerte für das Speichern von Objekten Abbildung 7.3: Graphische Darstellung der Meßwerte für das Speichern 8 | Kurzübersicht O/R-Mapping In Tabelle 8.1 werden die in den vorherigen Kapiteln vorgestellten Mapping-Techniken und -Werkzeuge anhand der folgenden neun Kriterien in Kurzform gegenübergestellt und verglichen. • Mapping GUI Bietet ein O/R-Mapper eine graphische Oberfläche an, mit der das Mapping problemlos realisiert werden kann? • arbiträre Klassen Können vorhandene Klassen ohne signifikante Änderungen benutzt werden? Oder wird eine spezielle Oberklasse benötigt, oder muß ein spezielles Interface implementiert werden? • Aggregatfunktionen Bietet eine API auch SQL-Aggregatfunktionen wie avg, min oder max an? • zusammengesetzte Primärschlüssel Kann ein O/R-Mapper zusammengesetzte Primärschlüssel (PKs) für die Objektidentifizierung benutzen? • Vererbung/Polymorphismus Unterstützt ein O/R-Mapper Vererbungshierarchien? • zusätzliche Tabellen Benötigt ein O/R-Mapper spezielle Tabellen, um seine Arbeit verrichten zu können? • Codegenerierung Handelt es sich bei einem O/R-Mapper um einen Codegenerator? Das heißt, erzeugt er aus einem vorhandenen Schema oder Mapping passende Java-Klassen? • vorhandenes DB-Schema Ist es möglich, Reverse Engineering auf ein vorhandenes Datenbank-Schema anzuwenden? • Standard-APIs Welche Standard-APIs neben der eigenen propietären bietet ein O/R-Mapper an? arbiträre Klassen Aggregatfunktionen zusammengesetzte PKs Vererbung/Polymorphismus zusätzliche Tabellen Codegenerierung vorhandenes DB-Schema -6 - - + + - - + IntelliBO KodoJDO LiDO TJDO XORM + + - +5 +5 +5 +4 + - + + + ? - + + + + - ? -3 + - +1 + + + + Abra Castor Cayenne +2 + + - + + + + ? -3 -3 + + + + + CocoBase DataBind DBGen Firestorm Hibernate Jaxor OJB PE:J TopLink + + + + + + + + + + + + + ? ? + ? + + + + + ? ? + + + + + + ? + + + + + -3 + -3 -3 + + +1 + - + + + + + + + + + EJB +5 1 Codegenerierung zur Laufzeit 2 Drittanbieter 3 nur für automatische Primärschlüsselerzeugung 4 nur Interfaces oder abstrakte Klassen 5 Bytecode Enhancement 6 anbieterspezifisch, JBoss: nein Tabelle 8.1: Kurzübersicht O/R-Mapping Standard-APIs Mapping GUI 126 JDO JDO JDO JDO JDO ODMG JDO, ODMG JDO 9 | Fazit Die vorangegangenen Kapitel haben einen Einblick in die Vielfalt der Möglichkeiten gegeben, mit denen die Persistenzschicht einer Applikation realisiert werden kann. Es hat sich herausgestellt, daß die Serialisierung und der Datenbankzugriff über JDBC nicht ausreichend sind, um die Anforderungen an moderne Webapplikationen zu erfüllen. Für den Zugriff auf relationale Datenbanken benutzen alle Werkzeuge natürlich JDBC. Nach außen hin hat der Entwickler jedoch eine vollständig objektorientierte Sicht auf die Persistenzschicht, da der Datenbankzugriff vollständig gekapselt wird. Dadurch verringern sich Entwicklungsaufwand und Entwicklungszeit von Applikationen merklich. Außer der Konfiguration der eingesetzten Persistenzschicht muß sich der Entwickler um keine weiteren datenbankspezifischen Details kümmern. Als erste große Entwicklung haben sich die Enterprise JavaBeans in Unternehmensanwendungen durchgesetzt. Jedoch hat sich gezeigt, daß gerade die für die Persistenz verantwortlichen Entity Beans sehr komplex sind. Sie benötigen einen hohen Einarbeitungsaufwand, und die richtige Konfiguration erweist sich zumeist als schwierig. Die Entity Beans nutzen wie alle anderen Werkzeuge die Technik des objektrelationalen Mappings, um eine Abbildung von Objekten auf relationale Datenbanken zu realisieren. Da sich die objektorientierten Datenbanken auf absehbare Zeit nicht gegen die etablierten relationalen Systeme durchsetzen werden, wird das objektrelationale Mapping auf lange Sicht die einzige und bevorzugte Technik bleiben, mit persistenten Objekten unter Vermeidung bzw. Verlagerung des Impedance Mismatch zu arbeiten. Die Einsatzfähigkeit in Webapplikationen ist bei allen vorgestellten Technologien mit Ausnahme der Objektdatenbanken gegeben. Diese können nur dann in Webapplikationen eingesetzt werden, wenn sie den Mehrbenutzerbetrieb - also den gleichzeitigen Zugriff mehrerer Clients - unterstützen. Die als Beispiel vorgestellte Objektdatenbank db4o wird diesem Umstand nur durch eine spezielle Servlet-API gerecht, die dafür sorgt, daß nur ein Objekt-Container verfügbar ist und dieser von anfragenden Clients benutzt wird. Bei allen anderen Werkzeugen entscheiden letztendlich das Projektumfeld, der Funktionsumfang des Werkzeugs sowie die anfallenden Kosten über den Einsatz in einer Produktivumgebung. Daher kann auch keine pauschale Aussage getroffen werden, welche Technologie am besten geeignet ist oder von welcher abzuraten ist. 128 Neue Ansätze Die etablierten Datenbanksysteme legen die persistenten Daten zumeist in Dateiform im physischen Sekundärspeicher ab. Dies hat zur Folge, daß der Zugriff etwas länger dauert, als der Zugriff auf Daten, die im Speicher liegen. Aus diesem Grund integrieren die DBMS spezielle Caching-Mechanismen, um die Zugriffszeiten zu verringern. In letzter Zeit haben sich einige Projekte gegründet, die hauptspeicherbasierte Datenbanken anbieten. Diese legen die persistenten Daten nicht im Sekundärspeicher ab, sondern direkt im Hauptspeicher. Dadurch wird der Zugriff extrem beschleunigt. Solche Systeme verlangen natürlich spezielle Sicherheitsmaßnahmen gegen Systemausfälle. Ein Rechnerabsturz würde bereits genügen, um einen hohen Datenverlust zu verursachen. Hauptspeicherbasierte Datenbanken müssen also in regelmäßigen Abständen Daten in den Sekundärspeicher sichern. Beispiele für solche Systeme sind ODIR von der FH Brandenburg und Prevayler. A | Anhang 130 A.1 Servlets und Java Server Pages A.1 Servlets und Java Server Pages Servlets und Java Server Pages (JSP) sind Bestandteil der Java 2 Enterprise Edition. Mit ihrer Hilfe können dynamische Webseiten generiert werden. Sie ähneln den Common Gateway Interfaces (CGI), sind allerdings aufgrund der Verwendung der Sprache Java plattformunabhängig. Beide Technologien benötigen einen Webserver, der die Ausführung von Servlets ermöglicht (z.B. Apache Tomcat). Java Server Pages werden beim ersten Aufruf vom Webserver in Servlets umgewandelt, kompiliert und ausgeführt. Ein Servlet ist nichts anderes als ein normales JavaProgramm und stellt die Schicht zwischen der Anfrage eines Clients und der Geschäftslogik auf dem Server dar. Es kann Sitzungen verwalten und als Schnittstelle zu Datenbanken, anderen Java-Applikationen sowie Mail- und Verzeichnisdiensten dienen [JS01]. Servlets sind in der Servlet-Spezifikation [JSS03] zusammen mit einer umfassenden API standardisiert, Java Server Pages in der JSP-Spezifikation [JSP03]. A.1.1 Servlets Normalerweise verarbeitet ein Servlet GET- oder POST-Anfragen des HTTP-Protokolls, vorstellbar sind jedoch auch Servlets für andere Protokolle (z.B. FTP). HTTP-Servlets müssen von der Klasse HttpServlet abgeleitet werden und können die benötigten Methoden überschreiben. Die zwei wichtigsten sind hierbei doGet() und doPost(). Je nachdem, welche Methode implementiert ist, verarbeitet ein Servlet nur GET-Anfragen, nur POST-Anfragen oder beide. public class MyServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); // HTML-Ausgabe erzeugen out.close(); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } } A | Anhang 131 Beide Methoden erwarten zwei Argumente: Der erste Parameter ist ein Objekt der Klasse HttpServletRequest, welches die Anfrage des Clients kapselt und Daten aus HTTPHeadern, HTML-Formularen sowie der laufenden Session enthält. Der zweite Parameter als Instanz der Klasse HttpServletResponse enthält die vom Servlet generierte Antwort für den Client. Dies kann ein HTTP-Statuscode oder eine vollständige HTML-Seite sein. Die Ausgabe wird in ein PrintWriter-Objekt geschrieben, welches vom AntwortObjekt zur Verfügung gestellt wird. A.1.2 Java Server Pages Die Java Server Pages Spezifikation [JSP03] erweitert die Java Servlet-API und bietet dem Entwickler von Web-Anwendungen ein Framework zur Gestaltung von dynamischen Inhalten auf einem Webserver [JS01]. Eine JSP-Seite kann sowohl aus HTML- als auch aus JSP-Elementen bestehen. Die Syntax der JSP-Elemente basiert auf XML, die Elementdaten einer JSP-Seite können in folgende Kategorien eingeteilt werden: • Direktiven • Deklarationen • Scriptlets • Ausdrücke • Standardaktionen Direktiven JSP-Direktiven dienen als Nachrichten, die von einer JSP-Seite zum JSP-Container übermittelt werden und sind innerhalb der gesamten JSP-Seite gültig, in der sie angegeben wurden [JS01]. Sie generieren keine Ausgaben für den Client. Die allgemeine Syntax lautet: <%@ directive attribute="value" %> JSP-Seiten können auf drei Direktiven zurückgreifen: • Die Direktive page Sie definiert Attribute, die die gesamte Seite betreffen. Dazu gehören u.a. die Teilnahme an einer HTTP-Session, der Inhaltstyp der Seite und Importanweisungen. <%@ page language="java" contentType="text/html" session="true" %> 132 A.1 Servlets und Java Server Pages • Die Direktive include Sie dient der Einbettung von statischen Ressourcen. Der Zugriff auf die angegebene Datei muß gewährleistet sein. <%@ include file="filename" %> • Die Direktive taglib Mit dieser Direktive kann die JSP-Datei benutzerdefinierte Tags verwenden. Stößt die JSP-Seite auf einen solchen Tag, so wird die angegebene Bibliothek benutzt, um den Tag auszuwerten. <%@ taglib uri="path" prefix="tagPrefix" %> Deklarationen Eine Deklaration ist ein Java-Codeblock in der JSP-Datei, der klassenweit gültige Variablen und Methoden in der generierten Klassendatei definiert. <%! int answer = 42; %> Scriptlets Ein Scriptlet ist ein Java-Codeblock, der während der Anfrageverarbeitung ausgeführt wird. Innerhalb eines solchen Codeblocks können beliebige Java-Ausdrücke angegeben werden, Methodenaufrufe ausgeführt werden, in den Ausgabestream geschrieben werden usw. <% for(int i=0; i<answer; i++) out.println("<p>Hello World!</p>"); %> Ausdrücke Ein Ausdruck ist die Kurzform eines Scriptlets und kann verwendet werden, um den Wert einer Variablen in den Ausgabestream zu schreiben. Dies entspricht dem Aufruf der toString()-Methode des angegebenen Objekts. Die Antwort auf alle Fragen des Universums lautet <%= answer %>. A | Anhang 133 Standardaktionen Aktionen sind Tags, die das Laufzeitverhalten der JSP-Seite und somit auch die Antwort für den Client beeinflussen können. Folgende Standardaktionen gibt es: • <jsp:useBean>, <jsp:setProperty>, <jsp:getProperty> Diese Tags werden in Verbindung mit JavaBeans eingesetzt, in denen der JavaCode der Geschäftslogik gekapselt werden kann. Mit Hilfe dieser Tags wird die Trennung der Präsentation von der Logik realisiert. Empfehlung ist, auf Scriptlets in der JSP-Seite möglichst zu verzichten und derartige Codeabschnitte in JavaBeans auszulagern. Mit dem Tag <jsp:useBean> wird eine JavaBean mit der JSPSeite unter Angabe eines Gültigkeitsbereichs verknüpft. Der Container versucht, die angegebene Bean zu finden; sollte das Objekt bereits existieren, so wird es verwendet, andernfalls wird eine Instanz erzeugt. <jsp:useBean id="name" class="className" scope="page|request|session|application" /> Die beiden anderen Tags werden benutzt, um Eigenschaften der Bean gemäß der JavaBeans-Spezifikation [JB97] zu setzen bzw. zu lesen. Angegeben wird jeweils der Name der Bean, die gewünschte Eigenschaft sowie beim schreibenden Zugriff der zu übergebende Wert. Wird beim Schreiben für die gewünschte Eigenschaft der Wert * eingesetzt, so wird versucht, alle im Namen übereinstimmenden Parameter aus dem Request und Eigenschaften der Bean zu finden und die Werte zu setzen. In dem Fall entfällt die Angabe von value. <jsp:setProperty name="beanName" property="..." value="..." /> <jsp:getProperty name="beanName property="..." /> • <jsp:param> Dieser Tag kann als Subtag für die Aktionen include, forward und plugin benutzt werden, um zusätzliche Daten zu übergeben. Anzugeben ist jeweils der Parametername und der Parameterwert. <jsp:param name="paramName" value="paramValue" /> • <jsp:include> Diese Aktion bindet eine statische oder dynamische Ressource in die JSP-Datei ein. Die Angabe erfolgt als URL, die eingebundene Datei darf ihrerseits keine Header oder Cookies setzen. <jsp:include page="pageUrl" /> 134 A.1 Servlets und Java Server Pages • <jsp:forward> Diese Aktion leitet eine Anfrage an die angegebene Ressource weiter; dabei kann es sich um ein Servlet, eine andere JSP-Datei oder eine statische HTML-Seite handeln. Das Ziel muß sich im gleichen Kontext wie die weiterleitende JSP-Seite befinden. <jsp:forward page="pageUrl" /> • <jsp:plugin> Diese Aktion kann ein Applet in eine Webseite einbinden. Dabei sorgt die Aktion automatisch dafür, daß die entsprechenden HTML-Tags (embed bzw. object) in der erzeugten Seite gesetzt werden. Implizite Objekte Einer JSP-Seite stehen eine Reihe von Objekten zur Verfügung, die nicht vom Entwickler deklariert oder instantiiert werden müssen. Alle impliziten Objekte können nur innerhalb von Scriptlets und Ausdrücken verwendet werden. Zu diesen Objekten gehören unter anderem: • Das Objekt request Dieses Objekt repräsentiert die vom Client stammende Anfrage, die von der JSPSeite verarbeitet wird. Darin enthalten sind unter anderem alle GET- und POSTParameter. Object o = request.getParameter{"param"); • Das Objekt session Dieses Objekt repräsentiert die Session des anfragenden Clients und hält alle sitzungsspezifischen Daten. Sitzungen werden vom Container automatisch erzeugt, jeder Client bekommt eine Sitzungs-ID zugeordnet. Object o = session.getAttribute("att"); • Das Objekt out Der Ausgabestream zum Client wird durch dieses Objekt repräsentiert. Innerhalb von Scriptlets können HTML-Ausgaben in dieses Objekt geschrieben werden. out.print("Hallo Welt!"); A | Anhang A.2 135 Java Database Connectivity Mit der Entwicklung der Java Database Connectivity (JDBC) hat Sun eine einheitliche Schnittstelle für den Zugriff auf relationale Datenbanken aus Java-Applikationen heraus geschaffen. JDBC wurde in Anlehnung an ODBC entwickelt und stellt ein Call-LevelInterface dar. Das heißt, SQL-Anfragen werden in Form von Zeichenketten übernommen und an die Datenbank weitergeleitet. Für den Zugriff auf eine Datenbank bietet der Datenbank-Hersteller üblicherweise einen JDBC-Treiber an, der die zu JDBC gehörigen Interfaces des Packages java.sql implementiert. Die JDBC-Treiber werden in vier Typen eingeteilt (nach [GK03]): • Typ-1-Treiber Die JDBC-ODBC-Bridge als Typ-1-Treiber gehört zum Lieferumfang der J2SE und kann Datenbanken ansprechen, für die ein ODBC-Treiber verfügbar ist. • Typ-2-Treiber In diese Gruppe gehören alle JDBC-Treiber, die auf einem proprietären Treiber des Datenbank-Herstellers aufsetzen. • Typ-3-Treiber Typ-3-Treiber sind komplett in Java entwickelte JDBC-Treiber, die jedoch zur Kommunikation mit der Datenbank auf eine funktionierende Middleware angewiesen sind. • Typ-4-Treiber JDBC-Treiber dieses Typs sind ebenfalls vollständig in Java entwickelt. Sie übersetzen die SQL-Anfragen direkt in das Protokoll der Datenbank. Bevor aus einer Java-Applikation heraus auf eine Datenbank zugegriffen werden kann, muß zunächst der notwendige Treiber geladen werden. Anschließend kann eine Verbindung hergestellt werden. Dazu wird ein Connection-String benötigt, der alle notwendigen Informationen wie Datenbank-Protokoll, Rechnername, Datenbankname sowie optional Benutzername und Paßwort enthält. Dieser String hat das allgemeine Format jdbc:<protocol>://[hostname][:port]/[dbname] Nach erfolgreicher Verbindung steht ein Connection-Objekt zur Verfügung, welches die Datenbank-Verbindung kapselt und Factory-Methoden für Anweisungsobjekte bereitstellt. Die Anwendungsobjekte implementieren das Interface Statement und ermöglichen Anfragen an die Datenbank. So steht die Methode executeQuery() zur Verfügung, um SELECT-Anfragen an die Datenbank abzusetzen. Der Rückgabewert ist eine Ergebnismenge vom Typ ResultSet. Dieses kann mit Hilfe der Methode next() zeilenweise durchlaufen werden. Um auf eine Spalte zuzugreifen, stehen eine Reihe von get-Methoden der Form getDatatype() zur Verfügung, abhängig vom Datentyp der Spalte. Zudem existiert jede dieser Methoden in zwei Varianten: Eine erwartet den Spaltenindex als numerischen Wert, die andere erwartet den Spaltennamen als Zeichenkette. 136 A.2 Java Database Connectivity Für Anfragen, die keine Ergebnismenge liefern, steht die Methode executeUpdate() zur Verfügung. Sie liefert einen numerischen Wert zurück, der angibt, wieviele Zeilen betroffen waren. Diese Methode wird angewandt, um INSERT- oder UPDATE-Anweisungen an die Datenbank zu schicken. Das folgende Beispiel lädt den JDBC-Treiber der MySQL-Datenbank, öffnet eine Verbindung zu einem Datenbankserver und setzt zwei Anfragen ab: Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/locations_db"; String user = "locations"; String passwd = "geheim"; Connection c = DriverManager.getConnection(url, user, passwd); Statement s = c.createStatement(); s.executeUpdate("create table locations (" + " id bigint not null, " + " name varchar(64))" ); ResultSet rs = s.executeQuery("select * from locations"); while ( rs.next() ) { System.out.println(rs.getLong("id")); System.out.println(rs.getString("name")); } s.close(); c.close(); Soll eine SQL-Anweisung wiederholt ausgeführt werden, allerdings mit unterschiedlichen Daten, bietet sich ein Prepared Statement an. Dabei handelt es sich um eine parametrisierte SQL-Anweisung, die der Datenbank zur Vorkompilierung übergeben wird. Eine solche Anfrage kann beliebig oft ausgeführt werden, wobei die formalen Parameter durch konkrete Werte ersetzt werden. Im Ergebnis ist die Ausführung eines Prepared Statements schneller, da unter anderem die Syntaxanalyse durch die Datenbank nur einmal vorgenommen werden muß. String stmt = "insert into location values(?, ?)"; PreparedStatement ps = c.prepareStatement(stmt); // locs sei ein zweidimensionales Array for(int i=0; i<locs.length; i++) { ps.setLong(1, locs[i][0]); ps.setString(2, locs[i][1]); ps.execute(); } Das Fragezeichen dient als Platzhalter für die Parameter, die Zählung beim Einfügen beginnt bei 1. A | Anhang A.3 137 Transaktionen und JTA Transaktionen sind ein zentraler Bestandteil der Datenbanktechnik speziell im Mehrbenutzerbereich [HS00]. Eine Transaktion wird definiert als eine Menge von Operationen, die die Datenbank von einem konsistenten Zustand in einen anderen konsistenten Zustand überführt. Jede Transaktion unterliegt dabei dem ACID-Prinzip. Die Abkürzung steht für Atomarität, Konsistenz, Isolation und Dauerhaftigkeit. Im einzelnen bedeutet dies: • Atomarität Eine Transaktion wird als eine Operation angesehen und somit komplett oder gar nicht ausgeführt. Schlägt eine der Teiloperationen fehl, so schlägt auch die Transaktion fehl. Bereits gemachte Änderungen an der Datenbank werden dann zurückgenommen (ROLLBACK). Waren hingegen alle Teiloperationen erfolgreich, so werden die Daten in die Datenbank übernommen (COMMIT). • Konsistenz Eine Transaktion muß nach Beendigung die Datenbank in einem konsistenten Zustand hinterlassen. • Isolation Laufen mehrere Transaktionen parallel ab, so sind sie voneinander isoliert. Das heißt, sie können sich nicht gegenseitig beeinflussen. Auch kann eine Transaktion nicht die geänderten Daten einer anderen Transaktion einsehen, solange diese nicht erfolgreich beendet wurde. • Dauerhaftigkeit Eine erfolgreiche Transaktion hinterläßt immer persistente (dauerhafte) Daten. Als Beispiel für eine Transaktion wird häufig der Vorgang einer Überweisung von einem Konta A auf ein Konto B herangezogen. Dieser Vorgang besteht aus zwei Operationen. So muß zunächst der Betrag vom Konto A abgebucht werden, um anschließend auf Konto B gutgeschrieben zu werden. Würden beide Operationen nicht zu einer Transaktion zusammengefaßt, so kann es zu Inkonsistenzen in der Datenbank führen. Als Transaktion werden jedoch beide Operationen ausgeführt. Kommt es im Laufe der Transaktion zu Problemen, so werden die Ergebnisse beider Teiloperationen rückgängig gemacht. So bleibt in jedem Fall der konsistente Zustand der Datenbank erhalten. Das Two-Phase-Commit-Protokoll In verteilten Systemen können Transaktionen wie oben definiert nur unter erschwerten Bedingungen eingesetzt werden. So ist es durchaus möglich, daß die Teiloperationen einer Transaktion auf verschiedenen Rechner ausgeführt werden. Für solche Fälle wurde das Two-Phase-Commit-Protokoll entworfen. Es definiert zwei Rollen: Der Koordinator löst eine Transaktion aus und überwacht sie, Teilnehmer repräsentieren die Teiloperationen. 138 A.3 Transaktionen und JTA Wie der Name bereits andeutet, besteht bei diesem Protokoll eine Transaktion aus zwei Phasen. In der ersten Phase (Precommit-Phase) sendet der Koordinator ein Signal an alle Teilnehmer, sich auf ein COMMIT vorzubereiten. In der zweiten Phase (Post-DecisionPhase) sendet der Koordinator dann abhängig von den Antworten aus der ersten Phase entweder ein COMMIT oder ein ROLLBACK an alle Teilnehmer. Nur wenn alle Teilnehmer in der ersten Phase mit einem CAN COMMIT geantwortet haben, wird ein COMMIT an alle Teilnehmer gesendet. Hat auch nur einer der Teilnehmer ein CANNOT COMMIT gesendet, so erhalten alle Teilnehmer den Befehl ROLLBACK und müssen die von ihnen gemachten Änderungen verwerfen. Die Java Transaction API Die meisten J2EE-Applikationsserver benutzen ein Transaktionsmanagement auf der Basis der Java Transaction API (JTA). In einem Transaktionskontext definiert JTA die folgenden Rollen: • Ressourcen-Manager Ein Ressourcen-Manager kann eine JDBC-konforme Datenbank, einen JMS-Provider oder ein J2EE-Connector-fähiges EIS1 darstellen [CK03]. • Transaktionsmanager Der Transaktionsmanager dient der Kontrolle von Transaktionen. Er hat also unter anderem die Aufgabe, Transaktionen zu starten und zu stoppen. • Applikation Eine Anwendung hat die Möglichkeit, Transaktionen selbst zu steuern. Für jede dieser Rollen stehen in der JTA entsprechende Interfaces zur Verfügung. So dient das Interface UserTransaction dazu, transaktionalen Applikationen die Steuerung von Transaktionen über entsprechende Methoden zu ermöglichen. 1 Enterprise Information System A | Anhang A.4 139 Key-Generatoren Die meisten der vorgestellten Werkzeuge können die Erzeugung von Primärschlüsseln für die Speicherung von Daten in der Datenbank selbst übernehmen. Hierzu stehen je nach Werkzeug unterschiedlich viele Key-Generatoren zur Verfügung, teilweise können sogar eigene Implementierungen benutzt werden. Die wichtigsten Key-Generatoren sollen im folgenden kurz vorgestellt werden. High/Low-Generator Dieser Generator benutzt einen High/Low-Algorithmus für die Erzeugung von Primärschlüsseln, die innerhalb einer Datenbank eindeutig sind. Für diesen Generator wird eine zusätzliche Tabelle in der Datenbank benötigt, in der der High-Wert gespeichert wird. Dieser wird beim ersten Zugriff ausgelesen, um die sogenannte Grab-Size n erhöht und wieder in der Tabelle gespeichert. Ohne Datenbankzugriff stehen dem Mapping-Tool nun n-1 Werte für die Vergabe an neue Objekte zur Verfügung. UUID-Generator Dieser Generator erzeugt global eindeutige Primärschlüsselwerte. Der erzeugte Schlüssel ist eine Kombination aus IP-Adresse, aktueller Zeit und einem Zähler. Das Ergebnis ist ein mindestens 16 Byte langer String im Hexadezimal- oder ASCII-Format. Sequenzen und Identity Hierbei handelt es sich nicht um Key-Generatoren, sondern um Features des darunterliegenden Datenspeichers. So bezeichnet Identity die u.a. bei MySQL verfügbaren AUTOINCREMENT-Spalten. Sequenzen werden u.a. von Oracle und Sybase nativ angeboten, können aber auch mit Hilfe einer zusätzlichen Sequenztabelle realisiert werden. Darin verzeichnet ist der Tabellenname sowie der nächste Wert der Sequenz. 140 A.5 A.5 JavaBeans JavaBeans Bei JavaBeans handelt es sich um wiederverwendbare Software-Komponenten laut dem Java-Komponentenmodell. JavaBeans sind Suns Antwort auf die in der Software-Industrie steigende Anfrage nach einem Standardsatz zur Definition von Software-Komponenten [JS01]. Dieser Standard ist in der JavaBeans API Specification [JB97] von 1997 festgeschrieben. Die drei wichtigsten Features einer JavaBean sind Eigenschaften, die den internen Zustand der Bean sowie deren Daten darstellen, Methoden, die von anderen Komponenten aufgerufen werden können sowie Ereignisse, die von der Bean ausgelöst werden können [JB97]. Die Eigenschaften einer Bean werden normalerweise in einem privaten oder geschützten Attribut gespeichert, welches durch ein Paar von öffentlichen Zugriffsmethoden anderen Komponenten zugänglich gemacht wird. Der Datentyp einer Eigenschaft kann beliebig sein, also primitiv, ein (benutzerdefiniertes) Java-Objekt oder ein Array. In der JavaBeans-Spezifikation wird das Muster der Zugriffsmethoden wie folgt definiert: public void set<PropertyName>(<propertyType> value) public <propertyType> get<PropertyName>() public boolean is<PropertyName>() Die Methoden zum Setzen eines Eigenschaften-Wertes haben das Namens-Präfix set gefolgt vom Namen der Eigenschaft. Der Rückgabewert ist void, der Übergabeparameter hat den gleichen Datentyp wie die Eigenschaft. Die Methoden zum Auslesen einer Eigenschaften beginnen mit get gefolgt vom Namen der Eigenschaft und geben einen Wert vom gleichen Datentyp wie die Eigenschaft zurück. Für Eigenschaften vom Typ boolean ist alternativ das Präfix is möglich. Auch indizierte Eigenschaften, das heißt Arrays, sind über Zugriffsmethoden zugänglich, das Muster sieht wie folgt aus: public public public public void set<PropertyName>(int index, <propertyType> value) void set<PropertyName>(<propertyType>[] value) <propertyType> get<PropertyName>(int index) <propertyType>[] get<PropertyName>() Alle Eigenschaften sollten grundsätzlich mit einem Kleinbuchstaben beginnen, bei allen Zugriffsmethoden wird dieser dann durch einen Großbuchstaben ersetzt, also beispielsweise: A | Anhang 141 String name; public void setName(String name) { this.name = name; } public String getName() { return this.name; } Zusätzlich zu den Zugriffsmethoden für die Eigenschaften kann eine JavaBean beliebige Geschäftsmethoden definieren, die jedoch keinen Restriktionen seitens der JavaBeansSpezifikation unterliegen. Weiterhin sind JavaBeans dazu in der Lage, mit anderen Objekten und Beans zu kommunizieren. Dies wird erreicht, indem eine Bean-Ereignisse auslösen und auf Ereignisse anderer Beans reagieren kann. Eine Bean kann sich hierfür als Listener2 für verschiedene Ereignisse registrieren lassen. 2 Listener: eine Applikation oder Komponente, die Ereignisnachrichten abhört 142 A.6 A.6 Die Beispielanwendung Die Beispielanwendung Die durchgängig aufgeführten Beispiele arbeiten mit einem kleinen Datenmodell, das zur Beispielanwendung gehört. Mit Hilfe von drei Klassen soll eine Ortsdatenbank modelliert werden. Es gibt Bundesländer (Klasse State), die eine Menge von Orten (Klasse Location) enthalten können. Jeder Ort hat zudem eine Referenz auf ein Objekt vom Typ Size, welches Größen-Informationen enthält (Einwohnerzahl). Die Beziehung zwischen State und Location ist bidirektional 1:n. Die Beziehung zwischen Location und Size ist unidirektional n:1. Abbildung A.1 zeigt das Datenmodell in UML-Notation, wobei die Angaben um die Konstruktoren und get-/set-Methoden gekürzt wurden. # !!" # Abbildung A.1: Datenmodell der Beispiel-Anwendung Der sogenannte Storage-Manager ist für das Laden und Speichern von Objekten verantwortlich. Um eine gleichartige Schnittstelle zu schaffen, wurde das Interface StorageManager entwickelt. Es definiert die Methoden, die eine Implementierung bereitstellen muß, um in die Anwendung integriert werden zu können. Alle Zugriffe der Geschäftslogik geschehen über die Methoden dieses Interfaces. Um eine Trennung der Schichten zu gewährleistet, liefert die Geschäftslogik nie eines der Objekte des Datenmodells an die Präsentationsschicht. Für den Datenaustausch wurden Datentransfer-Objekte (DTOs) entwickelt, die nur die notwendigen Daten von und zur Präsentationsschicht liefern. Um eine Implementierung des StorageManagers zu erhalten, steht eine Factory-Klasse zur Verfügung. Von dieser Klasse kann unter Angabe eines Identifikators eine Implementierung des StorageManager-Interfaces geholt werden. Sechs Implementierungen A | Anhang 143 sind bereits in der Beispielanwendung enthalten. Das Interface StorageManager public interface StorageManager { public public public public public public static static static static static static final final final final final final int int int int int int LIDO = 10; HIBERNATE = 20; DB4O = 30; EJB = 40; CASTOR = 50; TOPLINK = 60; public void init(Properties props) throws StorageManagerException; public void close() throws StorageManagerException; public int getId(); public void begin() throws StorageManagerException; public void rollback() throws StorageManagerException; public void commit() throws StorageManagerException; public Object load(Class clazz, Long oid) throws StorageManagerException; public Collection loadAll(Class clazz) throws StorageManagerException; public Collection loadByPattern(Class c, String m, String p) throws StorageManagerException; public boolean save(Object o) throws StorageManagerException; public boolean update(Object o) throws StorageManagerException; public boolean delete(Object o) throws StorageManagerException; } 144 A.7 A.7 Die JDO-DTD Die JDO-DTD <?xml version="1.0" encoding="UTF-8"?> <!ELEMENT jdo (package)+> <!ELEMENT package ((class)+, (extension)*)> <!ATTLIST package name CDATA #REQUIRED> <!ELEMENT class (field|extension)*> <!ATTLIST class name CDATA #REQUIRED> <!ATTLIST class identity-type (application|datastore|nondurable) #IMPLIED> <!ATTLIST class objectid-class CDATA #IMPLIED> <!ATTLIST class requires-extent (true|false) ’true’> <!ATTLIST class persistence-capable-superclass CDATA #IMPLIED> <!ELEMENT field ((collection|map|array)?, (extension)*)?> <!ATTLIST field name CDATA #REQUIRED> <!ATTLIST field persistence-modifier (persistent|transactional|none) #IMPLIED> <!ATTLIST field primary-key (true|false) ’false’> <!ATTLIST field null-value (exception|default|none) ’none’> <!ATTLIST field default-fetch-group (true|false) #IMPLIED> <!ATTLIST field embedded (true|false) #IMPLIED> <!ELEMENT collection (extension)*> <!ATTLIST collection element-type CDATA #IMPLIED> <!ATTLIST collection embedded-element (true|false) #IMPLIED> <!ELEMENT <!ATTLIST <!ATTLIST <!ATTLIST <!ATTLIST map map map map map (extension)*> key-type CDATA #IMPLIED> embedded-key (true|false) #IMPLIED> value-type CDATA #IMPLIED> embedded-value (true|false) #IMPLIED> <!ELEMENT array (extension)*> <!ATTLIST array embedded-element (true|false) #IMPLIED> <!ELEMENT <!ATTLIST <!ATTLIST <!ATTLIST extension extension extension extension (extension)*> vendor-name CDATA #REQUIRED> key CDATA #IMPLIED> value CDATA #IMPLIED> A | Anhang A.8 Primärschlüssel-Klasse für JDO Application Identity public class StatePK implements Serializable { public long id; public StatePK() { } public StatePK(long id) { this.id = id; } public StatePK(String sid) { this.id = Long.parseLong(sid); } public boolean equals(Object o) { try { StatePK os = (StatePK)o; if ( os.id == this.id ) return true; else return false; } catch ( Exception e ) { return false; } } public int hashCode() { return ( new Long(id).hashCode() ); } public String toString() { return Long.toString(id); } } 145 146 A.9 A.9 JDODoclet JDODoclet /* * @jdo.persistence-capable */ public class State implements Serializable { /** @jdo.field */ private String shortName; /** @jdo.field */ private String name; /** @jdo.field */ private String capital; /** * @jdo.field * collection-type="collection" * element-type="Location" * @sql.relation * style="foreign-key" * related-field="state" */ private Set locations; [...] } /* @jdo.persistence-capable * @jdo.class-vendor-extension * vendor-name="libelis" * key="sql-reverse" * value="javaField:size" */ public class Size implements Serializable { /** @jdo.field */ private int minimum; /** @jdo.field */ private int maximum; [...] } A | Anhang /* * @jdo.persistence-capable */ public class Location implements Serializable { /** @jdo.field */ private String name; /** @jdo.field */ private double latitude; /** @jdo.field */ private double longitude; /** @jdo.field */ private int zip; /** @jdo.field */ private State state; /** @jdo.field */ private Size size; [...] } 147 148 A.10 A.10 JDOMapper JDOMapper JDOMapper ist ein Drittanbieter-Programm für Castor. Das Tool kann aus vorhandenen Klassen sowohl das Mapping für CastorJDO als auch für CastorXML erzeugen und exportieren. Dabei validiert es ständig die Benutzereingaben und zeigt entsprechende Fehlermeldungen im unteren Bildschirmbereich an. Abbildung A.2: JDOMapper Die Modellklassen, für die ein Mapping erzeugt werden soll, müssen im Klassenpfad liegen; diese Anpassung wird am besten in der mitgelieferten Batch- bzw. ShellskriptDatei vorgenommen. Nach dem Programmstart und dem Erzeugen eines neuen Projekts werden alle Klassen über den Menüpunkt Project -> Add Classes unter Angabe ihres vollqualifizierten Namens hinzugefügt. Jede Klasse erscheint dann zusammen mit ihren Attributen im Baum auf der linken Seite, die Eingabefelder für die entsprechenden Informationen auf der rechten Seite. JDOMapper stellt die meisten Informationen bereits automatisch zusammen; über Project -> Preview Mapping kann jederzeit der aktuelle Stand des Mappings abgerufen werden. Jedem Projekt muß über den Menüpunkt Project -> Add Key Generator mindestens ein Keygenerator hinzugefügt werden. Zur Auswahl stehen jedoch lediglich ein High/Low- A | Anhang 149 Generator sowie ein Sequence-Generator. Für den High/Low-Generator müssen Informationen zu der Tabelle angegeben werden, in der die Daten zur Schlüsselerzeugung gespeichert werden sollen. Dazu gehört der Tabellenname, die Spalte mit den Namen der Primärschlüsselspalten (Key Column), die Spalte mit den verfügbaren Schlüsselwerten (Value Column) sowie eine Grab-Size. Wird Global markiert, so werden über alle Tabellen global eindeutige Werte erzeugt. Für jede Klasse werden mindestens die Zieltabelle (Map-To Table), das Java-Feld mit der Identität (Identity) sowie der Key-Generator angegeben. Für jedes Attribut muß der Spaltenname und JDBC-Datentyp angegeben werden, optional auch die Namen der get-/set-Methoden für jedes Attribut. Für Collections wird im Auswahlfeld Collection die Art der Collection angegeben, im Feld Type dann der Datentyp der in der Collection enthaltenen Elemente. Das Feld Column Name muß bei Collections gelöscht werden, stattdessen wird im Feld Many Key die Tabellenspalte der referenzierenden Tabelle angegeben, die den Fremdschlüssel zu dieser Klasse enthält. Trotz des sehr einfachen Aufbaus erleichtert das Programm die Erstellung der MappingInformationen. Der Anwender kann alle notwendigen Angaben machen, sofern dies nicht schon von JDOMapper erledigt wurde. Das Programm übernimmt die Validierung und erzeugt außerdem eine XML-Datei, die dann sofort in der Ziel-Applikation eingesetzt werden kann. Abbildungsverzeichnis 2.1 2.2 Relationenmodell vs. Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . Komponenten von J2SE und J2EE . . . . . . . . . . . . . . . . . . . . . . . . 15 17 3.1 3.2 3.3 3.4 3.5 3.6 Bestandteile einer EJB . . . . . . . . . . . . . . . . . . . . . . . . . Lebenszyklus einer Stateful Session Bean (nach [BG02], [EK02]) Lebenszyklus einer Stateless Session Bean (nach [BG02], [EK02]) Lebenszyklus einer Entity Bean (nach [BG02], [EK02]) . . . . . . Prinzip einer Session Fassade . . . . . . . . . . . . . . . . . . . . Lebenszyklus einer Message Driven Bean (nach [BG02], [EK02]) . . . . . . 24 30 31 32 34 36 4.1 Der Entwicklungsprozeß mit JDO (nach [Mo02]) . . . . . . . . . . . . . . . 54 5.1 5.2 5.3 Allgemeine Architektur eines O/R-Mappers . . . . . . . . . . . . . . . . . TopLink Mapping Workbench . . . . . . . . . . . . . . . . . . . . . . . . . . Unit of Work (nach [OTT02]) . . . . . . . . . . . . . . . . . . . . . . . . . . 71 91 94 6.1 6.2 Caché-Architektur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 Die ODMG-Anwendungsarchitektur . . . . . . . . . . . . . . . . . . . . . . 111 7.1 7.2 7.3 Graphische Darstellung der Meßwerte für das Lazy Loading . . . . . . . . 121 Graphische Darstellung der Meßwerte für das vollständige Laden . . . . . 122 Graphische Darstellung der Meßwerte für das Speichern . . . . . . . . . . 123 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.1 Datenmodell der Beispiel-Anwendung . . . . . . . . . . . . . . . . . . . . . 142 A.2 JDOMapper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 Tabellenverzeichnis 3.1 3.2 3.3 3.4 Spezielle Methoden der Bean-Klasse . . . . . . . . . . . . . . Überblick über Enterprise JavaBeans (nach [BG02], [EK02]) . Zuordnung von Bean-Methoden zu Datenbank-Operationen Operatoren und Ausdrücke in EJB-QL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 29 37 42 4.1 Operatoren in JDOQL (nach [Ro03]) . . . . . . . . . . . . . . . . . . . . . . 53 7.1 7.2 7.3 Meßwerte für das Laden von Objekten (Lazy Loading) . . . . . . . . . . . 121 Meßwerte für das vollständige Laden von Objekten . . . . . . . . . . . . . 122 Meßwerte für das Speichern von Objekten . . . . . . . . . . . . . . . . . . . 123 8.1 Kurzübersicht O/R-Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . 126 Literaturverzeichnis [AB+89] M. Atkinson, F. Bancilhon et al.: The object-oriented database system Manifesto. 1989 [BG02] Martin Backschat, Otto Gardon: Enterprise Java Beans. Spektrum Akademischer Verlag, 2002 [CK03] Claus Kerpen: Transaction Management in der J2EE, Teil 1. Javamagazin, 9/2003 [DP02] S. Denninger, I. Peters: Enterprise JavaBeans 2.0. Addison-Wesley, 2002 [EJB2] Enterprise JavaBeans Specification, Version 2.0. Sun Microsystems, Inc., 2001 [EK02] A. Engel, A. Koschel, R. Tritsch: J2EE kompakt. Spektrum Akademischer Verlag, 2002 [GK03] Guido Krüger: Handbuch der Java-Programmierung. Addison-Wesley, 2003 [H2RD] Hibernate2 Reference Documentation, Version 2.0.3 [HS00] Andreas Heuer, Gunter Saake: Datenbanken - Konzepte und Sprachen. MITPVerlag, 2000 [ISO99] ANSI/ISO/IEC International Standard (IS) Database Language SQL, ISO/IEC 9075, September 1999 [JB97] JavaBeans. Sun Microsystems, Inc., 1997 [JDOSp] Java Data Objects JSR 12, Version 1.0. Sun Microsystems, Inc., 2002 [JS01] Avedal, Ayers et al.: JSP professionell. MITP-Verlag, 2001 [JSP03] JavaServer Pages Specification, Version 2.0. Sun Microsystems, Inc., 2003 [JSS03] Java Servlet Specification, Version 2.4. Sun Microsystems, Inc., 2003 [Mc01] B. McLaughlin: Java und XML. O’Reilly, 2001 [Mo02] P. Monday: Hands On Java Data Objects, IBM developerWorks Online Tutorial, 2002 156 Literaturverzeichnis [OTS02] Oracle9iAS TopLink Getting Started, Release 2 (9.0.3). Oracle Corporation, 2002 [OTT02] Oracle9iAS TopLink Tutorials, Release 2 (9.0.3). Oracle Corporation, 2002 [Ro03] R. Roos: Java Data Objects. Addison-Wesley, 2003 [Se01] M. Seeboerger-Weichselbaum: Das Einsteigerseminar XML. Verlag Moderne Industrie Buch, 2001 Selbständigkeitserklärung Ich erkläre hiermit, daß die vorliegende Arbeit von mir selbst und ohne fremde Hilfe erstellt wurde. Alle benutzten Quellen sind im Literaturverzeichnis aufgeführt. Diese Arbeit hat in gleicher oder ähnlicher Weise noch keinem Prüfungsausschuß vorgelegen. Brandenburg, den 08.01.2004