Analyse und praktischer Vergleich von neuen Access

Werbung
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
Herunterladen