Unterstützung objektrelationaler Konzepte durch JDBC und SQLJ

Werbung
Unterstützung objektrelationaler Konzepte
durch JDBC und SQLJ
Semesterarbeit im Fach Informatik
vorgelegt
von
Jussi Prevost
Borrweg 60
8055 Zürich
Matrikelnummer 95-665-915
Angefertigt am
Institut für Informatik
der Universität Zürich
Prof. Klaus R. Dittrich
Betreuer: Dr. Andreas Geppert
Abgabe der Arbeit: 31. März 2003
Inhaltsverzeichnis
1.
2.
Einleitung ........................................................................................................................... 3
Konzepte............................................................................................................................. 3
2.1.
Objektorientiertes Modell .......................................................................................... 3
2.1.1.
Objekte und Klassen........................................................................................... 4
2.1.2.
Vererbung........................................................................................................... 4
2.1.3.
Polymorphie ....................................................................................................... 6
2.2.
Relationales Modell.................................................................................................... 8
2.2.1.
Theoretisches Modell ......................................................................................... 8
2.2.2.
Umsetzung.......................................................................................................... 9
2.3. Unterschiede zwischen den Modellen...................................................................... 10
3. Objektrelationale Datenbanken ........................................................................................ 11
3.1.
SQL:99-Standard...................................................................................................... 11
3.1.1.
Neuerungen ...................................................................................................... 12
3.1.2.
Anfragen und Manipulationen ......................................................................... 14
3.2.
Bewertung ................................................................................................................ 15
4. Datenbanken und Java...................................................................................................... 15
4.1.
JDBC ........................................................................................................................ 15
4.1.1.
Treiber .............................................................................................................. 16
4.1.2.
Verbindungsaufbau .......................................................................................... 17
4.1.3.
SQL-Befehle..................................................................................................... 17
4.1.4.
Transaktionen und Abschottungsgrade ............................................................ 18
A. Transaktionen ....................................................................................................... 18
B. Abschottungsgrade (Transaction Isolation Levels).............................................. 20
4.1.5.
Statement.......................................................................................................... 20
A. Ausführung von SQL-Befehlen ........................................................................... 20
B. Stapelaktualisierungen (Batch Updates) .............................................................. 21
4.1.6.
Ergebnismengen (ResultSet) ............................................................................ 22
A. Bildlauffähige (scrollable) Ergebnismengen........................................................ 22
B. Aktualisierbare Ergebnismengen ......................................................................... 23
4.1.7.
PreparedStatement............................................................................................ 24
4.1.8.
CallableStatement............................................................................................. 24
4.1.9.
Datentypen ....................................................................................................... 25
A. Übersicht .............................................................................................................. 25
B. SQL:99 Datentypen.............................................................................................. 27
4.2.
SQLJ-1: Java Stored Procedures .............................................................................. 27
4.2.1.
Überblick über den SQLJ Standard.................................................................. 27
4.2.2.
Funktionsweise................................................................................................. 28
4.2.3.
Benutzerdefinierte Routinen (Stored Procedures)............................................ 29
4.2.4.
Datenbankzugriffe............................................................................................ 30
4.3.
Bewertung ................................................................................................................ 31
1
5.
Verwendung objektrelationaler Eigenschaften ................................................................ 32
5.1.
Konzeptueller und logischer Entwurf ...................................................................... 33
5.1.1.
Erweiterungen .................................................................................................. 33
5.1.2.
Implementation der Methoden ......................................................................... 34
5.1.3.
Tabellenhierarchien.......................................................................................... 34
5.2.
Abbildung von Objekten zwischen SQL und Java................................................... 35
5.2.1.
Schwache Typisierung ..................................................................................... 35
5.2.2.
Strenge Typisierung ......................................................................................... 36
A. Erstellen benutzerdefinierter Klassen................................................................... 36
B. Abbildung von Methoden..................................................................................... 38
5.2.3.
Werkzeugunterstützung.................................................................................... 38
5.3.
Gesamtbild ............................................................................................................... 40
5.4.
Bewertung ................................................................................................................ 42
5.4.1.
Oracle9i ............................................................................................................ 42
5.4.2.
Typabbildung ................................................................................................... 42
6. Schlussfolgerungen und Ausblick.................................................................................... 42
7. Anhang ............................................................................................................................. 44
7.1.
Carsharing Klassendiagramm .................................................................................. 44
7.2.
Abbildungsverzeichnis ............................................................................................. 45
7.3.
Listing-Verzeichnis .................................................................................................. 46
7.4.
Tabellenverzeichnis.................................................................................................. 47
7.5.
Quellenverzeichnis ................................................................................................... 48
2
1. Einleitung
Datenbanken gehören wohl zu den am weitesten verbreiteten Computeranwendungen. Viele
Anwendungen, von der Adressverwaltung bis zur komplexen CRM-Software (Customer
Relationship Management), müssen Daten für einen späteren Gebrauch abspeichern. Sind die
Anforderungen an die Datenspeicherung hoch (z.B. verteilte Anwendung, viele Benutzer,
hohes (erwartetes) Datenvolumen), dann wird auf ein DBMS zurückgegriffen.
Am weitesten verbreitet sind zur Zeit relationale Datenbanken. In der Softwareentwicklung
haben sich dagegen objektorientierte Programmiersprachen, wie z.B. Java, C++ oder
Smalltalk, durchgesetzt. Zwischen beiden Modellen existiert eine grosse „semantische Lücke“
(impedance mismatch). In der Praxis hat sich herausgestellt, dass der Abbildungsaufwand
zwischen Objekten der Programmiersprache und persistenten Datenelementen der Datenbank
ziemlich gross ist.
Um dem entgegen zu wirken, wurden relationale Datenbanksysteme um objektorientierte
Konzepte zu den objektrelationalen Datenbanksystemen erweitert. Damit wird der Versuch
unternommen, die objektorientierten Konzepte der Programmiersprachen in die bewährten
relationalen Datenbanken zu integrieren. Das soll den gemeinsamen Einsatz erleichtern.
In dieser Arbeit wird untersucht, wie objektrelationale Datenbanksysteme (am Beispiel von
Oracle9i) mit der objektorientierten Programmiersprache Java interagieren. Im Zentrum
stehen die beiden von Java unterstützten DB-Verbindungskonzepte JDBC 2.0 und SQLJ
sowie die Frage ob – und allenfalls wie – sich der impedance mismatch verringern lässt.
Zuerst werden die Unterschiede zwischen relationalem und objektorientierten Modell
herausgearbeitet. Darauf aufbauend werde ich auf den SQL:99-Standard eingehen, dem
Standard für objektrelationale DBS. Im vierten Abschnitt geht es um die Anbindung von
Datenbanken an Java (als Vertreter der objektorientierten Programmiersprachen). Im letzen
Abschnitt gehe ich dann noch auf praktische Erfahrungen ein. Die praktischen Erfahrungen
basieren dabei auf einer Carsharing-Anwendung (vgl. dazu Geppert 403).
2. Konzepte
Als zentrale Frage dieser Arbeit soll die Integration objektorientierter Konzepte in das
relationale Modell diskutiert werden. In diesem Kapitel werde ich die wichtigsten
Eigenschaften des objektorientierten und des relationalen Modells herausarbeiten und
anschliessend die Unterschiede aufzeigen.
2.1. Objektorientiertes Modell
Bisher gibt es kein allgemein anerkanntes, formales objektorientiertes Modell, das in objektorientierten Systemen, Datenbanken ebenso wie Programmiersprachen, umgesetzt wird.
Daraus resultiert, dass die Konzepte in den verschiedenen Systemen unterschiedlich
umgesetzt werden. Das hier verwendete Modell orientiert sich an den objektorientierten
Programmiersprachen, insbesondere an Java.
3
2.1.1. Objekte und Klassen
Ein Objekt ist eine (vereinfachte) Abbildung eines Gegenstandes aus der Umwelt. So besitzt
ein Auto eine Modellbezeichnung, es hat eine Farbe, eine bestimmte Anzahl von Türen und es
kann benutzt werden. Viele gleichartige Objekte, die sich nur durch unterschiedliche Werte
(Zustände) unterscheiden, aber von der Struktur und vom Verhalten her sich entsprechen,
werden in einer Klasse zusammengefasst. Eine Klasse definiert auch gleich einen neuen
Datentyp im Typsystem. Ein Objekt wird auch als (konkrete) Instanz einer Klasse bezeichnet.
Die Eigenschaften eines Objekts werden in Instanzvariablen festgehalten. Diese Variablen
werden als Klassen- oder Membervariablen bezeichnet. Sie können sowohl aus einfachen
Datentypen bestehen als auch aus vorhandenen Klassen.
Neben Eigenschaften kann ein Objekt auch ein gewisses Verhalten besitzen. Das Verhalten
wird durch Methoden definiert. Auch Methoden werden innerhalb der Klassendefinition
angelegt und haben Zugriff auf alle Variablen des Objekts. Methoden sind das Pendant zu den
Funktionen anderer Programmiersprachen, arbeiten aber immer mit den Variablen des
aktuellen Objekts.
Zudem besitzt jedes Objekt eine eigene Identität (object identifier, OID), die es von allen
anderen Objekten unterscheidet. Die OID ist unabhängig vom Zustand des Objekts. So
können zwei Objekte, welche den gleichen Zustand haben, trotzdem unterschieden werden.
Als Beispiel wollen wir die einfache Klasse Auto erstellen, welche die drei Variablen Name,
Erstzulassung und Leistung, und eine Methode zur Berechnung des Alters (in Anzahl Jahren)
besitzt.
public class Auto {
public String name;
public int erstZulassung;
public int ps;
public int alter() {
Calendar cal = Calendar.getInstance();
int aktuellesJahr = cal.get(Calendar.YEAR);
return aktuellesJahr - erstZulassung;
}
}
Listing 2-1: Eine einfache Klassendefinition
Um die Klassenvariablen vor unbefugten Zugriff zu schützen, werden diese gegenüber der
Umwelt meistens versteckt. Lese- und Schreibzugriff auf die Variablen erfolgt dann über getund set-Methoden. Man spricht von Datenkapselung. Dies hat auch den Vorteil, dass z.B. vor
dem Ändern einer Variablen eine Konsistenzprüfung gemacht werden kann. In Java könnte
man die Klassenvariablen mit dem Attribut private deklarieren um sie zu verstecken
(anstatt wie in Listing 2-1 mit public).
2.1.2. Vererbung
Ein weiteres wichtiges Merkmal von objektorientierten Sprachen ist das der Vererbung, also
der Möglichkeit, Eigenschaften vorhandener Klassen in neuen Klassen wiederzuverwenden.
Dabei wird unterschieden zwischen einfacher Vererbung, bei der eine Klasse von genau einer
anderen Klasse abgeleitet werden kann, und Mehrfachvererbung, bei der eine Klasse von
4
mehr als einer anderen Klasse abgeleitet werden kann. Neben den Instanzvariablen erbt eine
abgeleitete Klasse auch die Methoden ihrer Vaterklasse. Natürlich können auch neue
Methoden definiert werden. Die geerbten Methoden können aber auch überschrieben werden.
Man spricht dann von Methodenüberlagerung.
Abbildung 1 zeigt eine einfache Vererbung am Beispiel von Golfspielern. Man bezeichnet
dies als Klassenhierarchie. Ein Golfer besitzt folgende Eigenschaften: Er muss Mitglied in
einem Golfclub sein, dann werden die Anzahl gespielter Runden, Schläge sowie die Siege
gespeichert. Die Methode getAvgScorePerRound liefert die durchschnittlich benötigte
Anzahl Schläge pro Runde zurück. Im Golfsport unterscheidet man zwischen Amateuren, die
diesen Sport als Freizeitvergnügen betreiben, und solchen, die Golf als Beruf ausüben.
Abbildung 1: Beispiel für eine einfache Vererbung
Die beiden Subklassen unterscheiden sich von ihrer Superklasse durch die zusätzlichen
Variablen und Methoden. Ein Amateur besitzt ein Handicap. Das Handicap für eine Runde ist
vom jeweiligen Golfplatz abhängig und kann über die Methode getPlayingHCP abgefragt
werden. Da ein Amateur kein Preisgeld kassieren darf, führt er eine Liste mit all seinen
gewonnen Naturalpreisen. Ein Berufsspieler besitzt dagegen kein Handicap (mehr). Dafür
benötigt er eine Spielberechtigung für eine bestimmte Veranstaltung, welche durch die
Variable type festgelegt wird. Diese läuft nach einer gewissen Dauer ab. Das Ende kann
durch die Methode expires abgefragt werden. Die gewonnen Preisgelder werden in der
Variable prizeMoney gespeichert.
Die Klasse Golfer kann nicht nur instanziert werden, eine Variable vom Typ Golfer kann
nämlich auch Objekte vom Typ Amateur oder Professional speichern. Dies wird als
Polymorphie bezeichnet (mehr dazu in 2.1.3).
Golfer golfer = new Golfer();
Amateur amateur = new Amateur();
Professional pro = new Professinal();
golfer = amateur;
golfer = pro;
Listing 2-2: Polymorphie
Bei einer Mehrfachvererbung kann z.B. eine Klasse MidAmateur von Amateur und
Professional abstammen. Diese erbt dann die Eigenschaften von beiden Klassen und
5
kann auch die Methoden überschreiben (z.B. könnte die Methode getPlayingHCP immer
0 zurückgeben). Dabei kann es zu verschiedenen Problemen kommen, unter anderem wenn
Membervariablen oder Methoden in den Vaterklassen den gleichen Namen haben, aber auch
die ganze Klassenhierarchie wird komplizierter und ist nicht mehr so einfach zu verwalten. In
unserem Fall gibt es aber keine Probleme. Da Java1 und SQL-99 auf Mehrfachvererbung
verzichten, gehe ich darauf nicht weiter ein.
2.1.3. Polymorphie
Oft werden Gemeinsamkeiten verwandter Klassen in einer gemeinsamen Superklasse erfasst.
Man spricht von Faktorisierung. In der Carsharing-Anwendung können wir die
Gemeinsamkeiten der verschiedenen Arten von Mitgliedern in einer Oberklasse Member
zusammenfassen. Die Klasse umfasst allgemeine Eigenschaften wie die Mitgliedsnummer
und
ein
Passwort
(vgl.
Listing
2-3
und
1
In Java wurde mit den Interfaces ein Ersatzkonstrukt geschaffen, das es Klassen erlaubt
Methodendeklarationen von mehr als einer Klasse zu erben (vgl. z.B. Krüger 177).
6
Abbildung 9 im Anhang). Die unterschiedlichen Mitgliedschaften können wir mit den zwei
Subklassen CoopMember und CarsharingMember weiter spezialisieren. Erstere sind
Genossenschaftsmitglieder, die Anteile an der Genossenschaft besitzen. CarsharingMember sind ‚normale’ Mitglieder, von denen es weitere Subklassen gibt: persönliche
Mitglieder, Firmenmitglieder und Schnuppermitglieder. Im Wesentlichen unterscheiden sich
diese Mitgliedschaften durch die unterschiedliche Preisgestaltung.
7
public abstract class Member {
int membernr;
String password;
public Member() { }
public abstract String getName();
public abstract Address getAddress();
}
Listing 2-3: Eine abstrakte Klasse
Die Methoden getName und getAddress haben noch keine Implementierung, man spricht
von abstrakten Methoden. Eine Klasse, welche mindestens eine abstrakte Methode hat, wird
als abstrakte Klasse bezeichnet. Ebenfalls als abstrakte Klasse definieren wir die Klasse
CarsharingMember.
Die Klassen CoopMember, PersonMember, CompanyMember und SnoopyMember
erstellen wir als konkrete Klassen. Um eine konkrete Klasse von einer abstrakten abzuleiten,
müssen die zur Beschreibung der Objekte erforderlichen Variablen hinzugefügt und die
abstrakten Methoden implementiert werden. Bestehende Methoden können durch Überladen
angepasst werden. Für die Klasse PersonMember könnte dies so aussehen (nicht
vollständig):
public class PersonMember extends CarsharingMember {
private Person person;
boolean hadAccident;
public String getName() {
return person.getName();
}
public Address getAddress() { return person.getAddress(); }
public double getYearlyFee() { … }
}
Listing 2-4: Abbleitung einer konkreten Klasse von einer abstrakten
Es macht wenig Sinn, Objekte von abstrakten Klassen zu erzeugen, weil beispielsweise ein
Objekt der Klasse Member keine Jahresgebühr berechnen kann (und zudem ein gedankliches
Hilfskonstrukt ist). Sinnvoll ist dagegen die Deklaration von Variablen des Typs Member.
Eine solche Variable kann später Referenzen auf beliebige Subtypen von Member enthalten,
auch solche die noch gar nicht definiert worden sind. Man spricht von Polymorphie. In
Listing 2-5 werden alle Mitglieder mit ihrer Mitgliedernummer und ihrem Namen
ausgedruckt, wobei wir annehmen, dass die Methode getMembers alle Mitglieder in einem
Array zurückgibt:
Member[] member = getMembers();
for (int i=0; i<member.length; i++)
System.out.println(member.getMemberNr() + “ “
+ member.getName());
Listing 2-5: Die Verwendung von abstrakten Klassen
Vielfach besitzen konkrete Klassen noch weitere Methoden, die in der abstrakten Klasse
fehlen. Zum Beispiel besitzt die Klasse PersonMember noch eine Methode zur Berechnung
8
des Alters. Damit wir diese Methode aufrufen können, müssen wir eine Typumwandlung
durchführen. Würden wir sie auf dem Member-Objekt ausführen, so würde dies zu einem
Fehler führen. Eine Typumwandlung könnte z.B. so aussehen:
Member[] member = getMembers();
for (int i=0; i<member.length; i++) {
System.out.print(member.getName());
if (member[i] instanceof PersonMember) {
PersonMember pm = (PersonMember)pm[i];
System.out.print(“(Alter = “ + pm.getAge());
}
System.out.println();
}
Listing 2-6: Beispiel zur Typumwandlung
Zur Laufzeit des Systems werden Objekte frei im Speicher erzeugt. Ein Objekt ist dabei
immer ein Exemplar der erzeugenden Klasse. Soweit Mehrfachvererbung vom System nicht
unterstützt wird, besitzt ein Objekt somit auch genau einen Typ, der über die gesamte Lebenszeit des Objekts erhalten bleibt und sicht nicht verändert. Objekte können also zur Laufzeit
ihre Typ- oder Klassenzugehörigkeit nicht wechseln.
Wie schon erwähnt, bekommt ein Objekt bei seiner Erzeugung auch eine eigene Identität, die
ebenfalls über die gesamte Lebensdauer unverändert bleibt. Über diese OID werden die
Objekte auch eindeutig referenziert. So ist es möglich, dass Variablen, welche auf Objekte
verweisen, ‚nur’ die OID als Wert speichern und nicht das gesamte Objekt (dieses bleibt frei
im Speicher).
Tabelle 1 fasst die Eigenschaften des objektorientierten Modells nochmals zusammen.
9
Objektorientiertes Modell
Verhaltensorientierte Modellierung
Erweiterbares Typsystem
Vererbung
Substituierbarkeit
Polymorphie
Objekte mit unveränderlichem Typ
OID unabhängig vom Zustand
Verknüpfung durch Referenzen
Objekte frei im Speicher
Zugriff durch Methodenaufrufe
Verarbeitung einzelner Objekte
Zustand der Objekte gekapselt
Methoden an Objekte gebunden
Tabelle 1: Eigenschaften des objektorientieten Modells
2.2. Relationales Modell
2.2.1. Theoretisches Modell
Relationale Datenbanken basieren auf dem Relationenmodell, das erstmals 1970 von Ted
Codd beschrieben wurde. Die Grundidee des relationalen Modells besteht darin, eine
Datenbasis zur Verfügung zu stellen, aus der dann Aussagen abgeleitet werden können. Das
Modell basiert auf der Grundlage der Mengentheorie.
Daten werden in Relationen gespeichert. Eine Relation wird auch als Tabelle bezeichnet und
als solche dargestellt. Dabei nimmt jede Spalte ein Attribut auf und jede Zeile enthält einen
Datensatz (Tupel). Eine Relation ist eine konkrete Füllung der Tabelle mit Daten (vgl.
Abbildung 2).
STUDENT
Name
Matrikelnr
Fakultät
Semester
Fredi Manser
89-456-456
Phil I
27
Hans Muster
99-123-123
WI
7
Tamara Meier
98-646-655
oec
9
Abbildung 2: Die Relation "Person" mit vier Attributen und fünf Tupeln
Attribute einer Relation können aber nur atomare Werte aufnehmen (1. Normalform). So ist
es beispielsweise nicht möglich, in einem Attribut ‚Telefonnr’ eine Liste von
Telefonnummern zu speichern.
10
Ein weiteres wichtiges Element sind die Schlüssel. Ein Schlüssel dient dazu einzelne Tupel zu
unterscheiden. Er kann aus einem oder mehreren Attributen bestehen. Wenn dieser Schlüssel
eineindeutig ist, d.h. es wird genau ein Tupel in der Relation identifiziert, dann wird dieser als
Primärschlüssel bezeichnet. Für die Relation ‚Student’ aus Abbildung 2 könnte man das
Attribut ‚Matrikelnr’ als Primärschlüssel festlegen. Die Matrikelnummer ist nämlich
eineindeutig (zumindest in der Schweiz). Mit Primärschlüsseln lassen sich nun auch
Beziehungen zwischen Relationen herstellen. Dazu werden einfach die gleichen Attribute,
welche den Primärschlüssel einer Relation ausmachen, in einer zweiten Relation definiert.
Dort bezeichnet man diese Attributmenge als Fremdschlüssel. Im Fremdschlüssel werden die
gleichen Werte gespeichert wie im Primärschlüssel. In Abbildung 3 kann man über den
Fremdschlüssel ‚Student’ den Namen des Studenten aus der Relation ‚Student’ (Abbildung 2)
wieder ausfindig machen.
TELEFON
Student
Typ
Nummer
89-456-456
Privat
014651245
89-456-456
Mobil
0761231312
99-123-123
Mobil
0797984578
98-646-655
Privat
017897896
Abbildung 3: Relation ‚Telefon’ mit Fremdschlüssel ‚Student’
Im Operationenteil des Relationenmodells sind generische Operationen festgelegt, mit deren
Hilfe auf die in den Relationen gespeicherten Daten zugegriffen werden kann. Es stehen die
folgenden Operationen zur Verfügung:
•
Die Selektion dient zur Auswahl von Tupeln einer Relation.
•
Die Projektion ist für die Auswahl bestimmter Attribute einer Relation.
•
Natürliche Verbunde (Joins) dienen zum Verknüpfen mehrerer Relationen über ihre
Fremdschlüsselbeziehungen.
•
Mengenoperationen
Als Ergebnis liefern all diese Operationen wiederum eine Relationen und können so beliebig
orthogonal miteinander kombiniert werden. Diese Eigenschaft wird auch als Abgeschlossenheit bezeichnet.
2.2.2. Umsetzung
Das Relationenmodell eignet sich vorzüglich für die Entwicklung von Datenbanken.
Kommerzielle Produkte haben inzwischen eine so hohe Leistungsfähigkeit erreicht, dass
mehrere tausend Anfragen pro Sekunde behandelt werden können und bei gleichzeitigem
Zugriff auch die Konsistenz der Daten gewährt bleibt. Daneben zeichnen sie sich durch ihre
hohe Zuverlässigkeit und Robustheit aus.
Der Zugriff auf Datenbanken wird durch SQL unterstützt, welches von allen Herstellern
unterstützt wird. Theoretisch wäre es möglich die Implementation eines Herstellers durch
diejenige eines anderen zu ersetzen. In der Praxis ist dies aber nicht so, da nicht alle Hersteller
den SQL-Standard auf identische Weise umsetzen.
11
Bei einer konkreten Implementation des Relationenmodells können nicht alle Eigenschaften
des Modells erhalten bleiben. Andererseits können neue Eigenschaften hinzugefügt werden.
Längst nicht alle Einschränkungen einer konkreten Implementierung sind aber auf mangelnde
Eigenschaften des Relationenmodells zurückzuführen. So erfüllt etwa die Anfragesprache
SQL nicht alle Anforderungen des Relationenmodells, da es nicht abgeschlossen ist. So ist es
nicht möglich, alle Operationen beliebig miteinander zu kombinieren.
2.3. Unterschiede zwischen den Modellen
Beide Modelle unterscheiden sich in vielerlei Hinsicht. Man spricht auch von der
‚semantischen Lücke’ (impedance mismatch). Der gemeinsame Einsatz von relationalen
Datenbanken und objektorientierten Programmiersprachen wird dadurch erschwert.
Das objektorientierte Modell ist mehr auf die Entwicklung von Software ausgerichtet, und die
Objekte müssen miteinander kooperieren um ein bestimmtes Verhalten zu modellieren.
Dagegen werden im relationalen Modell Daten gespeichert, welche später abgefragt und
manipuliert werden können.
Im objektorientierten Modell besitzt jedes Objekt eine eigene Identität (OID), welche
unabhängig vom Zustand ist. Die OID ist notwendig, weil sich die Objekte frei im Speicher
befinden und nur so unterschieden werden können. Durch die OID können die Objekte aber
auch miteinander verknüpft werden. Im relationalen Modell befinden sich die Daten innerhalb
von Relationen. Einzelne Daten können nur aufgrund ihres Wertes unterschieden werden.
Damit Datensätze miteinander verknüpft werden können, benötigt man Fremdschlüsselbeziehungen.
Objektorientiertes Modell
Relationales Modell
Verhaltensorientierte Modellierung
Strukturorientierte Modellierung ohne Verhalten
Erweiterbares Typsystem
Typsystem ausserhalb des Modells
Vererbung
Keine Vererbung
Polymorphie
Keine Polymorphie
Objekte mit unveränderlichem Typ
Keine Objekte
OID unabhängig vom Zustand
Keine Identität unabhängig von Werten
Verknüpfung durch Referenzen
Verknüpfung durch Fremdschlüssel
Objekte frei im Speicher
Daten in Relationen strukturiert
Zugriff durch Methodenaufrufe
Zugriff durch relationale Algebra
Verarbeitung einzelner Objekte
Verarbeitung mehrerer Datensätze
Zustand der Objekte gekapselt
Zustand der Daten frei zugreifbar
Methoden an Objekte gebunden
Keine Methoden
Konsistenzsicherung durch Axiome und Methoden
Konsistenzsicherung durch Constraints
Tabelle 2: Unterschiede zwischen dem relationalen und dem objektorientierten Modell
Ein weiterer Unterschied betrifft das Typsystem. Hier sticht vor allem das erweiterbare
Typsystem des objektorientierten Modells hervor (mitsamt Vererbung und Polymorphie),
wogegen sich das relationale Modell dazu nicht äussert, ausser dass Attribute einen Datentyp
12
zugewiesen bekommen. Die kommerziellen relationalen Datenbanken weisen meistens ein
abgeschlossenes Typsystem auf. Vererbung wird nicht unterstützt.
Im objektorientierten Modell wird der Zugriff auf den Zustand eines Objektes über
Methodenaufrufe ermöglicht. Zudem werden die Objekte einzeln bearbeitet. Im relationalen
Modell können die Daten direkt manipuliert werden. Manipulationen werden auf der
gesamten Relation angewendet.
Tabelle 2 fasst diese und weitere Unterschiede nochmals zusammen.
3. Objektrelationale Datenbanken
Im vorhergehenden Kapitel haben wir das Relationale und Objektorientierte Modell sowie
deren Unterschiede kennen gelernt. Die Zusammenarbeit von objektorientierten Programmiersprachen mit relationalen Datenbanken wird vor allem durch folgende fehlende Eigenschaften
des relationalen Modells erschwert:
•
Kein erweiterbares Typsystem
•
Keine Unterstützung von Vererbung und Polymorphie
•
Keine Identität unabhängig von Werten
•
Keine Modellierung von Verhalten möglich
Glücklicherweise lassen sich diese fehlenden Eigenschaften in das relationale Modell
integrieren. Die Grundidee besteht darin, die relationale Grundstruktur durch objektorientierte
Konzepte zu erweitern. Eine objektrelationale Datenbank besteht also immer noch aus einer
Menge von Relationen, erweitert durch Sprachkonstrukte, welche die Definition von
Objekttypen, die Schaffung von Typhierarchien sowie das Speichern von Objekten in
Tabellen erlauben.
Ein theoretisches Modell stammt von C.J. Date und Hugh Darwen. Ihr Modell führt zwar
wichtige objektorientierte Erweiterungen wie erweiterbares Typsystem und Vererbung ein.
Dagegen fehlen weiterhin Objektidentitäten und Referenzen. Zudem wird ein
Vererbungskonzept gewählt, beim dem Objekte ihren Datentyp zur Laufzeit verändern
können. In der Praxis spielt dieses Modell keine Rolle (es gibt denn auch keine
Implementation). Stärkeres Gewicht bei der Umsetzung objektrelationaler Erweiterungen hat
hier der SQL:99-Standard, auf den ich in diesem Kapitel eingehen werde.
3.1. SQL:99-Standard
Durch die Erweiterung relationaler Datenbanken mit objektorientierten Eigenschaften kann
die Kompatibilität zu bisherigen relationalen Datenbanken gewahrt bleiben. So ist es möglich,
die neuen Eigenschaften schrittweise einzuführen, während auf bisherige Datenbestände mit
der herkömmlichen Weise zugegriffen werden kann. Ebenfalls entfällt das Anpassen der
Anwendungen, die auf relationale Datenbanken zugreifen. Ein Wechsel auf ein objektorientiertes Datenbankensystem würde viel mehr Aufwand bedeuten, da alle Daten und alle
Anwendungen auf einen Zeitpunkt hin übernommen und angepasst werden müssten.
13
Inzwischen bieten alle grossen Hersteller objektrelationale Erweiterungen auf Basis des
Standards an.
3.1.1. Neuerungen
Im SQL:99-Standard wurden u.a. folgende wichtige Neuerungen eingeführt, auf die ich in
diesem Abschnitt eingehen werde:
•
die Schaffung von benutzerdefinierten Datentypen (user definied types, UDT) mit
Objektidentität und Referenzen
•
Anbindung von Verhalten (in Form von Methoden) an UDTs
•
typisierte Tabellen
•
Typen- und Tabellenhierarchien
Daneben wurden auch die Basistypen erweitert. So ist es nun möglich, grössere Objekte
(large objects, LOBs) und auch Kollektionen zu schaffen. Ein LOB kann eine sehr lange
(sprachspezifische) Zeichenfolge (CLOB bzw. NCLOB) oder eine Bytefolge (BLOB) sein.
Bei den Kollektionen konnte man sich nur auf eine Minimallösung einigen, nämlich auf
geordnete Kollektionen mit einer im Schema zu definierenden Maximalkardinalität (Array).
Somit kann ein Attribut nun auch mehrere Werte aufnehmen.
Bei den benutzerdefinierten Datentypen unterscheidet man zwischen einfachen und
strukturierten. Die einfachen Datentypen (distinct types) stellen dabei eine spezielle
Verwendung eines vordefinierten Typs dar. So kann man z.B. einen Datentyp „Liter“ und
einen Datentyp „Kilometer“ erstellen, als Spezialisierung der (Gleitkomma-)Zahlen.
Beliebige Ausdrücke der beiden Datentypen lassen sich so nicht mehr miteinander
vergleichen, obwohl sie auf dem gleichen vordefinierten Datentyp beruhen. Diese einfachen
Typen sind in dieser Arbeit nicht von besonderer Bedeutung, wichtiger sind die strukturierten
Datentypen. Deshalb verwende ich ab jetzt den Begriff „benutzerdefinierte Datentypen“ für
diese.
Die strukturierten Datentypen (structered types) sind eine Aggregation von verschiedenen
Datentypen. Sie sind mit den Klassen in der Objektorientierung vergleichbar: an die
Datentypen können Methoden gebunden werden, ihre Instanzen besitzen eine Objektidentität
und lassen sich referenzieren.
OIDs können auf unterschiedliche Weise erstellt werden. Sie können aus Attributen gebildet
oder künstlich generiert werden. Das folgende Beispiel definiert den Typ der Fahrzeuge. Der
Objektidentifikator wird dabei vom System erzeugt.
create type VEHICLE_T as
(…) …
ref is system generated
Listing 3-1: UDT-Definition
Durch die Unterstützung von OIDs ist es möglich, Referenzen auf Elemente darzustellen. Der
Standard führt zu diesem Zweck den Referenztyp (Ref-type) ein, der auch als Wertebereich in
Typdefinitionen verwendet werden kann. Im nächsten Beispiel besitzt VEHICLE_T das
Attribut HOME, das eine Referenz auf eine Mietstation speichern kann.
14
create type VEHICLE_T as (
HOME
ref(LOCATION_T),
…
) …
ref is system generated
Listing 3-2: UDT mit Referenztyp
Typspezifisches Verhalten, d.h. die Anbindung von Methoden an UDTs, wird im SQL:99Standard ebenfalls unterstützt. Anders als in objektorientierten Modellen, erfolgt die
Implementierung der Methoden getrennt von der Definition. Die Definition, Methodensignatur genannt, beinhaltet den Methodennamen, Parameter und einen Ergebnistyp. Die
Implementierung einer Methode kann in einer Reihe von Sprachen erstellt werden
(prozedurales SQL, Java, C, …). Dabei muss aber bereits in der Signatur angegeben werden,
welche Sprache für die Umsetzung benutzt wird. Vergleiche dazu auch Kapitel 4.2.3. Neben
den benutzerdefinierten Methoden werden für einen UDT auch automatisch Methoden
erzeugt. Dabei werden für jedes Attribut eine Methode zum Lesen und eine zum Ändern des
Wertes erzeugt.
UDTs können an zwei verschiedenen Stellen benutzt werden. Einerseits können sie als
Datentyp von Attributen innerhalb einer Tabelle verwendet werden – so wie bisher. Andererseits lassen sich typisierte Tabellen erstellen. Eine typisierte Tabelle ist eine Tabelle, welche
Objekte von einem bestimmten Typ (und seinen Subtypen) speichern kann. Ihre Definition
ergibt sich dabei aus dem Datentyp. Dabei wird für jedes Attribut des Datentyps eine Spalte in
der Tabelle erzeugt. Hinzu kommt eine zusätzliche Spalte für den OID. Dieser Wert wird
beim Erstellen des Objektes erzeugt und kann danach nicht mehr verändert werden. Das
folgende Beispiel erstellt eine Tabelle über dem UDT VEHICLE_T, der OID erhält den
Namen VID.
create table VEHICLE of VEHICLE_T
ref is VID system generated
Listing 3-3: Tabellendefinition
Damit der Zugriff auf Referenzen funktioniert, muss der Wertebereich (scope) angegeben
werden. Dies kann entweder schon beim Erstellen der Tabelle oder, wie im folgenden
Beispiel, in einer Änderungsanweisung geschehen.
alter table VEHICLE alter HOME add scope LOCATION
Listing 3-4: Festlegen des Wertebereichs einer Referenz
Vererbung und Spezialisierung werden in SQL:99 auch angeboten. Sogar die Bildung von
abstrakten Typen ist möglich. Strukturierte Typen können in Typhierarchien angeordnet
werden. Ein Subtyp erbt dabei alle Eigenschaften und das Verhalten des Supertyps. Neue
Eigenschaften und Methoden können hinzugefügt werden, geerbte Methoden können auch
überschrieben werden.
create type COMBI_T under VEHICLE_T as (
Loadingvolume int)
overriding instance method getPricePerKilometer()
returns double
Listing 3-5: Spezialisierung im SQL:99-Standard
15
Neben Typhierarchien werden auch Tabellenhierarchien unterstützt. Dabei muss bei der
Relationendefinition angegeben werden, dass die neue Tabelle eine Subtabelle einer bereits
existierenden Tabelle ist (vgl. Listing 3-6). Dabei muss die Typhierarchie streng eingehalten
werden, d.h. es kann keine Stufe übersprungen werden. Eine Subtabelle stellt somit eine
Teilmenge ihrer Obertabelle dar.
create table COMBI of COMBI_T under VEHICLE
Listing 3-6: Definition einer Subtabelle
3.1.2. Anfragen und Manipulationen
Für Anfragen können wir auch in SQL:99 immer noch die select-from-where-Klausel
benutzen. Für die Benutzung der neuen objektrelationalen Eigenschaften wurden zusätzlich
neue Operationen eingeführt.
Der Zugriff auf Attribute einer typisierten Tabelle bleibt gleich. Möchte man auf Attribute
eines referenzierten Tupels zugreifen, kann dies mit Hilfe des Dereferenzierungsoperators (->
oder DEREF) tun. Der Deref-Operator wandelt die Referenz wieder in ein Objekt zurück. Auf
diesem Objekt kann man auf die Attribute zugreifen oder auch Methoden aufrufen. Das
folgende Beispiel beinhaltet eine Dereferenzierung.
select LICENSEPLATE, HOME->NAME
from VEHICLE
Listing 3-7: Dereferenzierung in Anfragen
Möchte man anstelle des Namens die Adresse der Heimbasis, so kann dies so aussehen:
select LICENSEPLATE, HOME->ADDRESS.TOSTRING()
from VEHICLE
Listing 3-8: Beispiel für einen Pfadausdruck
Daneben muss auch die Vererbung bzw. die Tabellenhierarchie in die Syntax und Semantik
von Anfragen miteinbezogen werden können. Eine Anfrage an eine typisierte Obertabelle
liefert grundsätzlich immer alle Tupel zurück. Eine Anfrage an eine Subtabelle liefert dagegen
nur diejenigen Tupel zurück, welche auf dem Typ der Tabelle basieren (und dessen
Subtypen). Möchte man eine Anfrage an eine Tabelle stellen, welche nur Tupel von genau
dem Typ der Tabelle zurückliefert, so benötigt man den Operator ONLY. Die folgende
Abfrage würde nur Tupel zurück liefern, welche vom Typ VEHICLE_T sind.2
select LICENSEPLATE from only (VEHICLE)
Listing 3-9: only Operator
Auch die Einfüge-Operationen werden immer noch in der insert-into-Struktur durchgeführt.
Zum Einfügen eines Objekts in einer Tabelle, muss zuerst das Objekt erzeugt werden. Dazu
stellt die Datenbank zu jedem Datentyp automatisch einen Konstruktor bereit. Der Aufruf
liefert ein leeres Objekt dieses Typs zurück. Die Attributwerte können mittels der schon oben
erwähnten Änderungsmethoden gesetzt werden. Dasselbe gilt auch für Änderungen mittels
der update-Anweisung.
2
In meinem Schema würde die Abfrage keine Tupel zurückliefern, da ich Vehicle_T als abstrakten Typ definiert
habe. Und diese können ja nicht instanziert werden!
16
3.2. Bewertung
Der SQL:99-Standard integriert objektorientierte Erweiterungen in die bestehende relationale
Datenbanksprache SQL. Im Mittelpunkt dieser Erweiterungen stehen das erweiterbare
Typsystem sowie die Wahrung der Abwärtskompatibilität.
Mit der Einführung benutzerdefinierter Typen wird es möglich, objektorientierte
Eigenschaften wie Objektidentität, Referenzen, Vererbung und typspezifisches Verhalten
anzubieten. Durch diese Neuerungen und auch durch die Einführung von Kollektionen ist es
möglich, reale Sachverhalte noch besser in einem DB-Schema abzubilden.
Die Verwendung von Zugriffspfaden (über Referenzen) ermöglicht ein einfacheres
Navigieren durch Tabellen. So entfällt das Verknüpfen von Tabellen durch Fremdschlüsselbeziehungen bzw. durch den JOIN-Operator.
Aufgrund der relationalen Strukturen sind die Objekte aber immer noch in Tabellen
strukturiert. Objekte können zwar temporär im Speicher sein, aber die Auswahl und Änderung
von Objekten erfolgen über die herkömmliche SQL-Syntax. Zudem beziehen sich
Änderungen immer noch auf die gesamte Tabelle, sofern die Auswahl nicht eingeschränkt
wird.
Vererbung und Polymorphie werden ebenfalls unterstützt. Im Gegensatz zum
objektrelationalen Modell von Date und Darwen besitzen die Objekte aber einen
unveränderlichen Datentyp. Auch die Unterstützung von abstrakten sowie „finalen“ Typen
wird angeboten.
Der SQL:99-Standard ist für mich ein gelungener Mittelweg zwischen einem rein relationalen
und einem rein objektorientierten Modell. Dabei ist es gelungen, wichtige objektorientierte
Konzepte einzuführen, und dabei die relationalen Strukturen zu erhalten. Wie diese
Neuerungen dazu beitragen können, den impedance mismatch zwischen DB und
Programmiersprache zu verringern, wird in den nächsten beiden Kapiteln behandelt.
4. Datenbanken und Java
Benötigt eine Java-Applikation eine Datenbankanbindung, so kann diese mit JDBC realisiert.
Anwendungen haben dabei vollkommen freie Hand, um auf (verteilte) Datenbankserver
zuzugreifen. Für die Realisierung der Anwendung spielt es keine Rolle, ob eine
herkömmliche Client-/Server-Architektur oder eine Multi-Tier-Architektur verwendet wird.
4.1. JDBC
JDBC besteht aus zwei Schichten. Die obere Schicht ist die eigentliche JDBC–API. Diese
kommuniziert mit der unteren Schicht, dem JDBC-Treibermanager-API, und sendet dieser die
verschiedenen SQL-Anweisungen. Der Treibermanager sollte mit den verschiedenen Treibern
von Drittherstellern kommunizieren, welche die eigentliche Verbindung zur Datenbank
herstellen.
Der Programmierer muss sich nicht um herstellerspezifische Eigenheiten eines DBMS
kümmern. Diese Unterschiede übernimmt ein JDBC-Treiber vom jeweiligen Datenbankhersteller. Abbildung 4 gibt einen Überblick.
17
Java Applets/
Applications
JDBC-API
JDBC Driver Manager or
DataSource Object
JDBC/ODBC
Bridge Driver
Partial Java
JDBC Driver
Pure Java
JDBC Driver
ODBC
DB Client
Libraries
DB Middleware
Pure Java
JDBC Driver
DB Client
Libraries
DB-Server
DB-Server
DB-Server
DB-Server
Abbildung 4: Die Architektur von JDBC
4.1.1. Treiber
Wie aus Abbildung 4 ersichtlich ist, werden vier verschiedene Typen von Treibern unterschieden, die zumindest den SQL-92 Entry Level unterstützen müssen.
•
Ein Typ 1 Treiber (JDBC-ODBC Brücke) benützt die ODBC-Schnittstelle, um mit der
Datenbank zu kommunizieren. Ein solcher Treiber wird im JDK angeboten.
Allerdings muss auf allen Clients eine ODBC-Schnittstelle installiert sein, was nicht
immer der Fall ist bzw. möglich wäre.
•
Ein Typ 2 Treiber (Native API Treiber) ist zum Teil in Java geschrieben. Für eine
Installation werden aber auch noch proprietäre Bibliotheken benötigt.
•
Ein Typ 3 Treiber (JDBC-Net Treiber) ist eine reine Client-Bibliothek in Java, die
über ein datenbankunabhängiges Protokoll Datenbankanforderungen an eine Serverkomponente schickt, die wiederum die Anforderungen in ein datenbankspezifisches
Protokoll umsetzt. Die Client-Bibliothek ist unabhängig von der eigentlichen
Datenbank und vereinfacht somit die Entwicklung.
•
Ein Typ 4 Treiber (Native-Protokoll Treiber) ist auch eine reine Java-Bibliothek, die
JDBC-Anforderungen direkt in ein datenbankspezifisches Protokoll übersetzt. Der
Treiber kommuniziert aber ohne Zusatzkomponenten mit dem DB-Server
Die meisten Datenbankhersteller stellen mit ihren Datenbankensystem entweder einen Typ 3
oder Typ 4 Treiber zur Verfügung. Solche Treiber eignen sich hervorragend für verteilte
Anwendungen, z.B. wenn der Benutzerzugriff über ein Applet erfolgt. Typ 1 und Typ 2
Treiber werden oft für das Testen verwendet oder in Unternehmensnetzwerken, wo die
zusätzliche Installation der notwendigen Bibliotheken kein grosses Problem sein sollte.
18
4.1.2. Verbindungsaufbau
Über die Klasse java.sql.DriverManager wählt man einen Datenbanktreiber aus und
erstellt eine neue Datenbankverbindung. Der Treibermanager muss einen Treiber aber erst
registrieren, bevor er ihn aktivieren kann.
Dazu gibt es zwei Möglichkeiten:
•
Der Treiber wird manuell registriert, indem seine Klasse geladen wird. Zum Beispiel
für einen JDBC/ODBC-Brückentreiber:
Class.forName(“sun.jdbc.JdbcOdbcDriver”);
•
Das Programm kann die Systemeigenschaft jdbc.drivers auf einen (oder
mehrere) Treiber setzen. Zum Beispiel setzt die Zeile
System.setProperty(“jdbc.drivers”,“org.gjt.mm.mysql.Driver”)
die Systemeigenschaften auf den MySQL-Treiber.
Ist der Treiber registriert, kann man mit der Methode getConnection ein ConnectionObjekt erstellen. Diese Methode benötigt als Parameter im Minimum die Netz-URL der
Datenbank, welche sich an folgende Form zu halten hat.
jdbc:Unterprotokoll:andere_Angaben
Weitere Parameter sind der Anmeldenamen und das Kennwort zur Anmeldung an die
Datenbank oder ein Eigenschafts-Objekt. Das Eigenschafts-Objekt muss zumindest die
Attribute Anmeldenamen und Kennwort beinhalten, kann aber noch weitere (Datenbankabhängige) Attribute enthalten.
Connection con = DriverManager.getConnection(
“jdbc:mysql://localhost/snow”,“user”,“password”);
Connection con = DriverManager.getConnection(
“jdbc:oracle:thin:@localhost:1521:snow2”,“user”,“password”);
Listing 4-1: Beispiele für die Erstellung von DB-Verbindungen
Das erste Beispiel aus Listing 4-1 stellt eine Verbindung zur MySQL-Datenbank snow auf
localhost her. Das Zweite verbindet den Benutzer über Port 1521 von localhost mit der
Oracle-Datenbank snow2. Mit diesem Verbindungsobjekt lassen sich nun Abfragen und
Aktionen ausführen.
Wird die Verbindung nicht mehr benötigt, empfiehlt die JDBC-API Dokumentation die
Verbindung explizit mit dem Aufruf der Methode close zu schliessen. Der Garbage
collector kann bei Verwendung von externen Ressourcen (was ja der Zugang via JDBC zu
einem DBMS ist) nicht den Status dieser Ressourcen feststellen. Dann können nämlich
Ressourcen, die eigentlich nicht mehr benötigt werden, nicht freigegeben werden.
4.1.3. SQL-Befehle
Die JDBC-API macht keine Einschränkungen auf die Art und den Inhalt von SQLAnweisungen, welche an das DBMS gesendet werden. Ein SQL-Befehl wird nämlich als
Zeichenkette dargestellt. Das führt zu einer grossen Flexibilität, indem dem Benutzer erlaubt
wird Datenbank-spezifische oder sogar Nicht-SQL-Anweisungen zu benutzen. Dies bedingt
aber, dass der Benutzer dafür verantwortlich ist, dass die darunterliegende Datenbank die
Anweisungen auch ausführen kann bzw. die Konsequenzen trägt, wenn dies nicht der Fall ist.
19
Eine solche Konsequenz kann sich z.B. dann ergeben, wenn versucht wird, eine gespeicherte
Prozedur (stored procedure) aufzurufen, aber das DBS dies gar nicht unterstützt. In diesem
Fall wird eine Ausnahme (exception) vom Typ java.sql.SQLException geworfen, die
natürlich abgefangen werden kann (und sollte). Um eine möglichst grosse Portabilität einer
Anwendung zu erreichen, sollte man auf eine direkte Nutzung der Datenbank-spezifischen
Funktionalität verzichten und diese stattdessen auf indirekte Weise zu nutzen. Eine
Möglichkeit sind z.B. gespeicherte Prozeduren (siehe Kapitel 4.2.3).
Zum Ausführen von SQL-Befehlen gibt es drei verschiedene Schnittstellen:
1. java.sql.Statement: Ein Statement-Objekt kann SQL-Anweisungen (ohne
Parameter) behandeln.
Es wird durch Connection.createStatement Methoden erstellt.
2. java.sql.PreparedStatement: Ein PreparedStatement-Objekt kann für
vorübersetzte SQL-Anweisungen benutzt werden. Diese Anweisungen können einen
oder mehrere Parameter als Eingabeargumente (IN parameters) übernehmen.
PreparedStatement ist eine Erweiterung von Statement. Der Vorteil
gegenüber dem einfachen Statement ist, dass die Möglichkeit besteht, dass die
Anfragen vor der Ausführung optimiert werden können. Das hat vor allem dann
Vorteile, wenn die Anfrage viele Male ausgeführt wird.
Es wird durch Connection.prepareStatement Methoden erstellt.
3. java.sql.CallableStatement: Solche Objekte werden benutzt um
gespeicherte DB-Prozeduren aufrufen zu können. CallableStatement ist eine
Erweiterung von PreparedStatement.
Es wird durch Connection.prepareCall Methoden erstellt.
Abbildung 5 (S. 19) gibt einen Überblick über die Schnittstellen im JDBC-API. In den
Abschnitten 4.1.5 (Statement), 4.1.6 (ResultSet), 4.1.7 (PreparedStatement) und 4.1.8
(CallableStatement) folgen die ausführlichen Beschreibungen dazu.
4.1.4. Transaktionen und Abschottungsgrade
A.
Transaktionen
Eine Transaktion besteht aus einer oder mehreren SQL-Befehlen, die alle der Reihe nach
ausgeführt werden. Änderungen in der DB werden nur vorgenommen, wenn alle einzelnen
Anweisungen der Transaktion erfolgreich ausgeführt wurden. Im Falle eines Fehlers werden
alle Anweisungen rückgängig gemacht und die DB befindet sich wieder in ihrem
ursprünglichen Zustand. Somit wird verhindert, dass die DB in einen inkonsistenten Zustand
fällt.
Die Steuerung von Transaktionen übernimmt dabei das Connection-Objekt. Dazu gibt es
die beiden Methoden commit und rollback.
Ein neu geschaffenes Connection-Objekt ist immer im auto-commit-Modus. D.h. wenn ein
Statement-Objekt ausgeführt wurde, wird automatisch ein commit ausgeführt. In einem
solchen Fall besteht die Transaktion aus nur einer Anweisung. Der auto-commit-Modus kann
auch deaktiviert werden:
con.setAutoCommit(false);
20
Im manuellen commit-Modus wird einen Transaktion erst beendet, wenn ausdrücklich
commit oder rollback aufgerufen wurde. Solange dies nicht geschieht, werden alle
Transaktionen gesammelt und beim nächsten commit ausgeführt. Die gesammelten
Transaktionen werden dann als eine Transaktion behandelt, d.h. im Falle eines Fehlers
werden alle Transaktionen zurückgesetzt.
Es gibt aber keine explizite Transaktionssteuerung für den Anfang einer Transaktion. Eine
Transaktion beginnt entweder mit dem Deaktivieren des auto-commit-Modus oder nach
jedem Aufruf von commit oder rollback. Dadurch können keine verschachtelten
Transaktionen ausgeführt werden. Zum Ausgleich dafür wurden neben den Connection-Pools
die Savepoints eingeführt. „Diese Etappenziele innerhalb einer Transaktion lassen sich nach
Belieben zurückrollen, ohne die anderen Änderungen innerhalb der Transaktion dadurch
ungültig zu machen“ (Pöschmann 25).
JDBC 2.0 ermöglicht aber auch die Unterstützung von verteilten Transaktionen. Wenn ein
Connection-Objekt an einer verteilten Transaktion teilnimmt, übernimmt eine
Transaktionsverwaltung (connection pool) die Steuerung über die einzelnen Transaktionen.
Die dazu notwendigen Schnittstellen sind im Paket javax.sql definiert. Ob ein JDBC-Treiber
überhaupt verteilte Transaktionen unterstützt ist aber von dessen Implementation abhängig.
Abbildung 5: Die JDBC Schnittstellen
21
B.
Abschottungsgrade (Transaction Isolation Levels)
Mit den Abschottungsgraden kann dem DBMS mitgeteilt werden, wie es vorgehen soll, wenn
zwei oder mehrere Transaktionen auf dieselben Daten zugreifen. Was soll z.B. geschehen,
wenn eine Transaktion (TA1) einen Wert ändert und eine zweite Transaktion (TA2) diesen
Wert lesen will, bevor TA1 ein „commit“ durchgeführt hat? Der Anwendungsentwickler kann
zum Beispiel den Zugriff auf den Wert mit der folgenden Anweisung zulassen („dirty read“):
con.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);
Die Connection-Schnittstelle definiert fünf Abschottungsgrade. Je höher der Grad, desto
grösser sind die Vorkehrungen zur Vermeidung von Konflikten. Die unterste Ebene lässt
keine Transaktionen zu, während die höchste Stufe allen anderen Transaktionen den Zugriff
auf die momentan verarbeiteten Werte verbietet. TRANSACTION_READ_UNCOMMITTED aus dem vorherigen Beispiel ist die zweite Stufe.
Dirty Reads
Abschottungsgrad
NonRepeatable
Reads
Phanton
Insert
0.
TRANSACTION_NONE
-
-
-
1.
TRANSACTION_READ_UNCOMMITTED
Ja
Ja
Ja
2.
TRANSACTION_READ_COMMITTED
Nein
Ja
Ja
3.
TRANSACTION_REPEATABLE_READ
Nein
Nein
Ja
4.
TRANSACTION_SERIALIZABLE
Nein
Nein
Nein
Tabelle 3: Auftreten von einigen Transaktionsproblemen in den jeweiligen Abschottungsgraden
Typischerweise nimmt mit zunehmendem Abschottungsgrad die Performance der DBAnwendung ab (zunehmende Anzahl Locks und damit der Wartezeiten). Bei der
Entscheidung, welcher Grad zur Anwendung kommt, muss man also abwägen zwischen der
Ausführungsgeschwindigkeit und der Konsistenz der Daten. Aber: der Grad, welcher
schliesslich unterstützt wird, hängt von den Fähigkeiten des DBMS ab! Zum Beispiel unterstützt Oracle9i ‘nur’ TRANSACTION_READ_COMMITTED und TRANSACTION_SERIALIZABLE. Die unterstützten Grade können mit der Methode DatabaseMetaData.supportsTransactionIsolationLevel() abgefragt werden.
4.1.5. Statement
A.
Ausführung von SQL-Befehlen
Ein Statement-Objekt wird benötigt, um SQL-Befehle auszuführen. Ein solches Objekt wird
durch die Methode Connection.createStatement erzeugt. Für die tatsächliche
Ausführung stehen die drei folgenden Methoden zur Verfügung:
1. Die Methode executeQuery dient zum Ausführen einer Anfrage und liefert eine
Ergebnismenge zurück.
2. Die Methode executeUpdate dient zum Ausführen von DML- und DDLAnweisungen. Der Rückgabewert einer DML-Anweisung ist die Anzahl geänderter
Zeilen, eine DDL-Anweisung liefert immer 0 zurück.
22
3. Die Methode execute dient zum Ausführen von beliebigen Anweisungen, die
mehrere Ergebnismengen zurückliefern oder mehrere Operationen ausführen.
Als Parameter ist jeweils die SQL-Anweisung anzugeben. Zum Beispiel eine SelectAnweisung:
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(“SELECT membernr, name FROM
members”);
Die Ergebnismenge rs ist eine Ergebnismenge, welche nicht aktualisierbar ist und auch nur
vorwärts abgearbeitet werden kann.
JDBC 2.0 bietet neu die Möglichkeit, Statement-Objekte zu schaffen, welche
Ergebnismengen zurückliefern, die in beiden Richtungen durchlaufen werden können und die
auch aktualisierbar sind. Das folgende Beispiel erstellt ein Statement-Objekt, dessen
Ergebnismenge bildlauffähig3 (scrollable)und auch aktualisierbar ist:
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
Mehr zu den Ergebnismengen folgt in Abschnitt 4.1.6.
B.
Stapelaktualisierungen (Batch Updates)
Eine weitere wichtige Neuerung in JDBC 2.0 ist die Möglichkeit, mehrere SQL-Anweisungen
zu sammeln und in Form eines Stapels an das DBMS zu senden. Die SQL-Anweisungen die
in einem Stapel gesammelt werden, dürfen keine Select-Anweisungen enthalten, da diese eine
Ergebnismenge zurückliefern.
Listing 4-2 stellt ein Beispiel für eine Stapelaktualisierung dar. Um einen Stapel auszuführen,
erstellt man zuerst ein Statement-Objekt. Danach schaltet man den Autocommit-Modus ab.
Nun kann man die Anweisungen im Stapel sammeln: Anstelle von executeUpdate wird
addBatch aufgerufen. Sind alle Anweisungen gesammelt, können sie mit executeBatch
ausgeführt werden
Statement stmt = con.createStatement();
con.setAutoCommit(false);
stmt.addBatch(“INSERT INTO emps VALUES(1000, ‘John Doe’)”);
stmt.addBatch(“INSERT INTO dept VALUES(260, ‘Research’)”);
stmt.addBatch(“INSERT INTO works_in VALUES(1000, 260));
int[] counts = stmt.executeBatch();
Listing 4-2: Stapelaktualisierungen
JDBC-Treiber müssen Stapelaktualisierungen nicht unterstützen. Ob ein Treiber diese
Unterstützung bietet, kann mit der Methode DatabaseMetaData.supportsBatchUpdates abgefragt werden.
3
Diese etwas merkwürdig klingende Übersetzung stammt aus Core Java 2 – Band II – Expertenwissen. Auf
jeden Fall ist damit gemeint, dass eine Ergebnismenge in beiden Richtungen durchlaufen werden kann.
23
4.1.6. Ergebnismengen (ResultSet)
Ein java.sql.ResultSet-Objekt enthält das Ergebnis einer SQL-Abfrage. Die Daten
der aktuellen Zeile der Ergebnismenge können mit Get-Methoden abgerufen werden. Ein
ResultSet-Objekt enthält einen Zeiger, welcher auf die aktuelle Zeile zeigt. Das folgende
Beispiel zeigt ein einfaches Beispiel, welches die ID als int, den Namen als String und das
Salär als double aus der Tabelle emps auswählt und ausgibt:
Statement = con.createStatement();
ResultSet rs = stmt.executeQuery(
“SELECT id, name, salary FROM emps”);
while (rs.next()) {
int id = rs.getInt(“id”);
String name = rs.getString(“name”);
double salary = rs.getDouble(“salary”);
System.out.println(id + “ ” name + “ ” +salary);
}
Listing 4-3: Bearbeiten einer Ergebnismenge
Für jeden Datentypen gibt es eine entsprechende get-Methode. Dabei kann entweder über den
Spaltennamen (wie im Beispiel) oder über den Spaltenindex zugegriffen werden. Dabei ist zu
beachten, dass der Spaltenindex bei 1 beginnt, im Gegensatz zur üblichen Java-Konvention.
Wenn zum Beispiel die Methode getString() aufgerufen wird, so versucht der Treiber den
Wert aus der Datenbank in eine Zeichenkette (String) zu verwandeln.
Das JDBC 2.0 API unterstützt die SQL:99-Datentypen Arrays, Referenzen, Lobs und Structs.
Kapitel 4.1.9 gibt einen Überblick über die unterstützten Datentypen und auch die
Konvertierung zwischen SQL-Datentyp und Java-Datentyp.
A.
Bildlauffähige (scrollable) Ergebnismengen
Jedes ResultSet-Objekt besitzt einen internen Zeiger, der immer auf die aktuelle Zeile zeigt.
Der Zeiger bewegt sich auf die nächste Zeile, wenn die Methode ResultSet.next()
aufgerufen wird. Wird ein ResultSet-Objekt kreiert, dann verweist der Zeiger auf die Position
vor der ersten Reihe. Durch den erstmaligen Aufruf von next() springt der Zeiger auf die
erste Reihe und diese wird zur Aktuellen. Mit dem JDBC 1.0 API können Ergebnismengen
nur vorwärts durchlaufen werden.
Das JDBC 2.0 API bietet daneben noch weitere Möglichkeiten an: neben dem sequentiellen
Zugriff (in beide Richtungen) kann auch wahlfrei auf die einzelnen Reihen zugegriffen
werden.
Wie schon in Abschnitt 4.1.5.A erwähnt, muss bereits beim Erstellen des Statement-Objekts
angegeben werden, ob die Ergebnismenge bildlauffähig sein soll. Das wird mittels einer
Konstanten gemacht. Mit dieser wird neben der Bildlauffähigkeit auch noch die Sensibilität
auf Datenbankänderungen festgelegt. Die ResultSet-Schnittstelle bietet drei Konstanten an:
1. TYPE_FORWARD_ONLY:
Die Ergebnismenge ist nicht bildlauffähig und bietet somit nur sequentiellen Zugriff
an.
2. TYPE_SCROLL_INSENSITVE:
Die Ergebnismenge ist bildlauffähig. Änderungen der Daten in der DB werden nicht
angezeigt.
24
3. TYPE_SCROLL_SENSITVE:
Die Ergebnismenge ist bildlauffähig und Änderungen werden angezeigt.
B.
Aktualisierbare Ergebnismengen
Eine Ergebnismenge kann in JDBC 2.0 auch aktualisierbar sein. D.h. die einzelnen Werte
können im ResultSet-Objekt geändert werden. Es lassen sich auch Tupel einfügen bzw.
löschen.
Allerdings sind nicht alle Ergebnismengen aktualisierbar. Ob eine Ergebnismenge aktualisiert
werden kann, hängt von folgenden drei Kriterien ab, die alle erfüllt sein müssen:
1. Die Abfrage wird nur auf einer Relation ausgeführt.
2. Die Abfrage darf kein JOIN oder keine GROUP BY Anweisung beinhalten.
3. Der Primärschlüssel muss ausgewählt werden.
Werden auch neue Daten eingefügt, so müssen zusätzlich noch folgende Bedingungen erfüllt
sein:
4. Der Benutzer muss Schreibrechte auf der Tabelle besitzen.
5. Die Abfrage muss alle Nicht-Null-Spalten beinhalten.
6. Die Abfrage muss alle Spalten auswählen, die keine Standardwerte haben.
Damit eine Ergebnismenge aktualisierbar ist, muss dies zusammen mit der Angabe der „Bildlauffähigkeit“ gemacht werden. Zur Verfügung stehen folgende beiden Konstanten der
ResultSet-Schnittstelle:
1. CONCUR_READ_ONLY:
Die Ergebnismenge kann nur zum Lesen verwendet werden.
2. CONCUR_UPDATABLE
Die Ergebnismenge ist aktualisierbar.
Das folgende Beispiel sucht die Saläre aller Mitarbeiter der Forschungsabteilung zusammen
und führt eine Lohnerhöhung von 10% durch:
Statement stmt = con.createStatement(
ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery(
“SELECT id, name, salary FROM emps WHERE dept=’Research’”);
while (rs.next()) {
double salary = rs.getDouble(“salary”);
rs.updateDouble(“salary”, salaray*1.1);
rs.updateRow();
}
Listing 4-4: Ein Beispiel für aktualisierbare Ergebnismengen
Beide neuen Möglichkeiten haben ihren Nachteil in der Zunahme des Verwaltungsaufwands
für das DBMS. Dabei ist die Leistungseinbusse von der Implementierung des JDBC Treibers
abhängig.
Die standardmässige Ergebnismenge, wenn die parameterlose Methode Connection.createStatement aufgerufen wird, ist weder bildlauffähig noch aktualisierbar. Damit
wird die Kompatibilität mit dem JDBC 1.0 API gewahrt.
25
4.1.7. PreparedStatement
Die Schnittstelle PreparedStatement erbt von Statement und unterscheidet sich davon auf
zwei Arten:
1. PreparedStatement-Instanzen beinhalten eine SQL-Anweisung, die vom Treiber
bereits vorkompiliert wurde. Diese Anweisungen können dann immer wieder
verwendet werden, ohne dass sie jedes Mal neu kompiliert werden müssen.
2. Die SQL-Anweisung kann IN-Parameter beinhalten. Ein IN-Parameter ist ein
Parameter, dessen Wert zum Erstellzeitpunkt noch nicht bekannt ist. IN-Parameter
werden in SQL-Anweisungen durch Platzhalter, einem Fragezeichen „?“, dargestellt.
Bevor eine solche Anweisung ausgeführt werden kann, muss jedem IN-Parameter ein
Wert zugewiesen werden.
Weil PreparedStatement-Objekte im Voraus übersetzt werden, kann ihre Ausführung
schneller erfolgen als bei einem Statement-Objekt. SQL-Anweisungen, die häufig ausgeführt
werden, sollten deshalb als PreparedStatement-Objekte geschaffen werden.
Das folgende Beispiel enthält eine Update-Anweisung mit zwei IN Parametern:
PreparedStatement pstmt = con.prepareStatement(
“UPDATE vehicles SET price = ? WHERE model = ?”);
Bevor die Anweisung ausgeführt werden kann, muss allen Platzhaltern ein Wert zugewiesen
werden. Dies geschieht durch das Aufrufen von set-Methoden. Für jeden Datentyp gibt es
eine entsprechende Methode. Diese Methoden besitzen zwei Parameter. Der erste ist die
(ordinale) Position des Platzhalters, der zweite ist der zuzuweisende Wert. Es liegt in der
Verantwortung des Programmierers zu garantieren, dass der Java-Datentyp des IN-Parameters
auch in den Datentyp der Datenbank konvertiert werden kann. Tabelle 4 zeigt, welche
Methode für welche Datentypen zu verwenden ist.
Sind die Werte zugewiesen, kann die Anweisung ausgeführt werden. In Fortsetzung des
vorherigen Beispiels, müssten der Preis als double und das Modell als Zeichenkette festgelegt
werden:
pstmt.setDouble(1, 26570);
pstmt.setString(2, “Clio 1.4 16V”);
int rowCount = pstmt.executeUpdate();
Natürlich werden auch bildlauffähige und aktualisierbare Ergebnismengen sowie
Stapelaktualisierung unterstützt. Die Anwendung erfolgt gleich wie bei einem StatementObjekt.
4.1.8. CallableStatement
Ein CallableStatement-Objekt besitzt die Möglichkeit, gespeicherte Prozeduren (stored
procedures, vgl. dazu auch 4.2.3) aufzurufen. Eine gespeicherte Prozedur kann entweder eine
Methode oder eine Funktion sein und ist in der Datenbank gespeichert. Der Aufruf (call) der
Prozedur ist das, was ein CallableStatement-Objekt beinhaltet. Für den Aufruf einer Prozedur
gibt es zwei Formen: Eine Form mit einem Ergebnisparameter und die andere ohne. Ein
Ergebnisparameter, eine Art von OUT-Parameter, speichert den Rückgabewert der
gespeicherten Prozedur. Beide Formen können noch weitere Parameter beinhalten. Dies
können IN-, OUT- oder INOUT-Parameter sein. Ein Fragezeichen („?“) dient wiederum als
Platzhalter:
26
{call procedure_name[(?,?,…)] }
{? = call procedure_name[(?,?,…)] }
Listing 4-5: Die Syntax für den Aufruf einer gespeicherten Prozedur
CallableStatement-Objekte erben von PreparedStatement. Zusätzlich zu den PreparedStatement-Instanzen können CallableStatement-Instanzen auch mit OUT- bzw. INOUT-Parametern umgehen. Die IN- bzw. INOUT-Parameter werden mit den gleichen set-Methoden
festgelegt. Um die Rückgabewerte zu lesen, gibt es get-Methoden, ähnlich denjenigen der
ResultSet-Schnittstelle.
Das folgende Beispiel führt die gespeicherte Prozedur addMember aus. Die Prozedur fügt
ein neues Mitglied hinzu. Sie besitzt zwei Parameter (den Vor- und Nachnamen) und liefert
die Mitgliedernummer zurück.
CallableStatement cstmt = con.prepareCall(
“{ ? = call addMember(?,?) }”);
cstmt.setString(2, “Larry”);
cstmt.setString(3, “Ellison”);
cstmt.registerOutParameter(1, java.sql.Types.INTEGER);
cstmt.executeUpdate();
int membernr = cstmt.getInteger(1);
Listing 4-6: Der Aufruf einer gespeicherten Prozedur
Wie man aus dem Beispiel erkennen kann, ist es notwendig, dass der Datentyp des OUTParameters dem CallableStatement-Objekt bekannt gemacht wird (das gilt auch für die
INOUT Parameter). Die Nummerierung der Parameter entspricht wieder ihrer Reihenfolge.
Die Klasse DatabaseMetaData stellt aber zahlreiche Methoden zur Verfügung um die
Unterstützung von gespeicherten Prozeduren zu überprüfen. Zum Beispiel wird die Methode
supportsStoredProcedures wahr zurückliefern, wenn das DBMS gespeicherte Prozeduren
unterstützt, und die Methode getProcedures gibt eine Beschreibung der vorhandenen
Prozeduren zurück.
4.1.9. Datentypen
A.
Übersicht
Leider sind Datentypen in SQL und solche in der Java Programmiersprache nicht identisch.
Deshalb sind Mechanismen zur Transformation von Java-Datentypen in SQL-Datentypen
notwendig.
Das JDBC API stellt drei Gruppen von Methoden zum Datenaustausch zwischen Datenbank
und Java-Anwendung zur Verfügung:
1. Methoden der ResultSet-Schnittstelle, um das Ergebnis einer Abfrage in Java Typen
zu konvertieren.
2. Methoden der PreparedStatement-Schnittstelle, um Java Typen als Parameter einer
SQL-Anweisung mitzugeben.
3. Methoden der CallableStatement-Schnittstelle, um OUT-Parameter in Java Typen zu
verwandeln.
Tabelle 5 stellt die standardmässige Abbildung von SQL Typen nach Java Typen dar. Tabelle
4 zeigt die Abbildung von Java nach SQL Typen.
27
JDBC Typ
Java Typ
Java Typ
JDBC Typ
CHAR
String
String
CHAR,
VARCHAR
String
NUMERIC
java.math.BigDecimal
DECIMAL
java.math.BigDecimal
java.math.BigDecimal
NUMERIC
BIT
boolean
boolean
BIT
TINYINT
byte
byte
TINYINT
SMALLINT
short
short
SMALLINT
INTEGER
int
int
INTEGER
BIGINT
long
long
INTEGER
REAL
float
float
REAL
FLOAT
double
double
DOUBLE
DOUBLE
double
byte[]
VARBINARY
BINARY
byte[]
java.sql.Date
DATE
VARBINARY
byte[]
java.sql.Time
TIME
LONG VARBINARY
byte[]
java.sql.Timestamp
TIMESTAMP
DATE
java.sql.Date
java.sql.Clob
CLOB
TIME
java.sql.Time
java.sql.Blob
BLOB
TIMESTAMP
java.sql.Timestamp
java.sql.Array
ARRAY
CLOB
java.sql.Clob
java.sql.Struct
STRUCT
BLOB
java.sql.Blob
java.sql.Ref
REF
ARRAY
java.sql.Array
Tabelle 4: Konvertierung von Java- nach
JDBC-Typen
STRUCT
java.sql.Struct
REF
java.sql.Ref
VARCHAR or
LONGVARCHAR
Tabelle 5: Konvertierung von JDBC- nach
Java Typen
28
B.
SQL:99 Datentypen
In JDBC 2 wird auch auf die neuen SQL:99 Datentypen Bezug genommen: So werden neben
Arrays auch Referenzen sowie UDTs unterstützt, und zwar durch die neuen Schnittstellen
Array, Ref sowie Struct des Pakets java.sql.
Die Schnittstelle Ref erlaubt das Lesen bzw. Verarbeiten von Referenzen in JDBC. Der
Zugriff erfolgt über die neuen Methoden getRef bzw. setRef der Schnittstellen
ResultSet, PreparedStatement und CallableStatement.
Die Schnittstelle Struct bietet Methoden für das Lesen und Schreiben von UDT Instanzen.
Natürlich werden auch Methoden für das Lesen der Attributwerte zur Verfügung gestellt. Die
Verwendung ist aber ziemlich mühsam, denn die UDT Instanz muss in ihre ganzen
Bestandteile zerlegt werden.
Listing 4-7 zerlegt Instanzen vom UDT Address_t. Es sollen z.B. alle Adressen der Mitglieder
irgendwie4 bearbeitet werden. Dafür müssen die Adressen aus der Datenbank gelesen werden.
Die Select-Anweisung liefert eine Ergebnismenge mit einem Attribut vom Typ Address_t
zurück. Da Address_t weder ein Standard-Java-Datentyp noch ein Standard-SQL-Datentyp
ist, muss er als Struct behandelt werden. Damit wir auf die einzelnen Attribute zugreifen
können, muss die Methode Struct.getAttributes ausgeführt werden. Diese liefert ein
Objekt-Array zurück, die Grösse entspricht der Anzahl Attribute von Address_t. Die Werte
des Arrays müssen nun noch in ihren tatsächlichen Datentyp konvertiert werden.
…
ResultSet rs = stmt.executeQuery(
„SELECT m.getAddress() FROM member m“);
while(rs.next()) {
Struct address = (Struct) rs.getObject(1);
Object attributes[] = address.getAttributes();
String street = (String) attributes[0];
int housNr = (int) attributes[1];
String zip = (String) attributes[2];
String city = (String) attributes[3];
…
}…
Listing 4-7: Verwendung von java.sql.Struct
Mit der Schnittstelle java.sql.SQLData können UDTs als eigenständige Java-Klassen
umgesetzt werden. Diese Schnittstelle wird in Kapitel 5.2.2 ausführlicher behandelt.
4.2. SQLJ-1: Java Stored Procedures
4.2.1. Überblick über den SQLJ Standard
Verschiedene Datenbankhersteller und andere Partner haben sich für die Entwicklung von
allgemeinen Standards für die Integration von Java und SQL zusammengeschlossen. Das Ziel
4
Was mit den Adressen genau gemacht wird, ist hier nicht relevant. Wichtiger ist wie und in welcher Form die
Adressen aus der DB geholt werden.
29
ist eine effektive, betriebs- wie auch DB-unabhängige Lösung. SQLJ befasst sich mit
verschiedenen Aspekten der Integration von SQL und Java und besteht aus drei Teilen:
•
Der erste Teil (Part 0: Embedded SQL for Java, abgekürzt SQLJ-0) stellt eine
standardisierte Syntax und Semantik für die statische Einbettung von SQL in JavaProgrammen zur Verfügung.
•
Der zweite Teil (Part 1: Java Stored Procedures, abgekürzt SQLJ-1) befasst sich mit
der Implementierung von Prozeduren und Funktionen in Java, welche in einer
Datenbank gespeichert werden können.
•
Der dritte Teil (Part 2: Java Datatypes, abgekürzt SQLJ-2) beschreibt Möglichkeiten,
wie man Java-Klassen als benutzerdefinierte SQL-Datentypen in einer Datenbank
ablegen kann.
Alle Teile sind inzwischen ANSI-Standard. In dieser Arbeit geht es vor allem um SQLJ-1.
4.2.2. Funktionsweise
SQLJ bietet gegenüber JDBC eine einfachere Verwendung an: SQL-Anweisungen können
beinahe 1:1 übernommen werden. Man muss sich nicht noch um Statements, ResultSets u.a.
kümmern. Dies führt im Allgemeinen zu weniger Programmzeilen. SQLJ-Quellcode kann
aber nicht mit dem Javacompiler übersetzt werden, sondern muss mit dem SQLJ-Translator
vorübersetzt werden. Dieser wandelt die SQLJ-Anweisungen in JDBC-Aufrufe um. Danach
kann der erhaltene Java-Quelltext mit einem normalen Java Compiler übersetzt werden (vgl.
Abbildung 6). SQLJ kann JDBC also nicht ersetzen, sondern baut darauf auf. Der
Anwendungsentwickler, der sich für SQLJ entscheidet, kann aber auf JDBC verzichten.
Example.sqlj
SQLJ-Translator
Example_SJProfile0.ser
Checker
Example.java
Java Compiler
inspects
Example.class
Standard Runtime
DB
SQLJ Runtime
Abbildung 6: Kompilieren von SQLJ-Programmcode
Ein weiterer wichtiger Vorteil ist die Möglichkeit, SQL-Anweisungen schon zum
Übersetzungszeitpunkt des Programms auf folgende Punkte zu überprüfen:
30
•
Korrekte Syntax
•
Übereinstimmung der Anweisungen mit dem DB-Schema
•
Typkompatibilität der für den Datenaustausch genutzten Variablen
So können Fehler in SQL-Anweisungen schon beim Transformieren bemerkt werden, und
(hoffentlich) nicht erst durch den Endanwender! Vergleiche dazu Abbildung 6.
4.2.3. Benutzerdefinierte Routinen (Stored Procedures)
Benutzerdefinierte Routinen sind Prozeduren, Methoden, Funktionen und Trigger, welche die
Funktionalität des DBS erweitern. Neben prozeduralem SQL können die Routinen auch in
einer DB-fremden Programmiersprache geschrieben werden (C, Java, …). SQLJ-1 kümmert
sich um die Spezifikation von Java Stored Procedures.
Mit Java bzw. SQLJ-1 können somit Prozeduren entwickelt werden, welche die gesamte
Mächtigkeit einer objektorientierten Programmiersprache benutzen können und dabei erst
noch plattformunabhängig sind. Die Java-Methoden müssen als statisch deklariert werden.
SQLJ-1 sollte vor allem dann verwendet werden, wenn die gewünschte Funktionalität in
prozeduralem SQL nicht implementiert werden kann, oder wenn die Implementierung in Java
wesentlich einfacher ist. Als Nachteil ist sicherlich die Performance zu erwähnen, denn der
Aufruf einer Java Stored Procedure hat nämlich immer einen Kontextwechsel zur Folge, was
natürlich auf Kosten der Geschwindigkeit geht.
Ein weiterer Vorteil von benutzerdefinierten Routinen ist, dass die Rechenkraft vom DBServer mehr genutzt werden kann. Somit können rechenintensive Aufgaben auf dem Server
ausgeführt werden und die Client-Rechner werden dadurch entlastet. Man denke da z.B. an
mobile Clients wie Mobiltelefone, Palmtops und ähnliche Geräte. Ebenfalls leistungssteigernd
wirkt die Möglichkeit, einen Teil der Anwendungslogik im DB-Server selbst ausführen zu
können.
Damit man die benutzerdefinierte Routinen ausführen und benutzen kann, benötigen sie einen
SQL Namen. Sie müssen durch eine Aufrufspezifikation (call specification) publiziert
werden. Durch die Aufrufspezifikation werden der Name der Routine und die Datentypen der
Parameter und des Rückgabewertes publiziert. Im Gegensatz zu einem Wrapper, welcher eine
zusätzliche Schicht darstellt, wird durch die Aufrufspezifikation einfach die Existenz einer
gespeicherten Prozedur bekannt gemacht.
Das folgende Beispiel (Listing 4-8) erstellt die Klasse MemberUtil mit einer Methode, die die
Anzahl der Mitglieder zurückgibt:
public class MemberUtil {
public static int countMembers() {
int counter = 0;
try {
#sql {
SELECT COUNT(*) INTO :counter FROM member m
};
} catch (SQLException e) {
} finally {
return c;
}
}
}
31
Listing 4-8: Die Klasse MemberUtil
Nun muss die (kompilierte) Klasse in die DB geladen werden. Mehrere Klassen können auch
in eine Jar-Datei (Java archive file) gepackt werden und so geladen werden.
Die dazugehörige Aufrufspezifikation kann zum Beispiel so aussehen (für void-Methoden
benutzt man CREATE PROCEDURE):
CREATE FUNCTION count_members RETURN INT
READS SQL DATA
EXTERNAL NAME ‘MemberUtil.countMembers()’
LANGUAGE JAVA PARAMETER STYLE JAVA;
Listing 4-9: Ein Beispiel für eine Aufrufspezifikation
Die CREATE FUNCTION (bzw. die CREATE PROCEDURE) Anweisung definiert den
SQL-Namen für die Java-Methode, welche im Ausdruck EXTERNAL NAME aufgeführt ist.
Es ist möglich, dieselbe Java-Methode gleichzeitig unter verschiedenen SQL-Namen zu
publizieren. Der Ausdruck READS SQL DATA gibt an, dass die Methode Lesezugriffe auf
Tabellen und Sichten ausführen darf (die weiteren Möglichkeiten werden im nächsten
Abschnitt behandelt).
Die so publizierte Prozedur kann z.B. bei Oracle mit dem PL/SQL-Befehl CALL aufgerufen
werden. Die Variable s ist notwendig, um den Rückgabewert der Funktion zu speichern.
VARIABLE counter int;
CALL count_members() INTO :counter;
Die Funktion kann aber auch in Abfragen und anderen Anweisungen verwendet werden
INSERT INTO personmembers VALUES (
personmember_typ(
(count_members() + 1), -- membernumber
‘Dirk’, ‘Nowitzki’, to_date(’01.02.1973’),‘secret’
)
);
Listing 4-10: Anwendungsbeispiele für benutzerdefinierte Funktionen
4.2.4. Datenbankzugriffe
Java stored procedures können auch auf DB-Werte zugreifen. Dazu stehen beide
Zugriffsmöglichkeiten, JDBC und SQLJ-0, zur Verfügung. Die Verbindung wird
folgendermassen hergestellt:
•
JDBC:
Connection con = DriverManager.getConnection(
”jdbc.default.connection”);
•
SQLJ-0: Es muss kein Verbindungskontext angegeben werden. Das wird durch den
SQLJ-Translator erledigt.
Wird eine Verbindung bzw. ein Verbindungskontext zu einer anderen, nicht auf dem gleichen
DB-Server liegenden Datenbank benötigt, so kann diese verwendet werden. Das Verwenden
unterschiedlicher Verbindungen und Verbindungskontexte ist problemlos möglich.
Verwendet eine Prozedur SQL Daten, so muss dies in der Aufrufspezifikation mitgeteilt
werden. Dazu stehen vier Parameter zur Verfügung:
32
•
NO SQL: Die Prozedur führt keine SQL-Operationen aus.
•
CONTAINS SQL: Die Prozedur kann SQL-Operationen ausführen, aber keine Daten
lesen oder ändern.
•
READS SQL DATA: Die Methode kann SQL-Befehle ausführen, und Lesezugriffe
auf Tabellen oder Sichten sind erlaubt.
•
MODIFIES SQL DATA: Die Methode kann SQL-Operationen ausführen und Daten
lesen und ändern.
4.3. Bewertung
JDBC unterstützt den Zugriff auf beinahe jede Datenquelle und funktioniert dabei auf jeder
Plattform mit einer Java Virtual Machine. Die Verwendung des JDBC-API innerhalb der
Java-Programmiersprache macht es so möglich, sowohl plattform- als auch datenbankunabhängige Anwendungen zu schreiben.
Nachteile bestehen darin, dass der Programmcode relativ stark aufgebläht und unübersichtlich
wird. Weiter können Fehler in SQL-Anweisungen erst zur Laufzeit entdeckt werden. Diesen
Nachteilen versucht SQLJ-0 Abhilfe zu schaffen. SQLJ bettet SQL-Befehle in statischer
Weise in Java-Programmen ein. Das bedingt, dass bei Verwendung von SQLJ die SQLBefehle (bis auf Parameterwerte) schon zum Entwicklungszeitpunkt bekannt sein müssen.
Dies ist aber meistens der Fall. Die Verwendung von SQLJ wird u.a. auch von den DBHerstellern vorgeschlagen: Der SQL-relevante Code wird so einfacher und kürzer und
semantische und syntaktische Fehler können schon zur Übersetzungszeit entdeckt werden.
Zudem können die Anfragen schon bei der Übersetzung optimiert werden, was Vorteile in der
Geschwindigkeit bringen kann.
Bei beiden Varianten bleibt aber zu bemerken, dass die semantische Lücke (impedance
mismatch) zwischen Java und objektrelationalem Datenbanksystem auf konzeptioneller Ebene
zwar kleiner wird, auf System- und Sprachebene jedoch unverändert vorhanden ist. Der
Abbildungsaufwand zwischen den beiden Welten ist trotz neuer Datentypen (Struct, Ref
und Array) von JDBC 2.0 praktisch unverändert geblieben. Für den Zugriff auf die
atomaren Werte eines Structs ist immer noch die genaue Kenntnis der Struktur des UDTs
notwendig – keine wirkliche Verbesserung gegenüber der Verwendung von relationalen DBS.
Dasselbe gilt auch für Ref (beim Dereferenzieren) und für Array. Vorhandene
objektorientierte Eigenschaften können so nicht ohne weitere Vorkehrungen und
Transformationen in Java ausgenutzt werden (vgl. Kapitel 5.2).
SQLJ-1 befasst sich mit Java Stored Procedures. SQLJ-1 ist vor allem dann zu verwenden,
wenn die gewünschte Funktionalität nicht in prozeduralem SQL implementiert werden kann
oder wenn die Implementierung in Java einfacher ist. Zum Schliessen bzw. Verringern der
semantischen Lücke kann SQLJ-1 etwas beitragen, denn so ist es möglich bereits bestehende
Java-Methoden und -Funktionen auch innerhalb der DB zu verwenden. Daraus nun zu
folgern, nur noch Java Stored Procedures zu verwenden, ist keine so gute Idee. Dagegen
sprechen vor allem Perfomancegründe. Die Geschwindigkeitseinbussen, welche vor allem
durch den Kontextwechsel entstehen, können (bis jetzt) durch keine noch so ausgefuchsten
Optimierungen wettgemacht werden.
SQLJ-2 erlaubt es vorhandene Java-Klassen als UDT in der Datenbank zu definieren und
Instanzen davon in (typisierten) Tabellen zu speichern. Als Gründe dafür und dagegen können
33
die gleichen wie bei SQLJ-1 herangezogen werden. SQLJ-2 kann also dazu beitragen, die
Lücke zwischen Java und DB zu verringern.
5. Verwendung objektrelationaler Eigenschaften
In diesem Kapitel geht es nun um die konkrete Verwendung eines objektrelationalen
Datenbanksystems (Oracle9i) mit der objektorientierten Programmiersprache Java anhand
einer Carsharing-Anwendung. Im Zentrum dieses Kapitels stehen dabei:
1. die Umsetzung des konzeptuellen in das logische Schema (natürlich unter objektrelationalen Aspekten)
2. Abbildung von benutzerdefinierten Typen zwischen SQL und Java
3. Werkzeugunterstützung
Oracle9i unterstützt eine Vielzahl der objektrelationalen Erweiterungen des SQL:99Standards. Somit sollte es möglich sein, das konzeptuelle Schema (fast) ohne
Einschränkungen in das logische Schema zu transformieren. Das konzeptuelle Schema soll
aber auch für die Geschäftslogik der Java-Anwendung gelten. D.h. wir möchten auf beiden
Seiten die gleichen Datentypen verwenden, was zur Folge hat, dass die Entwickler die
benutzerdefinierten Typen zweimal machen müssen. Diese Abbildung zwischen SQL- und
Java-Typen ist – wie schon verschiedentlich in dieser Arbeit dargelegt – aber ziemlich
aufwendig. Diesen Aufwand kann man durch Werkzeuge minimieren. Oracle stellt dazu das
Werkzeug JPublisher zur Verfügung, dass genau diese Abbildung von DB-Typen zu JavaKlassen vornimmt.
Die Beispielanwendung soll die Verwaltung einer Carsharing-Organisation unterstützen.
„Carsharing bezeichnet den gemeinsamen Besitz und die Benutzung von Fahrzeugen durch
die Mitglieder einer Organisation. Teilnehmer können somit nach Bedarf Fahrzeuge benutzen,
ohne dazu privat ein Auto kaufen und unterhalten zu müssen“ (Geppert 403). Die wichtigsten
Aufgaben der Anwendung sind:
•
Verwaltung der Mitglieder
•
Verwaltung der Fahrzeuge
•
Verwaltung der Fahrzeug-Standorte
•
Reservationsabwicklung sowie Rechnungsstellung
•
Bereitstellen von Informationen über Auslastung und Effizienz von Fahrzeugen und
Standorten
Der Zugriff auf die Anwendung wird dabei über das Web ermöglicht. Dabei gibt es eine Sicht
für die Mitglieder und eine für den Administrator. Mitglieder können neue Reservationen
eingeben und auch löschen, sowie ihre Rechnungen betrachten. Daneben erhalten sie einen
Überblick über Standorte und Fahrzeuge. Der Administrator kann Mitglieder, Fahrzeuge und
Standorte verwalten, sowie Statistiken erstellen.
Eine detaillierte Beschreibung der Anwendung findet man bei Geppert ab S. 403.
34
5.1. Konzeptueller und logischer Entwurf
5.1.1. Erweiterungen
Das konzeptuelle Schema habe ich praktisch unverändert übernommen (vgl. 7.1 und Geppert
407 f.). Einige Änderungen betreffen objektorientierte Erweitungen, andere neue (Sub-)
Klassen (InsuranceCompany, SnoopyMember).
InsuranceCompany erbt von Company, fügt aber keine neuen Attribute oder Methoden
ein. Die Klasse dient nur zur Unterscheidung, z.B. wenn ein neues Fahrzeug in die DB
eingetragen wird. Dabei wird eine Versicherungsgesellschaft benötigt (für die
Versicherungspolice).
SnoopyMember soll Probemitglieder darstellen. Die Probemitgliedschaft ist auf drei
Monate beschränkt und kostet dabei nur Fr. 20. Ist die Probezeit vorüber, können keine
Reservationen mehr gemacht werden. Ist das Probemitglied weiterhin interessiert, so kann es
in ein vollständiges Mitglied vom Typ PersonMember konvertiert werden.
Daneben habe ich auch einige Schemaänderungen vorgenommen. Bei der Modellierung der
Rechnungen habe ich einen anderen Ansatz gewählt: Oracle9i unterstützt nämlich die
Speicherung von geschachtelten Tabellen, also ungeordneten Kollektionen ohne Maximalkardinalität. Eine Rechnung (Invoice) kann somit nicht nur einen Eintrag, sondern eine
unbestimmte Anzahl von Einträgen aufnehmen. InvoiceLineItem stellt einen solchen
Eintrag dar. Die Benutzung eines Fahrzeuges wird in der Klasse UseOfVehicle dargestellt,
einer Subklasse von InvoiceLineItem (vgl. Abbildung 7). Somit könnte man den
Mitgliedern z.B. Sammelrechnungen anbieten, worin alle Fahrzeugbenutzungen innerhalb
eines Monats zusammengefasst sind.
Abbildung 7: Die Modellierung von Rechnungen
35
Bei den Stationen (Location) habe ich etwas Ähnliches gemacht. Die Mietverträge für die
Parkplätze (LeaseContract) werden ebenfalls in einer geschachtelten Tabelle gespeichert.
Der Vorteil dieser hierarchischen Lösung ist, dass man auf separate Relationen für die
geschachtelten Daten verzichten kann. So bleiben die Objektstrukturen erhalten und müssen
nicht aufgebrochen werden. Ein Nachteil ist, dass der Zugriff auf die geschachtelten Daten
relativ mühsam ist. Möchte man z.B. eine Auflistung aller Mietverträge, so muss zuerst auf
alle Stationen zugegriffen werden und von dort dann auf die einzelnen Mietverträge.
Für zentrale Entitäten (Member, Location, Vehicle, Reservation), die miteinander
in Beziehung stehen, ist eine solche Lösung nicht geeignet oder auch nicht möglich. Dann
muss auf die „herkömmliche“ Modellierung über zusätzliche Relationen zurückgegriffen
werden. Im Gegensatz zum relationalen Entwurfsmuster, sollten die Beziehungen zwischen
den Tabellen nicht durch Fremdschlüssel, sondern durch Referenzen modelliert werden. Der
Vorteil der Referenzen ist, dass diese navigierbar sind, d.h. man kann direkt auf die Attribute
und Methoden des referenzierten Objekts zugreifen.
5.1.2. Implementation der Methoden
Mit PL/SQL steht dem Programmierer eine gute prozedurale Erweiterung von SQL zur
Verfügung, welche auch in Bezug auf die neuen objektrelationalen Neuerungen kaum
Wünsche offen lässt. Schade ist, dass Referenzen nur innerhalb einer SFW-Anweisung
navigierbar sind und dass die Manipulation von Attributen auch nur innerhalb einer INSERToder UPDATE-Anweisung erfolgen können.
Die meisten Methoden und Funktionen der UDTs habe ich in PL/SQL implementiert. Der
Grund dafür liegt einerseits in der besseren Performance und andererseits hatte ich keine so
komplexen Methoden zu implementieren, bei welcher PL/SQL an seine Grenzen gestossen
wäre. Allerdings liessen sich einige Methoden sicherlich einfacher in Java entwickeln.
Einige Methoden habe ich trotzdem als Java Stored Procedures implementiert, vor allem für
die Prozeduren und Methoden von Fleet_Manager. Der erstmalige Aufruf dieser
Methoden benötigte deutlich mehr Zeit. Bei späteren Aufrufen waren die Verzögerungen zu
vernachlässigen.
5.1.3. Tabellenhierarchien
Im Gegensatz zum SQL:99-Standard sind bei Oracle keine Subtabellen möglich.
Tabellenhierarchien können allerdings mit Sichten (Views) geschaffen werden. Das macht es
möglich, bisherige relationale Tabellen ohne Anpassungen als Objekttabellen darzustellen.
Alles was man dazu benötigt sind eine oder mehrere Relationen aus der die Daten stammen
und einen UDT.
Tabellenhierarchien sind dann sinnvoll, wenn viele Anfragen nur bestimmte (Sub-)Typen
betreffen. Denn der Zugriff auf Attribute und Methoden von Subtypen in einer typisierten
Tabelle ist nur durch Typumwandlung möglich. Das folgende Beispiel verdeutlicht dies.
Dabei wird auf dass Attribut responsibleFor des Typs CoopMember zugegriffen:
SELECT membernr, m.getName(),
TREAT(VALUE(m) AS coopmember_t).responsibleFor
FROM member m
WHERE VALUE(m) IS OF(coopmember_t);
Listing 5-1: Abfrage von Attributen und Methoden von Subtypen
36
Listing 5-1 zeigt verschiedene Aspekte auf: Zugriffe auf Attribute (membernr) des
Supertyps sind ohne Tabellen-Alias möglich. Für den Zugriff auf (geerbte) Methoden ist ein
Alias notwendig. Wurde eine Methode überschrieben, so wird automatisch die richtige
Methode ausgeführt. Für den Zugriff auf zusätzliche Attribute und Methoden von Subtypen
ist eine Typkonvertierung notwendig. Dies wird durch die Anweisung TREAT…AS…
bewerkstelligt. Damit nur Genossenschaftsmitglieder ausgewählt werden, muss in der WhereKlausel eine Einschränkung durch den IS OF-Operator gemacht werden.
Bei der Sichtendefinition für einen bestimmten UDT müssen alle Attribute in der richtigen
Reihenfolge ausgewählt werden. Listing 5-2 erstellt die Sicht coopmember_view:
CREATE OR REPLACE VIEW coopmember_view OF coopmember_t
WITH OBJECT IDENTIFIER (membernr) AS
SELECT membernr, person , password, homelocation, since,
TREAT(Value(m) as coopmember_t).shares,
TREAT(value(m) as coopmember_t).responsibleFor
FROM member m
WHERE VALUE(m) IS OF (coopmember_t);
Listing 5-2: Sichtendefinition
Die in Listing 5-2 definierte Sicht vereinfacht gegenüber Listing 5-1 den Zugriff auf
Instanzen vom Typ coopmember_t oder auf dessen Klassenvariablen und –methoden.
SELECT membernr, c.getName(), responsibleFor
FROM coopmember_view c;
Listing 5-3: Abfrage auf einer Subtabelle
5.2. Abbildung von Objekten zwischen SQL und Java
In diesem Abschnitt geht es nun darum, wie die in der DB erstellten UDTs (gemäss
Klassendiagramm) auch in einer konkreten Java-Anwendung als eigene Klassen benutzt
werden können.
Dabei gibt es grundsätzlich zwei Möglichkeiten für die man sich entscheiden kann: eine
schwache und eine strenge Typisierung (weak bzw. strong typing).
5.2.1. Schwache Typisierung
Bei der Abbildung durch schwache Typisierung wird ein UDT durch eine Instanz von
java.sql.Struct abgebildet. Diese Vorgehensweise ist vor allem dann von Vorteil,
wenn es darum geht, SQL-Daten zu manipulieren. Wenn z.B. die Java-Anwendung als
Werkzeug zum Manipulieren von beliebigen Objektdaten dienen soll, kann dies nur über eine
schwache Typisierung geschehen. Ein Vorteil ist, dass die Daten innerhalb des Structs im
SQL-Format bleiben und erst bei Bedarf in Java-Datentypen konvertiert werden. Ein Nachteil
ist die umständliche Benutzung (vgl. Listing 4-7, S. 27).
Bei der Verwendung von Oracle-DB und Oracle-Treibern steht neben der Standard-JDBC
Schnittstelle java.sql.Struct noch die Schnittstelle oracle.sql.STRUCT sowie die
Oracle-Datentypen im Packet oracle.sql zur Verfügung. Oracle schlägt denn auch die
Verwendung der eigenen Schnittstellen vor. Im Hinblick auf eine bessere Portabilität
empfehle ich aber die Verwendung des Standards.
37
5.2.2. Strenge Typisierung
A.
Erstellen benutzerdefinierter Klassen
Bei der Abbildung durch strenge Typisierung wird ein UDT in eine eigene Java-Klassen
abgebildet. Die Klassen müssen instanzierbar sein sowie allgemeines Verhalten für die
darunterliegenden Datenbankoperationen implementieren. Dazu hat man die Möglichkeit der
Verwendung des JDBC 2 API (die Schnittstelle java.sql.SQLData) oder des Oracle API
(die Schnittstelle oracle.sql.ORAData).
Die zu erstellende Klasse muss eine dieser Schnittstellen implementieren. Die Verwendung
des Oracle APIs hat den Vorteil, dass auch Referenzen als eigene Klassen implementiert
werden können. Bei Benutzung von JDBC 2 können die Referenzen nur als java.sql.Ref
abgebildet werden.
Damit die Objekte vom JDBC-Treiber auch konvertiert werden können, muss der UDTTypname und die entsprechende Java-Klasse dem Treiber bekannt gemacht werden:
Connection con = DriverManager.getConnection(…);
Map typeMap = con.getTypeMap();
typeMap.put(“CASH.ADDRESS_T”,
Class.forName(„cash.oracle.types.AddressT”));
con.setTypeMap(map);
Listing 5-4: Registrierung von benutzerdefinierten Datentypen
Listing 5-5 bildet den UDT Address_t als Klasse AddressT ab. Dazu notwendig ist die
Kenntnis der inneren Struktur des UDT, also die Anzahl Attribute, deren Reihenfolge sowie
deren Datentypen. Das Beispiel verwendet die Schnittstelle java.sql.SQLData:
package cash.oracle.types;
import java.sql.*
public class AddressT implements SQLData {
protected String sql_type = null;
private
private
private
private
private
private
String street = null;
int houseNr;
String zip = null;
String city= null;
String state = null;
String country = null;
public String getSQLTypeName() throws SQLException { return
this.sql_type; }
public void readSQL(SQLInput stream, String typeName)
throws SQLException {
sql_type = typeName;
street = stream.readString();
houseNr = stream.readInt();
zip = stream.readString();
city = stream.readString();
state = stream.readString();
38
country = stream.readString();
}
public void writeSQL(SQLOutput stream)
throws SQLException {
stream.writeString(this.street);
stream.writeInt(this.houseNr);
stream.writeString(this.zip);
stream.writeString(this.city);
stream.writeString(this.state);
stream.writeString(this.country);
}
// getters and setters for attributes
…
}
Listing 5-5: Die Klasse cash.oracle.types.AddressT
Die drei Methoden getSQLTypeName, readSQL und writeSQL der Schnittstelle
SQLData sollten dabei nicht von Benutzeranwendungen aufgerufen werden. Diese
Methoden werden nur vom JDBC-Treiber benutzt. Die Methode readSQL wird vom Treiber
beim Aufruf der Methode getObject von einem ResultSet-Objekt ausgeführt. Ähnlich
wird die Methode writeSQL benutzt, wenn die Methode setObject eines
PreparedStatement-Objektes aufgerufen wird (d.h. Änderungen von Attributwerten müssen
mit einer Update-Anweisung in der DB gespeichert werden!). Der Zugriff auf die
Attributwerte kann z.B. über die get- und set-Methoden erfolgen. Der folgende
Programmausschnitt wählt die Adressen aller Mitgliedern aus und gibt diese anschliessend
aus:
…
ResultSet rs = stmt.executeQuery(
“SELECT m.getAddress() FROM member m”);
while (rs.next()) {
AddressT address = (AddressT) rs.getObject(1);
System.out.println(address.getStreet() + " "
+ address.getHouseNr());
System.out.println(address.getZip() + " "
+ address.getCity());
}
…
Listing 5-6: Strenge Typisierung
Die im DB-Schema definierte Klassenhierarchie kann auch in Java ohne Probleme
nachgebildet werden. Dazu müssen in der erbenden Klasse einfach die neuen Attribute
hinzugefügt werden, sowie die drei Methoden der Schnittstelle SQLData überschrieben
werden.
39
B.
Abbildung von Methoden
Die Transformation von Methoden von UDTs nach Klassenmethoden ist in den erwähnten
Schnittstellen nicht definiert. Deshalb kann der Zugriff auf Methoden nur innerhalb einer
‚normalen’ SQL-Anfrage realisiert werden. Dafür wird aber für JDBC ein Connection-Objekt
bzw. für SQLJ ein Kontext-Objekt benötigt. Listing 5-7 zeigt die Methode intersects()
der Klasse cash.oracle.types.IntervalTyp. Man bezeichnet solche Methoden
auch als Wrapper Methoden (wrapper methods). Für den DB-Zugriff wird hier SQLJ
verwendet.
…
public boolean intersects (IntervalTyp other)
throws SQLException {
IntervalTyp temp = this;
short result;
#sql [getConnectionContext()] {
BEGIN
:OUT result := temp.INTERSECTS(
:other);
END;
};
return (result != 0);
}
…
Listing 5-7: Eine Wrapper Methode
Für Methoden und Funktionen, welche in UDTs als Java Stored Procedures definiert wurden,
ist natürlich der direkte Aufruf dieser statischen Methoden sinnvoller. So benötigt man keinen
weiteren DB-Zugriff und auch die mehrmalige Konvertierung der Input- und Rückgabewerte
zwischen SQL und Java entfällt.
5.2.3. Werkzeugunterstützung
Wie schon mehrfach erwähnt, ist die Abbildung von DB-Objekten zu Java-Klassen ziemlich
mühsam:
•
Die UDTs müssen auf der Java-Seite nochmals kreiert werden. Dazu ist die Kenntnis
der Struktur des jeweiligen UDT notwendig.
•
Das manuelle Erstellen dieser Java-Klassen ist zeitraubend und fehleranfällig.
Mit geeigneten Werkzeugen kann man diese Probleme umgehen.
JPublisher ist ein Werkzeug von Oracle zur automatischen Erzeugung von Java Klassen,
welche folgende benutzerdefinierten Datenbankentitäten in einer Java-Anwendung abbilden:
•
UDTs
•
Referenztypen (REF types)
•
SQL-Sammlungen wie VARRAY oder verschachtelte Tabellen (nested tables)
•
PL/SQL Pakete
JPublisher selbst ist komplett in Java geschrieben.
40
Für diese Arbeit von besonderem Interesse ist der erste Fall, also die Abbildung von UDTs
(durch eine strenge Typisierung). JPublisher erstellt dabei für jedes Attribut die entsprechenden get- und set-Methoden. Optional ist auch die Generierung der WrapperMethoden möglich, welche die Methoden und Funktionen der UDTs aufrufen (Listing 5-7 ist
ein Beispiel dafür). Wrapper-Methoden werden dabei in SQLJ implementiert, d.h. es gibt eine
Datei mit der Endung .sqlj. Falls keine Wrapper-Methoden erstellt werden sollen oder es sich
um eine Referenztyp (REF type) handelt, dann wird eine ‚normale’ .java-Datei erstellt.
PL/SQL Pakete werden immer als .sqlj-Dtei erstellt (vergleiche die Klassen FleetManager, MemberManager und LocationManager im Paket cash.oracle.services).
Für die Abbildung eines UDT stehen die erwähnten Schnittstellen SQLData oder ORAData
zur Verfügung. Standardmässig wird die Oracle-Schnittstelle verwendet. In diesem Fall
erstellt JPublisher für einen UDT immer zwei Dateien: 1) eine Java Klasse des UDT und 2)
eine Java Klasse für die entsprechende Referenz. Wird die Schnittstelle SQLData verwendet,
so können nur die Objekttypen erstellt werden. Die Referenzen und auch die Sammlungen
müssen dann als java.sql.Ref und java.sql.Array benutzt werden.
Bei der Verwendung der generierten Klassen kann es nützlich sein diese Klassen zu
erweitern. Das ist vor allem dann sinnvoll, wenn man noch weiteres Verhalten hinzufügen
möchte. Auch Schema-Änderungen sind so direkt sichtbar, ohne dass man das eigene
Verhalten nochmals implementieren muss (bzw. in die neu erzeugte Datei kopieren muss).
Bei der Typabbildung stehen folgende Optionen zur Verfügung:
•
jdbc: Bei dieser Option werden die Datentypen gemäss Tabelle 5 (S.26) konvertiert.
Also viele numerische Werte werden zu primitiven Javawerten wie int oder float
konvertiert. Für benutzerdefinierte Objekte wird (falls keine Methoden übersetzt
werden) die Schnittstelle java.sql.SQLData implementiert.
•
objectjdbc: Mit dieser Option werden allen Datentypen zu den entsprechenden
Objekten konvertiert. Der Unterschied zu vorhin ist, dass keine primitiven Datentypen
verwendet werden.
•
bigdecimal: Alle numerische Datentypen werden als java.math.BigDecimal
abgebildet.
•
oracle: Diese Option bildet, ähnlich wie objectjdbc, alle Datentypen als Objekte ab,
verwendet aber die Datentypen aus dem Paket oracle.sql. Für benutzerdefinierte
Objekte werden die Schnittstellen ORAData und ORADataFactory implementiert.
Die Typabbildung wird in drei Kategorien unterteilt. Dies ermöglicht eine bessere
Konvertierung der unterschiedlichen Typen (vgl. Tabelle 6).
Option
Beschreibung
Standardwert
-usertypes
Bestimmt die Konvertierung für benutzerdefinierte SQL Typen.
Mögliche Werte: jdbc oder oracle
oracle
-numbertypes
Bestimmt die Konvertierung von numerischen SQL Typen.
Mögliche Werte: jdbc, objectjdbc, bigdecimal, oracle
objectjdbc
-lobtypes
Bestimmt die Abbildung von LOBs.
Möglich Werte: jdbc oder oracle
oracle
Tabelle 6: Möglichkeiten bei der Typabbilung
41
5.3. Gesamtbild
In diesem Abschnitt möchte ich noch das Gesamtbild der Carsharing-Anwendung aufzeigen.
Die Architektur der Anwendung ist so ausgelegt, dass die Endanwendung unabhängig von
einem bestimmten DBMS funktioniert (vgl. Abbildung 8). Dies wird durch die Definition von
Schnittstellen erreicht. Die Endanwendung verwendet dabei nur diese Schnittstellen. Im
Entwurf gibt es zwei Arten von Schnittstellen:
1. Schnittstellen für persistente Objekte (die Java-Klassen von den UDTs)
2. Schnittstellen für Dienstfunktionalität (CashServices)
Die Schnittstellen sind dabei so zu definieren, dass sie das gewünschte Verhalten und
Funktionalität vollständig abdecken, ohne dabei schon Annahmen über eine Implementierung
zu treffen. Konkrete Implementierungen der Schnittstellen hängen vom jeweiligen DBMS ab.
Somit ist es nicht notwendig, die Endanwendung neu zu programmieren, wenn sich das
DBMS ändert oder ein neues hinzukommt (siehe Geppert 362).
Im Falle der Carsharing-Anwendung sind die Schnittstellen im Paket cash definiert. Im
Rahmen dieser Arbeit wurden die beiden bestehenden Implementation für DB2 und
FastObjects (in Abbildung 8 nicht dargestellt) durch eine Oracle-Implementation erweitert.
Die konkreten Implementationen der Schnittstellen für Oracle befinden sich in den Paketen
cash.oracle.types (persistente Objekte) und cash.oracle.services (Dienstfunktionalität).
Anwendung
Zugriff und Manipulation
CashServices
IMember
ILocation
IVehicle
IReservation
…
Implementation
Member
Location
Vehicle
Reservation
Member
Location
Vehicle
…
Reservation
…
Transformation
Oracle
DB2
Abbildung 8: Architektur der Carsharing-Anwendung
Betrachten wir als Beispiel die Aufnahme eines neuen ‚normalen’ Mitglieds (PersonMember): Über eine Maske werden die benötigten Angaben (Name, Adresse, usw.)
eingegeben. Die Methode addMember von CashAdmin (ein Teil der Endanwendung) ruft
nun die Methode addPersonMember von CashAdminServices auf, welche das neue
persistente Mitglied zurückliefert (Listing 5-8).
42
…
IMember newMember;
…
if (memberType.equals("person")) {
newMember = cashServices.addPersonMember(conn,
firstName, lastName, dobCal.getTime(), address,
bankAccount, homeLocation, password, accident);
else (memberType.equals(“company”)) {
…
}
…
if (newMember != null) {
out.println(newMember.toHTML());
}
…
Listing 5-8: Ausschnitt aus cash.servlet.CashAdmin
CashAdminServices ist aber eine Schnittstelle, die keine konkreten Methoden besitzt.
Während der Laufzeit der Anwendung werden die abstrakten Schnittstellen durch konkrete
Implementationen „ersetzt“. Eine konkrete Umsetzung von CashAdminServices ist
OracleCashAdminServices. Die Methode addPersonMember ist in Listing 5-9
dargestellt. Das Speichern des Mitglieds in der DB erfolgt über eine PL/SQL-Prozedur. Die
Prozedur vereinfacht die Speicherung, da diese aus mehren Anweisungen besteht. Der Aufruf
erfolgt im SQLJ-Block.
public IMember addPersonMember(Object conn, String fname,
String lname, Date dob, IAddress iAddress, IBankAccount
iAccount, String location, String password, boolean
accident) throws Exception {
try {
CashContext ctxt = new CashContext((Connection)conn);
// get shortsign for homelocation
location = getLocation(ctxt, location).getShortSign();
AddressTyp address = (AddressTyp)iAddress;
add.setConnectionContext(ctxt);
BankAccountTyp account = (BankAccountTyp)iAccount;
bank.setConnectionContext(ctxt);
int memberNr = -1;
#sql [ctxt] membernr = { VALUES(
MEMBER_MANAGER.ADDPERSONMEMBER(
:fname, :lname, :address, :dob, :account,
:password, :location))
};
return getMember(ctxt, membernr);
} catch(Exception e) {
logException("addPersonMember", e);
throw e;
}
}
Listing 5-9: Implementation von addPersonMember
43
5.4. Bewertung
5.4.1. Oracle9i
Oracle9i ist ein objektrelationales Datenbanksystem, dass viele vom SQL:99-Standard
geforderte Eigenschaft abdeckt und auch darüber hinaus geht (z.B. bei den SQLSammlungen). Die fehlende Tabellenhierarchie wird durch eine Sichtenhierarchie
kompensiert. Diese Lösung ist besonders in Bezug auf bereits vorhandene (relationale) Daten
praktisch: Diese Daten müssen nicht in eine neue typisierte Tabellen exportiert werden,
sondern können in ihren relationalen Tabellen verbleiben. Zur Implementation von
benutzerdefinierten Routinen steht mit PL/SQL eine mächtige Programmiersprache zur
Verfügung. Aber auch die enge Einbindung von Java sticht hervor. Dies zeigt sich z.B. in
einer eigenen Java Virtual Machine.
5.4.2. Typabbildung
Wie schon erwähnt, ist die Benutzung der objektrelationalen Eigenschaften von Datenbanken
in Java ohne weitere Vorkehrungen und Massnahmen nicht möglich.
Die Abbildung von UDTs kann auf zwei Arten erfolgen. Die schwache Abbildung der
Objekttypen durch die JDBC 2.0 Schnittstellen Struct, Ref und Array ist nur für
generische Programme zu empfehlen (z.B. ein Werkzeug zum Manipulieren von
unterschiedlichsten SQL-Daten). Für eine anwendungsspezifische Anbindung der SQLDatentypen in Java ist die JDBC 2.0 Schnittstelle SQLData zu empfehlen. Die Schnittstelle
übernimmt dabei elementare Datenbank-Operationen, wie das Einlesen und Zurückschreiben
der Daten. So ist es nicht mehr notwendig die Objektstruktur beim Datenaustausch zu
zerlegen. Bei beiden Methoden ist aber die Kenntnis des Aufbaus des UDTs notwendig.
Oracle bietet gegenüber JDBC 2.0 einige interessante Erweiterungen, vor allem durch die
Schnittstellen ORAData und ORADataFactory. Diese Schnittstellen erlauben eine noch
bessere Anbindung der objektrelationalen Eigenschaften an Java (Referenzen und
Sammlungen können auch als spezialisierte Klassen erstellt werden). Dabei ist das Werkzeug
JPublisher eine grosse Hilfe beim Erstellen der benutzerdefinierten Java Klassen, auch wenn
etwas ‚Handarbeit’ übrig bleibt.
Die Oracle-Lösung (Oracle9i DBS mit JPublisher) scheint mir auf dem richtigen Weg. So
kann man das DB-Schema praktisch unverändert in die Java-Welt übernehmen. Es bleibt
‚nur’ noch die Anwendungslogik sowie die Präsentation zu entwickeln.
6. Schlussfolgerungen und Ausblick
Die grössten Schwierigkeiten beim Einsatz von relationalen Datenbanken und objektorientieren Programmiersprachen entstehen dadurch, das zwei unterschiedliche Konzepte
aufeinander treffen. In Kapitel 2.3 (S. 10) wurden diese Unterschiede aufgezeigt. Dieser
impedance mismatch bereitet in der Anwendungsentwicklung einen erheblichen
Zusatzaufwand. Bonvanie beziffert diesen auf 40 %.
Das objektrelationale Modell möchte die Vorteile der Objektorientierung im relationalen
Modell nutzbar machen In der Praxis spielt der SQL:99-Standard eine wesentlicheRolle. Im
Vordergrund stehen dabei mehr die praktische Umsetzung der objektorientierten
Eigenschaften und der Erhalt der bisherigen (relationalen) Strukturen. Ein formales Modell
44
fehlt dem Standard. Trotzdem bietet SQL:99 einen guten Ansatz zur Integration von objektorientierten Eigenschaften in relationalen Datenbanksystemen.
Die Verwendung objektrelationaler Erweiterungen ist – mit einigem Aufwand – in Java
nutzbar. JDBC 2.0 bietet (zwar noch nicht zwingend) die Unterstützung der SQL:99-Datentypen an. Auch benutzerdefinierte SQL-Typen können abgebildet werden (vgl. 5.2 S. 35).
SQLJ bietet ebenfalls einige gute Erleichterungen (SQLJ-0) und Erweiterungen (SQLJ-1) an.
Oracle bietet mit Oracle9i eine objektrelationale Datenbank an, die dem Standard schon ein
bisschen voraus ist. Insbesondere lassen sich Beziehungen vom Muster 1:n und n:m mit
geschachtelten Tabellen direkt abbilden. In SQL:99 ist dies nur beschränkt möglich (wenn die
Anzahl bekannt ist). Ebenfalls in die richtige Richtung zeigt das Werkzeug JPublisher, das
automatisch die UDTs in Java-Klassen übersetzt. Bei der Verwendung des ‚Oracle-Pakets’
lässt sich der Entwicklungsaufwand für die Integration der Daten deutlich unter die 40 %Marke drücken.
Die Erweiterung des relationalen Modells hin zum objektrelationalen Modell ist aus meiner
Sicht eine evolutionäre Entwicklung, die sich aus den beiden Extremen ‚relational’ und
‚objektorientiert’ ergibt. Wie immer, wenn man aus zwei Welten das Beste möchte, muss man
zu Kompromissen bereit sein. So muss man sich entscheiden, was und was nicht man
mitnehmen will. Denn alles kann man nicht übernehmen. Wir können also von einer objektrelationalen DB nicht erhoffen, dass sie beide Modelle zu 100 % unterstützt.
Die semantische Lücke wird durch objektrelationale DBS auf formaler Seite sicherlich
kleiner. Auf der praktischen Seite hängt die Integration von der Unterstützung durch
Werkzeuge (wie z.B. JPublisher) ab. Will man die semantische Lücke komplett schliessen, so
bleibt nur eines: objektorientierte Datenbanken - und dann kommen wieder andere Aspekte
(z.B. Performance) ins Spiel!
Der aktuelle Entwicklungsstand bei den objektrelationalen Datenbanken, vor allem auf Seite
des Standards, lässt noch viel Platz für weitere Entwicklungen und Verbesserungen offen.
45
7. Anhang
7.1. Carsharing Klassendiagramm
Abbildung 9: Klassendiagramm der Carsharing Anwendung (siehe auch auf der CD)
46
7.2. Abbildungsverzeichnis
Abbildung 1: Beispiel für eine einfache Vererbung................................................................... 5
Abbildung 2: Die Relation "Person" mit vier Attributen und fünf Tupeln ................................ 8
Abbildung 3: Relation ‚Telefon’ mit Fremdschlüssel ‚Student’................................................ 9
Abbildung 4: Die Architektur von JDBC................................................................................. 16
Abbildung 5: Die JDBC Schnittstellen .................................................................................... 19
Abbildung 6: Kompilieren von SQLJ-Programmcode............................................................. 28
Abbildung 7: Die Modellierung von Rechnungen ................................................................... 33
Abbildung 8: Architektur der Carsharing-Anwendung............................................................ 40
Abbildung 9: Klassendiagramm der Carsharing Anwendung (siehe auch auf der CD) .......... 44
47
7.3. Listing-Verzeichnis
Listing 2-1: Eine einfache Klassendefinition ............................................................................. 4
Listing 2-2: Polymorphie ........................................................................................................... 5
Listing 2-3: Eine abstrakte Klasse.............................................................................................. 6
Listing 2-4: Abbleitung einer konkreten Klasse von einer abstrakten ....................................... 7
Listing 2-5: Die Verwendung von abstrakten Klassen............................................................... 7
Listing 2-6: Beispiel zur Typumwandlung................................................................................. 7
Listing 3-1: UDT-Definition .................................................................................................... 12
Listing 3-2: UDT mit Referenztyp ........................................................................................... 13
Listing 3-3: Tabellendefinition................................................................................................. 13
Listing 3-4: Festlegen des Wertebereichs einer Referenz ........................................................ 13
Listing 3-5: Spezialisierung im SQL:99-Standard ................................................................... 13
Listing 3-6: Definition einer Subtabelle................................................................................... 14
Listing 3-7: Dereferenzierung in Anfragen.............................................................................. 14
Listing 3-8: Beispiel für einen Pfadausdruck........................................................................... 14
Listing 3-9: only Operator........................................................................................................ 14
Listing 4-1: Beispiele für die Erstellung von DB-Verbindungen ............................................ 17
Listing 4-2: Stapelaktualisierungen.......................................................................................... 21
Listing 4-3: Bearbeiten einer Ergebnismenge .......................................................................... 22
Listing 4-4: Ein Beispiel für aktualisierbare Ergebnismengen ................................................ 23
Listing 4-5: Die Syntax für den Aufruf einer gespeicherten Prozedur .................................... 25
Listing 4-6: Der Aufruf einer gespeicherten Prozedur............................................................. 25
Listing 4-7: Verwendung von java.sql.Struct........................................................................... 27
Listing 4-8: Die Klasse MemberUtil........................................................................................ 30
Listing 4-9: Ein Beispiel für eine Aufrufspezifikation............................................................. 30
Listing 4-10: Anwendungsbeispiele für benutzerdefinierte Funktionen.................................. 30
Listing 5-1: Abfrage von Attributen und Methoden von Subtypen ......................................... 34
Listing 5-2: Sichtendefinition................................................................................................... 35
Listing 5-3: Abfrage auf einer Subtabelle ................................................................................ 35
Listing 5-4: Registrierung von benutzerdefinierten Datentypen.............................................. 36
Listing 5-5: Die Klasse cash.oracle.types.AddressT................................................................ 37
Listing 5-6: Strenge Typisierung.............................................................................................. 37
Listing 5-7: Eine Wrapper Methode......................................................................................... 38
Listing 5-8: Ausschnitt aus cash.servlet.CashAdmin............................................................... 41
Listing 5-9: Implementation von addPersonMember............................................................... 41
48
7.4. Tabellenverzeichnis
Tabelle 1: Eigenschaften des objektorientieten Modells............................................................ 8
Tabelle 2: Unterschiede zwischen dem relationalen und dem objektorientierten Modell ....... 10
Tabelle 3: Auftreten von einigen Transaktionsproblemen in den jeweiligen
Abschottungsgraden ......................................................................................................... 20
Tabelle 4: Konvertierung von Java- nach JDBC-Typen .......................................................... 26
Tabelle 5: Konvertierung von JDBC- nach Java Typen .......................................................... 26
Tabelle 6: Möglichkeiten bei der Typabbilung ........................................................................ 39
49
7.5. Quellenverzeichnis
Bonvanie, Rene. „Advances in Java’s Relationship to Data“. Oracle Magazine, XVII. 1
(2003): 58 – 60.
Finsterwalder, Malte. Anbindung objektorientierter Software an objektrelationale
Datenbanken. Universität Hamburg, Fachbereich Informatik, Oktober 2002.
Geppert, Andreas. Objektrelationale und objektorientierte Datenbankkonzepte und –systeme.
dpunkt.verlag, 2002.
Horstmann, Cay S. & Cornell, Gary. Core Java™2 Band II – Expertenwissen. München:
Markt+Technik Verlag, 2000.
Krüger, Guido. Go To Java 2. Bonn: Addison-Wesley-Longman, 1999.
Pöschmann, Thomas. “Wartungsintervall”. Java Magazin. 2 (2001): 24 – 26.
White, Seth et al. JDBC™ API Tutorial and Reference: Universal Data Access for the
Java™ 2 Platform. 2. Auflage, Addison Wesley, 1999.
White, Seth & Hapner Mark. JDBC™ 2.1 API. Version 1.1, Final Specification. Sun
Microsystems Inc., Palo Alto, CA, Oktober 1999.
Oracle-Handbücher:
Oracle9i: JPublisher User’s Guide. Release 2 (9.2), März 2002. Part No. A96658-01.
Oracle9i: JDBC Developer’s Guide and Reference. Release 2 (9.2), März 2002. Part No.
A96654-01.
Oracle9i: SQLJ Developer’s Guide and Reference. Release 2 (9.2), März 2002. Part No.
A96655-01.
Oracle9i: Java Stored Procedures Developer’s Guide. Release 2 (9.2), März 2002. Part No.
A96659-01
Oracle9i: SQL Reference. Release 2 (9.2), März 2002. Part No. A96540-01
Oracle9i: PL/SQL User’s Guide and Reference. Release 2 (9.2), März 2002. Part No.
A96624-01
50
Herunterladen