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