7 Was ist SQLJ? Im ersten Teil dieses Kapitels werden wir etwas über die Einbettung der SQL-Sprache im Allgemeinen sagen. Das ist aus zwei Gründen notwendig: erstens, um ein generelles Bild bei der Einbettung zu geben und zweitens, um grundsätzliche Unterschiede zwischen SQLJ und allen anderen eingebetteten Sprachen (wie ESQL/C++, ESQL/ Ada usw.) zu zeigen. Der Teil des Kapitels, der sich mit SQLJ befasst, zeigt verschiedene Möglichkeiten, wie diese Programmierschnittstelle verwendet werden kann. Zuerst wird die Implementierung einer einfachen DDL-Anweisung gezeigt, und danach die Programmierung der SELECT-Anweisung (mit und ohne Iterator). Des Weiteren werden Beispiele mit einem FOR UPDATE-Iterator und die Einbettung der Aufrufe in eine gespeicherte Prozedur vorgestellt. 7.1 Einbettung der SQL-Sprache eine Einführung Die Notwendigkeit, die SQL-Sprache einzubetten, ergibt sich daraus, dass es sich bei dieser Sprache um eine rudimentäre (non computational complete) Programmiersprache handelt, d.h. eine Sprache die nicht allein ausreicht, um komplexe Datenbankanwendungen zu schreiben. Eine Möglichkeit, Datenbankanwendungen zu schreiben, bietet die Einbettung der SQL-Sprache in eine prozedurale Programmiersprache wie C und C++. Dabei wird ein Vorübersetzer verwendet, der jede SQL-Anweisung übersetzt. Nach der Vorübersetzerphase wird ein Programm mit einem einheitlichen Code erzeugt, das anschließend übersetzt werden kann. (Die prozedurale Programmiersprache, in die SQL eingebettet wird, wird Host-Sprache genannt.) Hinweis Generell existiert auch eine zweite Möglichkeit, Datenbankanwendungen zu implementieren. Sie heißt CLI (Call Library Interface). Der wichtigste Unterschied zwischen diesen zwei Methoden (Einbettung und CLI) ist, dass für die Einbettung ein Vorübersetzer verwendet wird. Die in Kapitel 4 beschriebene Datenbanksprache JDBC ist ein Beispiel für CLI. 246 7 Was ist SQLJ? 7.1.1 Kennzeichnung der SQL-Anweisungen Innerhalb eines Datenbankprogramms, das mit Hilfe der Einbettung der SQL-Sprache implementiert ist, befinden sich zwei verschiedene Arten von Anweisungen: - Anweisungen der SQL-Sprache - Anweisungen der Host-Sprache. Damit ein Vorübersetzer die eingebetteten SQL-Anweisungen von den Anweisungen der Host-Sprache unterscheiden kann, ist es erforderlich, alle SQL-Anweisungen besonders zu kennzeichnen. Dabei muss sowohl der Anfang als auch das Ende einer SQL-Anweisung gekennzeichnet werden. Bei verschiedenen Host-Sprachen existieren unterschiedliche Notationen für eingebettete SQL-Anweisungen. Folgende Übersicht zeigt diese Unterschiede: Anfang der Anweisung: EXEC SQL (alle Sprachen, wie C, Ada, usw.) #sql {....} (nur Java) Ende der Anweisung: (unterschiedlich für verschiedene Sprachen) ";" (C, C++, Java) END-EXEC (Cobol) 7.1.2 Host-Variablen In einem Datenbankprogramm werden am häufigsten die DML-Anweisungen (SELECT, UPDATE, INSERT und DELETE) verwendet. Jede dieser Anweisungen ermöglicht mit Hilfe von Variablen den Datenaustausch zwischen dem Anwendungsprogramm einerseits und der Datenbank andererseits. In einer eingebetteten SQL-Anweisung werden solche Variablen Host-Variablen genannt. (Neben dem Begriff Host-Variablen existiert auch der Begriff des Host-Ausdrucks, der komplexer als eine Host-Variable ist, weil dabei eine oder mehrere Variablen innerhalb eines Ausdrucks geschrieben werden können.) Hinweis Manche Host-Sprachen wie C und C++ unterstützen zusätzlich zu den Host-Variablen auch die sogenannten Indikator-Variablen. Weil SQLJ diese Variablen nicht kennt, werden wir sie in diesem Buch nicht näher beschreiben. Host-Variablen dienen im Allgemeinen als Zielvariablen einer Abfrage bzw. können anstelle einer Konstante in den DML-Anweisungen verwendet werden. Sie können nur die Datentypen haben, die in der entsprechenden Host-Sprache erlaubt sind. Einbettung der SQL-Sprache eine Einführung 247 Host-Variablen können u.a. in folgenden Klauseln erscheinen: - INTO-Klausel einer SELECT bzw. FETCH-Anweisung - WHERE-Klausel - HAVING-Klausel - SET-Klausel der UPDATE-Anweisung - VALUES-Klausel der INSERT-Anweisung. 7.1.3 Eingebettete SQL-Anweisungen und der Cursor Ein Problem, das generell zwischen der SQL-Sprache und einer Host-Sprache existiert, ist, dass SQL eine mengenorientierte und alle Host-Sprachen satzorientierte Sprachen sind. Der Begriff »mengenorientiert« heißt, dass eine einzige SQL-Anweisung (SELECT z.B.) eine Menge von Reihen auf einmal bearbeitet, wobei der Benutzer in der Regel nicht weiß, wie viele Reihen eine solche Anweisung lesen bzw. ändern wird. Im Unterschied zu SQL können Host-Sprachen nicht gleichzeitig mehrere Datensätze bearbeiten, sondern holen sich immer einen Datensatz nach dem anderen. (Solche Sprachen werden dementsprechend satzorientiert genannt.) Damit der Unterschied zwischen SQL und einer Host-Sprache überbrückt werden kann, wird ein Datenpuffer benutzt, in welchem dann alle Ergebnisreihen einer SQL-Anweisung gespeichert werden. Der Puffer wird mit einem Zeiger verbunden, der für die Abarbeitung der Reihen im Puffer verwendet wird. Der Zeiger wird Cursor genannt. Um einen Cursor in einem Programm zu benutzen, muss generell: - der Cursor deklariert werden - der Cursor geöffnet werden - die Ergebnismenge satzweise abgearbeitet werden - der Cursor geschlossen werden. Hinweis Die Verarbeitung einer Ergebnismenge bei SQLJ ist, wie wir noch sehen werden, teilweise anders als bei anderen eingebetteten Sprachen. SQLJ verwendet Iteratoren anstatt eines Cursors. 248 7 Was ist SQLJ? 7.1.4 Die dynamische SQL-Sprache Die eingebettete SQL-Sprache kann im Allgemeinen: - statisch - dynamisch sein. Wenn eine SQL-Anweisung explizit in einem Datenbankanwendungsprogramm eingebettet wird, d.h. wenn die Anweisung zur Übersetzungszeit (bis auf Werte der HostVariablen) bekannt ist, reden wir von der statischen SQL-Anweisung bzw. statischen SQL-Sprache. Bei der dynamischen SQL-Sprache werden SQL-Anweisungen erst zur Laufzeit dem Programm bekannt gemacht. Hinweis SQLJ kann nur statisch verwendet werden. (Es ist aber durchaus möglich, durch die Kombination von SQLJ und JDBC dynamisches SQL zu simulieren.) 7.2 SQLJ die Grundlagen SQLJ ist eine eingebettete Sprache, die, im Unterschied zu den anderen entsprechenden Sprachen (wie ESQL/C++ oder ESQL/Ada) sehr eng in der eigenen Host-Sprache (Java) integriert ist. Diese enge Verbundenheit mit der Host-Sprache zeigt sich besonders dadurch, dass Java-Klassen als Datentypen der Tabellenspalten verwendet werden können und in der Definition des Iterator-Konzeptes. (Beide Themen werden später in diesem Kapitel ausführlich erörtert.) Ein wichtiger Vorteil von SQLJ ist, dass es sich um eine standardisierte Sprache handelt. Im Unterschied zu der Standardisierung der SQL-Sprache, wo das ANSI-Gremium zuerst die Funktionalität festlegt und diese dann von den einzelnen Herstellern implementiert wird, hat bei SQLJ ein Konsortium aus mehreren Datenbankherstellern (Oracle, Compaq/Tandem, JavaSoft, IBM, Informix und Sybase) die Grundlagen der Sprache festgelegt, die dann vom ANSI-Gremium übernommen wurden. Das Ergebnis ist eine Spezifikation (ANSI-Standard X3.135 »Information technology – Database languages – SQL – Part 10: SQL/OLB), die in drei Teile unterteilt ist: Teil 0, Teil 1 und Teil 2. Teil 0 beschreibt die Syntax für die statische SQL-Sprache. Er beinhaltet: - SELECT - INSERT, UPDATE, DELETE (mit UPDATE WHERE CURRENT OF und DELETE WHERE CURRENT OF) SQLJ die Grundlagen 249 - DDL-Anweisungen (CREATE, DROP und ALTER) - GRANT und REVOKE. Teil 1 beschreibt, wie portierbare Java-Prozeduren und benutzerdefinierte Funktionen implementiert werden können. Teil 2 spezifiziert die Schnittstelle zwischen einem Datenbanksystem und Java, die die Abbildung von Java-Klassen für benutzerdefinierte SQL-Datentypen ermöglicht. Ein wichtiges Ergebnis der Bemühungen der Datenbankhersteller ist die Implementierung eines gemeinsamen Translators (Vorübersetzers), mit dem ein einheitlicher Code auf der SQLJ-Quellebene für alle Datenbanksysteme erstellt werden kann. (Die Beschreibung des Translators siehe später in diesem Kapitel.) Abbildung 7.1: Übersetzung und Ablauf eines SQLJ-Programms 250 7 Was ist SQLJ? Die SQLJ-Sprache besteht aus einer Menge von Klauseln, die die Java-Sprache erweitern. (Eine SQLJ-Klausel ist nichts anders als eine SQL-Anweisung, die in einem SQLJProgramm nach dem Präfix #sqlj innerhalb der geschweiften Klammern geschrieben wird.) Mit Hilfe von SQLJ kann man Datenbankanwendungen sowohl auf der Clientals auch auf der Server-Seite implementieren. Weil solche Anwendungen in der JavaSprache geschrieben sind, sind sie portierbar und können mit Datenbanken unterschiedlicher Hersteller kommunizieren. Die Phasen, die ein SQLJ-Programm von der Vorübersetzerphase bis zu seiner Ausführung durchläuft, sehen etwas anders aus, als solche Phasen bei den anderen eingebetteten SQL-Sprachen. Abbildung 7.1 zeigt dies. Wie aus Abbildung 7.1 ersichtlich, wird ein SQLJ-Programm mit Hilfe eines Translators vorübersetzt. Während der Vorübersetzungsphase werden die eingebetteten SQLJAnweisungen durch die Aufrufe zur SQLJ-Laufzeitbibliothek ersetzt. Das Ergebnis dieser Phase ist ein Java-Programm, das von jedem Java-Compiler (oder Interpreter) übersetzt werden kann. Der übersetzte Java-Bytecode wird von der SQLJ-Laufzeitbibliothek in ein JDBC-Programm umgewandelt, das schließlich mit Hilfe eines JDBC-Treibers die Aufrufe auf die entsprechende Datenbank realisiert. Wie aus dem oben beschriebenen ersichtlich ist, enthält SQLJ zwei Grundkomponenten: - den Translator - die SQLJ-Laufzeitbibliothek. Der SQLJ-Translator ist in Java geschrieben, und seine Aufgabe ist, das erstellte SQLJQuellprogramm zu übesetzen. (Wie wir schon erwähnt haben, verwenden alle Datenbanksysteme den gleichen Translator.) Das Ergebnis dieser Phase sind eine .java-Datei und ein oder mehrere SQLJ-Profile (siehe unten), die die Information über die SQLOperationen enthalten. Die SQLJ-Laufzeitbibliothek ist genauso wie der Translator in Java geschrieben. Ihre Aufgabe ist, die Aktionen der im Programm befindlichen SQL-Anweisungen zu implementieren und auf die Datenbank (mit Hilfe von JDBC) zuzugreifen. Zwei weitere SQLJ-Komponenten sind: - Customizer - SQLJ-Profile. Customizer ist eine Komponente, die automatisch von SQLJ aufgerufen wird, um SQLJProfile für die entsprechende Datenbank und herstellerspezifische Funktionen und Datentypen anzupassen. Ein SQLJ-Profil ist eine Sammlung von Eintragungen, in der SQLJ die Grundlagen 251 jede Eintragung genau einer SQL-Operation entspricht. Damit transformiert ein SQLJProfil die eingebetteten SQL-Anweisungen in die ausführbaren Anweisungen. Aus diesem Grund muss jedes Profil mit Hilfe des Customizers angepasst (customized) sein. Ein SQLJ-Translator generiert ein Profil für jeden Verbindungskontext einer Datenbankanwendung. (Ein Verbindungskontext kennzeichnet die Umgebung einer Datenbank, zu der das Programm die Verbindung herstellt.) Die Dateinamen der SQLJProfile haben standardmäßig das Suffix .ser, können aber optional in die .class-Dateien umgewandelt werden. Eine wichtige Eigenschaft der SQLJ-Profile ist, dass sie portierbar sind. Das heißt, dass ein Profil aus einer herstellerspezifischen Umgebung (z.B. Oracle auf Windows NT) zu einer anderen (z.B. DB2 auf OS/390) übertragen werden und nach dem durchgeführten Anpassungsprozess einwandfrei in der neuen Umgebung verwendet werden kann. 7.2.1 Vorteile von SQLJ im Vergleich zu JDBC Der wichtigste Unterschied zwischen den beiden Sprachen ist, dass SQLJ eine statische und JDBC eine dynamische Sprache ist. (Eine Sprache wird dynamisch genannt, falls der Datenbankprogrammierer mit ihr Datenbankobjekte abfragen und ändern kann, die erst zur Ablaufzeit bekannt sind. Das gilt nicht nur für Datenbankobjekte, sondern auch für ganze SQL-Anweisungen.) Im Unterschied zu JDBC, kann SQLJ nur statisch verwendet werden, d.h. alle SQLAnweisungen und alle in ihnen verwendeten Datenbankobjekte (bis eventuell auf die Werte der Host-Variablen) sind zur Übersetzungszeit bekannt. Obwohl eine dynamische Sprache flexibler als eine statische ist, hat SQLJ mehrere Vorteile im Vergleich zu JDBC. Diese sind: - SQLJ befindet sich auf einem höherem logischen Niveau als JDBC - SQLJ hat eine einfachere Syntax als JDBC - SQLJ kann die Syntax- und Semantikanalyse zur Übersetzungszeit durchführen. Im Unterschied zu JDBC, wo alle SQL-Anweisungen, die in einem JDBC-Programm in Methodenaufrufen versteckt sind, ist jede SQL-Anweisung in einem SQLJ-Programm in ihrer ursprünglichen Form dargestellt und dadurch gleich sichtbar. Aus diesem Grund kann man SQLJ als logisch höhere Programmiersprache (als JDBC) bezeichnen. Um die einfachere SQLJ-Syntax zu zeigen, werden wir ein Beispiel benutzen. Beispiel 7.1 java.sql.PreparedStatement ps = con.prepareStatement(SELECT address FROM emp WHERE name=?); ps.setString(1,name); 252 7 Was ist SQLJ? java.sql.ResultSet names = ps.executeQuery(); names.next(); add = names.getString(1); names.close(); #sql (con) {SELECT address INTO :addr FROM emp WHERE name= :name}; Der erste Teil des Beispiels 7.1 zeigt einen Ausschnitt aus einem JDBC-Programm, in dem eine SELECT-Anweisung mit Hilfe der prepareStatement()-Methode vorbereitet wird und danach die Spaltenwerte den entsprechenden Variablen zugewiesen werden. Um dieselbe Funktionalität mit Hilfe von SQLJ zu implementieren, reicht eine einzige Programmzeile, wie aus dem zweiten Teil des Beispiels ersichtlich ist. SQLJ kann die Syntax- und Semantikanalyse zur Übersetzungszeit durchführen, weil zum Zeitpunkt der Übersetzung die Verbindung zur entsprechenden Datenbank bekannt ist. Jede SQLJ-Klausel steht im Zusammenhang mit einer Datenbankverbindung, welche die Umgebung dieser Anweisung eindeutig definiert. Aus diesem Grund ist es möglich, die Kompatibilität von Java und SQL-Ausdrücken zur Übersetzungszeit zu überprüfen. Im Unterschied zu SQLJ werden in einer JDBC-Umgebung die SQL-Anweisungen nicht zur Übersetzungszeit überprüft, wodurch syntaktische Fehler erst zur Laufzeit erkannt werden. 7.2.2 SQLJ-Anweisungen Alle SQLJ-Anweisungen können in zwei Gruppen unterteilt werden: - Vereinbarungsanweisungen (declaration statements) - Ausführungsanweisungen (executable statements). Hinweis Jede SQLJ-Anweisung hat am Anfang das Präfix #sql. Vereinbarungsanweisungen werden in SQLJ entweder für die Erstellung von IteratorKlassen oder für Verbindungskontexte verwendet. (Beide Begriffe werden später erläutert.) Beispiel 7.2 zeigt eine Vereinbarungsanweisung. Beispiel 7.2 // Iterator deklarieren #sql public iterator iter1 (String, String, double); In Beispiel 7.2 wird mit Hilfe einer Vereinbarungsanweisung der Iterator iter1 deklariert. SQLJ die Grundlagen 253 Ausführungsanweisungen werden benutzt, um eingebettete SQL-Anweisungen auszuführen. Im Unterschied zu den Vereinbarungsanweisungen werden Ausführungsanweisungen innerhalb der geschweiften Klammer geschrieben. In Beispiel 7.3 wird eine Ausführungsanweisung gezeigt. Beispiel 7.3 #sql{ SELECT name,umsatz INTO :name,:umsatz FROM kunden WHERE kundennr = :kundennr }; Beispiel 7.3 zeigt eine SELECT INTO-Anweisung, die unter anderem drei Host-Variablen (name, umsatz und kundennr) enthält. (Eine SELECT INTO-Anweisung stellt eine erweiterte Form der SELECT-Anweisung dar, die speziell in der eingebetteten SQL-Sprache verwendet wird, um Spaltenwerte der aktuellen Reihe den Host-Variablen zuzuweisen. Mit dem Begriff SQLJ-Klausel wird der Teil einer Ausführungsanweisung bezeichnet, der sich rechts von dem Präfix »#sql« und innerhalb der geschweiften Klammern befindet. Eine SQLJ-Klausel kann: - eine Zuweisungsklausel (assignment clause) - eine Anweisungsklausel (statement clause) sein. Eine Zuweisungsklausel enthält einen oder mehrere Ausgabewerte. Damit stellt die Klausel mit der SELECT INTO-Anweisung in Beispiel 7.3 eine Zuweisungsklausel dar. Wie aus Beispiel 7.3 ersichtlich, erlaubt SQLJ die Zuweisung ausgewählter Werte direkt zu Host-Variablen innerhalb einer SQL-Anweisung. Die einzige Einschränkung bei solcher Zuweisung ist, dass die Abfrage eine einzige Reihe als Ergebnismenge liefert. (Beispiel 7.5 zeigt ein SQLJ-Programm, in dem die SELECT INTO-Anweisung verwendet wurde.) Eine Anweisungsklausel enthält keinen Ergebnisausdruck und liefert damit keine Ausgabewerte. Jede DDL-Anweisung sowie alle DML-Anweisungen (außer SELECT) sind Beispiele für Anweisungsklauseln. Beispiel 7.4 zeigt eine Anweisungsklausel. Beispiel 7.4 INSERT INTO mitarbeiter VALUES(:nr,:name,:vorname, :abtnr) 7.2.3 SQLJ-Host-Ausdrücke SQLJ-Host-Ausdrücke dienen dem Datenaustausch zwischen dem Anwendungsprogramm (geschrieben in SQLJ) und der Datenbank. Diese Ausdrücke befinden sich in eingebetteten SQL-Anweisungen und sind nichts anderes als gewöhnliche Java-Aus- 254 7 Was ist SQLJ? drücke. Generell kann man sagen, dass jeder gültige Java-Ausdruck als Host-Ausdruck verwendet werden kann. Damit sind Felder, Klasseninstanzen, Methodenaufrufe und arithmetische Ausdrücke Beispiele für mögliche Host-Ausdrücke. Hinweis Die einfachste Form eines Host-Ausdrucks bildet eine Host-Variable, die einen einfachen Java-Identifikator darstellt. Die generelle Syntax für einen Host-Ausdruck ist: :[modus] (host_ausdruck) bzw. :[modus] host_var. Damit muss ein Host-Ausdruck, im Unterschied zu einer Host-Variablen, immer in Klammern geschrieben werden. Die optionale modus-Angabe kennzeichnet den Modus eines Host-Ausdrucks. Der Modus eines Host-Ausdrucks kann IN, OUT oder INOUT sein. IN spezifiziert, dass der Wert des Host-Ausdrucks von der SQL-Anweisung, die ihn beinhaltet, gelesen wird. Demgegenüber spezifiziert OUT, dass der Wert des Host-Ausdrucks geändert wird durch die SQL-Anweisung, die ihn enthält. Diese beiden Angaben müssen oft nicht explizit angegeben werden, weil sie als solche vom SQLJ-System impliziert werden. Bei der SELECT INTO-, sowie FETCH-Anweisung haben die verwendeten Host-Ausdrücke standardmäßig die Angabe OUT, während in fast allen anderen Fällen die standardmäßige Angabe für einen Host-Ausdruck IN ist. Die INOUT-Angabe spezifiziert, dass der Wert des Host-Ausdrucks sowohl lesend als auch schreibend verwendet wird. Diese Angabe muss vor einem Host-Ausdruck explizit angegeben werden. Hinweis Es ist bei der Verwendung von Host-Variablen immer auf die korrekte Typkonvertierung zwischen den SQL-Datentypen und den entsprechenden Java-Datentypen zu achten. Die Tabellen mit der Konvertierung entsprechender Typen sind in der Entwicklerdokumentation jedes Datenbanksystems zu finden. Als weiteres Beispiel soll hier noch die Syntax des SET-Statements angegeben werden: #sql { SET :host_var = ausdruck }; Diese Anweisung dient zur Berechnung eines Ausdrucks und der Zuweisung dieses Ausdrucks an eine Host-Variable. SQLJ die Grundlagen 255 7.2.4 Fehlerbehandlung bei SQLJ Die Fehlerbehandlung für SQLJ basiert auf den Ausnahmen der Sprache Java, d.h. auf den Ausnahmen der Klasse SQLException. (Diese Klasse ist eine Unterklasse der Klasse Exception.) Die Information, die die SQLException-Klasse liefert, enthält Fehlermeldungstext, verschiedene Fehler-Codes und eine Kette von Ausnahmen. Der Fehlermeldungstext beschreibt den aufgetretenen Fehler. Eine Anwendung, die den Fehlermeldungstext benutzt, greift auf diesen String-Wert mit Hilfe der getMessage()-Methode zu. Der Fehler-Code kann entweder herstellerspezifisch sein oder dem XOPEN-Standard entsprechen. Dementsprechend existieren zwei unterschiedliche Methoden der Klasse SQLException, die beide einen ganzzahligen Fehler-Code als Rückgabewert liefern. Weitere Informationen über die SQLException-Klasse befinden sich in Kapitel 4. 7.2.5 Übersetzung und Ausführung eines SQLJ-Programms In diesem Abschnitt werden wir die manuelle Übersetzung und Ausführung eines SQLJ-Anwendungsprogramms zeigen. (Bei dem Programm handelt sich um eine so genannte Client-Anwendung, d.h. um ein Programm, das sich auf der Client-Seite des Systems befindet.) Hinweis Wir nehmen an, dass die CLASSPATH-Umgebungsvariable gesetzt ist. Diese Variable legt fest, wo die .class-Dateien gespeichert sind. Für die ausführliche Beschreibung der CLASSPATH-Umgebungsvariablen siehe Kapitel 3. SQLJ verwendet grundsätzlich einen Translator, mit dem die Quellprogramme in Java übersetzt werden. Um den Translator zu starten, muss das sqlj-Kommando ausgeführt werden. Dieses Kommando hat folgende Syntax: sqlj [optionen] datei... wobei datei das Präfix .sqlj enthält und optionen eine Liste von Optionen darstellt. (Eine Auswahl der möglichen Optionen ist in Tabelle 7.1 gegeben.) Option Beschreibung help Zeigt die Liste aller Optionen mit Beschreibung. compile Aktiviert bzw. deaktiviert die Übersetzung generierter Java-Dateien. compiler-executable Name der ausführbaren Datei des Java-Übersetzers. Tabelle 7.1: Optionen des sqlj-Kommandos 256 7 Was ist SQLJ? Option Beschreibung ser2class Aktiviert bzw. deaktiviert die Umsetzung von serialisierten Profilen in Klassendateien. Dies kann beim Laden von ausführbaren SQLJ-Dateien in bestimmten Browsern erforderlich sein (siehe auch Kapitel 10). status Aktiviert bzw. deaktiviert die sofortige Statusanzeige der SQLJ-Verarbeitung. linemap Aktiviert bzw. deaktiviert die Verwendung von Klassendateien mit Zeilennummern aus SQLJ-Quellendateien. compiler-output-file Name der Datei, in der die Ausgabe des Java-Compilers gespeichert wird. Ist dieser nicht angegeben, wird die Ausgabe auf stdout erwartet. profile Aktiviert bzw. deaktiviert die Profilanpassung. encoding Gibt die Verschlüsselung von Quelldateien an. (Ist diese Option nicht angegeben, wird die Dateiverschlüsselung dem Systemmerkmal »file.encoding« entnommen.) d Das Stammverzeichnis, in dem sich die generierten *.ser-Dateien befinden. Diese Option wird auch an den Java-Übersetzer übergeben. driver Die Liste derjenigen JDBC-Treiber, die registriert sind. dir Das Basisverzeichnis für die generierten Java-Dateien. Tabelle 7.1: Optionen des sqlj-Kommandos (Fortsetzung) Hinweis Die Verwendung der Option linemap ist empfehlenswert, weil sie den Hinweis auf die Zeilennummern der SQLJ-Dateien liefert. Dieser Hinweis kann im Falle eines Fehlers nützlicher sein als der standardmäßige Verweis auf die Zeile des übersetzten Java-Programms. Die in Beispiel 7.5 dargestellte Batch-Datei wurde von uns benutzt, um Oracle-SQLJProgramme in diesem und den nächsten beiden Kapiteln zu übersetzen. Beispiel 7.5 @echo off REM Parameter entsprechend ändern, Batchparameter: <Name> <Endung> SET ORADIR=D:\Programme\Oracle SET JDK=D:\jdk1.3\bin SET WORKDIR=f:\jdb_buch SET CLASSPATH=%WORKDIR%;%ORADIR%\sqlj\lib\runtime12.zip;%ORADIR%\sqlj\lib\translator. zip;.;%ORADIR%\jdbc\lib\classes12.zip; echo Übersetze SQLJ REM .class-Datei löschen, damit nicht aus Versehen die alte Version ausgeführt wird echo Lösche alte .class-Datei del %1.class REM Diese Zeile für Onlinechecking: SQLJ die Grundlagen 257 REM %ORADIR%\bin\sqlj.exe -user=scott -password=tiger -ser2class -compile=false explain %1%2 REM Diese Zeile für kein Onlinechecking (falls Programm erst die Daten, die überprüft werden, einträgt) %ORADIR%\bin\sqlj.exe -ser2class -compile=false -explain %1%2 echo Kompiliere %JDK%\javac %1.java echo Fertig Die in Beispiel 7.5 verwendete Batch-Datei ist generell, d.h. mit kleineren Änderungen auch für andere Datenbanksysteme anwendbar. Zuerst wird eine Variable ORADIR deklariert, welche das Installationsverzeichnis zur Datenbank erhält. Damit kann die mehrfache Angabe im CLASSPATH eingespart werden. Der Klassenpfad zeigt auf alle datenbankspezifischen Archivdateien: Im vorliegenden Beispiel auf ein Archiv für die Laufzeitklassen und auf ein anderes für den Vorübersetzer. (Bei vielen Datenbanksystemen wird für den Vorübersetzer bzw. für SQLJ ein eigenes Archiv verwendet.) Manche Vorübersetzer erlauben die automatische Übersetzung der Klassendateien. Im Zusammenhang mit speziellen Editoren empfiehlt es sich jedoch, diese Option zu deaktivieren und explizit den Übersetzungsvorgang zu starten. Hinweis Es ist darauf zu achten, dass nur die Archive eines Datenbanksystems im Klassenpfad vorhanden sind. Sonst kann es vorkommen, dass der falsche Vorübersetzer verwendet wird oder nicht nachvollziehbare Fehlermeldungen entstehen. Je nach Anwendungsfall sollte die Online-Überprüfung der SQL-Anweisungen aktiviert oder deaktiviert werden. (Die Optionen user, password und url des sqlj-Kommandos aktivieren diese Überprüfung.) Falls sich SQL-Anweisungen auf noch nicht vorhandene Datenbankobjekte beziehen, wird bei einer Online-Überprüfung eine Fehlermeldung vom System ausgegeben. (Trotzdem sollte diese Überprüfung, wenn möglich, aktiviert sein.) 7.2.6 Eingebettete SQL-Anweisungen ohne Ergebnismenge Eingebettete SQL-Anweisungen ohne Ergebnismenge sind u.a. - alle Datendefinitionsanweisungen - UPDATE und DELETE ohne CURRENT OF-Klausel (die CURRENT OF-Klausel wird in einem späteren Abschnitt beschrieben) - die INSERT-Anweisung. Das folgende Beispiel stellt ein SQLJ-Programm dar, in dem eine Tabelle erstellt wird. Das Beispiel zeigt die prinzipielle Anwendung von SQLJ in einem Java-Programm. 258 7 Beispiel 7.6 DB2, INFORMIX, ORACLE, MySQL package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; import sqlj.runtime.ref.DefaultContext; public class DML { static final int DBMS = DBUtils.ORACLE; //benötigte Variablen für DB-Zugriff*** static Connection con = null; //************************************* public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\nTabelle erstellen..."); //Das Standardschema für DB2 wählen if( DBMS == DBUtils.DB2 ) #sql {SET CURRENT SCHEMA DB2ADMIN }; //statische SQL-DML-Anweisung mit SQLJ ausführen #sql { CREATE TABLE tabelle1 (column1 char(4) not null, column2 char(20) not null , column3 char(15) ) }; System.out.println("Tabelle tabelle1 erstellt"); #sql { DROP TABLE tabelle1 }; System.out.println("Tabelle tabelle1 gelöscht"); //Abschlussarbeiten******************* con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ Was ist SQLJ? SQLJ die Grundlagen 259 System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } } }//Ende von Main Die Ausgabe des Beispiels 7.6 sieht folgendermaßen aus: Verbindung zu DB OK Tabelle erstellen... Tabelle tabelle1 erstellt Tabelle tabelle1 gelöscht Beispiel 7.6 verwendet die CREATE TABLE-Anweisung, um eine neue Tabelle (tabelle1) zu erstellen. (Die Tabelle wird anschließend mit der DROP TABLE-Anweisung gelöscht.) Dieses Beispiel verwendet, genauso wie alle vorherigen JDBC-Programme, das Dienstprogramm namens DBUtils, um die entsprechende Umgebung (Oracle, MySQL, Informix oder IBM DB2) festzulegen. Für die Beschreibung dieses Dienstprogramms siehe Kapitel 4. 7.2.7 Die Einbettung einer SELECT-Anweisung ohne Iterator Eine SELECT-Anweisung kann eine oder mehrere Reihen als Ergebnismenge liefern. (Der Fall, wo eine Abfrage keine Reihen liefert, wird bei den eingebetteten Sprachen genauso behandelt wie wenn eine einzige Reihe als Ergebnis existiert.) Falls eine Abfrage eine einzige Reihe liefert, gibt es keine Notwendigkeit für eine Überbrückung zwischen einer mengenorientierten Sprache wie SQL und einer satzorientierten Sprache wie Java, weil jede satzorientierte Sprache eine einzige Reihe problemlos bearbeiten kann. Beispiel 7.7 zeigt ein SQLJ-Programm, das u.a. eine Abfrage auf die Tabelle mitarbeiter ausführt. (Dieses Programm kann nicht mit MySQL verwendet werden, weil dieses Datenbanksystem zur Zeit keine Transaktionen unterstützt.) Beispiel 7.7 Ändern Sie die Aufgabe jenes Mitarbeiters, dessen Personal- und Projektnummer am Bildschirm eingegeben werden. Zusätzlich soll die Anzahl der Reihen in der Tabelle mitarbeiter ausgegeben werden sowie die durchgeführte Änderung mit Hilfe der Rollback-Funktion rückgängig gemacht werden. DB2, INFORMIX, ORACLE package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; 260 7 import sqlj.runtime.ref.DefaultContext; public class SelUpdRollback { static final int DBMS = DBUtils.INFORMIX; //benötigte Variablen für DB-Zugriff*** static Connection con = null; static Statement stmt = null; static ResultSet rs = null; //************************************* public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { // für Rollback Autocommit abschalten: con.setAutoCommit(false); DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\n"); //Das Standardschema wählen für DB2 erforderlich!!! if( DBMS == DBUtils.DB2 ) #sql {SET CURRENT SCHEMA DB2ADMIN }; // Reihenanzahl aus Tabelle Mitarbeiter lesen: String m_nr,pr_nr,aufgabe; long count1; System.out.println("\nAnzahl der Reihen in Tabelle Mitarbeiter holen..."); #sql { SELECT count(*) into :count1 from mitarbeiter }; System.out.println ("Reihen in Mitarbeiter: " +count1 ); // Datenbank-Update: System.out.println("\n\nGeben Sie die Nummer des Mitarbeiters an: "); m_nr = new BufferedReader(new InputStreamReader(System.in)).readLine(); System.out.println("\nGeben Sie die Projektnummer an:"); pr_nr = new BufferedReader(new InputStreamReader(System.in)).readLine(); try{ #sql { SELECT aufgabe into :aufgabe from arbeiten where m_nr = :m_nr and pr_nr = :pr_nr }; System.out.println("Der Mitarbeiter mit der Personalnummer "+m_nr+" ist " +aufgabe+" im Projekt "+pr_nr); System.out.println("\nGeben Sie die neue Aufgabe des Mitarbeiters an: "); aufgabe = new BufferedReader(new Was ist SQLJ? SQLJ die Grundlagen 261 InputStreamReader(System.in)).readLine(); #sql { UPDATE arbeiten set aufgabe = :aufgabe where m_nr= :m_nr and pr_nr= :pr_nr }; //Update-Rollback: System.out.println("\n\nRollback des updates..."); #sql { ROLLBACK WORK }; System.out.println("Rollback fertig."); }catch(SQLException notfound){ System.out.println("Mitarbeiter nicht vorhanden!"); } //Abschlussarbeiten****************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } }//Ende von Main } Die Ausgabe des Beispiels 7.7 ist: Verbindung zu DB OK Anzahl der Reihen in Tabelle Mitarbeiter holen... Reihen in Mitarbeiter: 7 Geben Sie die Nummer des Mitarbeiters an: 10102 Geben Sie die Projektnummer an: p1 Der Mitarbeiter mit der Personalnummer 10102 ist Projektleiter im Projekt p1 Geben Sie die neue Aufgabe des Mitarbeiters an: Gruppenleiter Rollback des updates... Rollback fertig. In Beispiel 7.7 wird zuerst mit Hilfe einer SELECT INTO-Anweisung die gesamte Anzahl der Reihen der Tabelle mitarbeiter in die LONG-Variable namens count1 gespeichert. Danach wird (mit einer ähnlichen SELECT INTO-Anweisung) eine einzige Reihe der Tabelle arbeiten ausgewählt. (Den Primärschlüssel der Tabelle arbeiten stellt die Kombination der Spalten m_nr und pr_nr dar. Aus diesem Grund wird für jede Angabe der beiden Werte höchstens eine Reihe geliefert.) 262 7 Was ist SQLJ? Mit der nachfolgenden UPDATE-Anweisung wird die Aufgabe des Mitarbeiters durch einen vom Benutzer angegebenen Wert geändert. Abschließend setzt die ROLLBACKAnweisung diese Änderung zurück. 7.2.8 Die Einbettung der SELECT-Anweisung mit Iterator Falls eine Abfrage mehrere Reihen als Ergebnismenge liefert, kann diese innerhalb eines Java-Programms nicht direkt abgearbeitet werden, weil die Java-Sprache (genauso wie alle anderen prozeduralen Sprachen) satzorientiert ist, d.h. zu einem Zeitpunkt nur einen einzelnen Satz abarbeiten kann. Aus diesem Grund besteht die Notwendigkeit, die Ergebnismenge einer SQL-Abfrage der Java-Sprache reihenweise zu übergeben, damit diese jede einzelne Reihe problemlos bearbeiten kann. Für solche Fälle unterstützt SQLJ Iteratoren. Iteratoren entsprechen den Cursorn, die am Anfang dieses Kapitels erörtert wurden, zeigen aber zu ihnen signifikante Unterschiede. (In Bezug auf JDBC ist ein Iterator in seiner Wirkungsweise mit einer Instanz der ResultSet-Klasse vergleichbar.) Der wichtigste Unterschied zwischen einem Cursor und einem Iterator ist, dass ein Iterator direkt zur Java-Sprache gehört, weil es sich um eine Klasse handelt. (Solche Konstrukte, die direkt zu einer Sprache gehören, werden first class objects genannt.) Aus diesem Grund kann ein Iterator als Parameter einer Methode übergeben werden. Genauso kann er außerhalb der SQLJ-Übersetzungseinheit, die ihn deklariert hat, verwendet werden, ohne seine statischen Eigenschaften zu verlieren. Demgegenüber ist ein Cursor kein first class object der jeweiligen Hostsprache, weil es sich um ein spezifisches SQL-Konstrukt handelt, das benutzt wird, um den Unterschied zwischen einer mengenorientierten Sprache (SQL) und einer satzorientierten Sprache (wie C, C++) zu überbrücken. SQLJ unterstützt zwei Iterator-Typen: - benannte Iteratoren - positionierte Iteratoren. Die beiden Iterator-Arten werden in den nachfolgenden Abschnitten beschrieben. Benannte Iteratoren Ein SQLJ-Programm, das einen benannten Iterator verwendet, beinhaltet immer folgende Schritte: 1. die Definition einer Iterator-Klasse mit Hilfe einer SQLJ-Deklaration 2. die Deklaration einer Iterator-Instanz SQLJ die Grundlagen 263 3. die Erstellung der Verbindung zwischen der Ergebnismenge einer Abfrage und der Iterator-Instanz 4. den Zugriff auf die aktuelle Reihe der Ergebnismenge mit Hilfe der next()-Methode 5. das Lesen der Spaltenwerte der aktuellen Reihe 6. das Schließen der Iterator-Instanz mit Hilfe der close()-Methode. Folgendes Beispiel zeigt die Verwendung eines benannten Iterators. Beispiel 7.8 DB2, INFORMIX, ORACLE, MySQL package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; // einen Cursor deklarieren mit Hilfe eines Iterators: // Named Binding #sql iterator Cursor1 (String m_nr,String m_name, String m_vorname); public class NameIter { static final int DBMS = DBUtils.ORACLE; //benötigte Variablen für DB-Zugriff*** static Connection con = null; //************************************* public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\n"); // Das Standardschema wählen für DB2 erforderlich!!! if(DBMS==DBUtils.DB2) #sql {SET CURRENT SCHEMA DB2ADMIN }; Cursor1 cursor1; // Cursor deklarieren String str1 = null; String max; // Obergrenze System.out.println("\n\nGeben Sie an, bis zu welcher m_nr sie ausgeben möchten: "); max = new BufferedReader(new InputStreamReader(System.in)).readLine(); // Named Column-Binding: #sql cursor1 = { SELECT m_nr, m_name, m_vorname from mitarbeiter 264 7 Was ist SQLJ? where m_nr < :max order by m_nr }; System.out.println("Mitarbeiter mit m_nr < "+max+": "); while (cursor1.next()) { str1 = cursor1.m_nr(); System.out.print (" m_nr= " + str1); str1 = cursor1.m_name(); System.out.print (" m_name= " + str1); str1 = cursor1.m_vorname(); System.out.println (" m_vorname= " + str1); } cursor1.close(); //Abschlussarbeiten****************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } }//Ende von Main } Die Ausgabe des Beispiels 7.8 ist: Verbindung zu DB OK Geben Sie an, bis zu welcher m_nr sie ausgeben möchten: 20000 Mitarbeiter mit m_nr < 20000: m_nr= 10102 m_name= Huber m_vorname= Petra m_nr= 18316 m_name= Müller m_vorname= Gabriele m_nr= 9031 m_name= Meier m_vorname= Rainer m_nr= 2581 m_name= Kaufmann m_vorname= Brigitte In Beispiel 7.8 wird mit Hilfe der folgenden SQLJ-Vereinbarungsanweisung #sql iterator Cursor1 (String m_nr,String m_name,String m_vorname); der benannte Iterator namens Cursor1 deklariert. Auf der Basis dieser Anweisung wird eine Java-Klasse von SQLJ erstellt. Das heißt, dass der SQLJ-Translator die Iterator-Deklaration durch die Deklaration einer Java-Klasse mit dem gleichen Namen ersetzt. Falls der Iterator innerhalb der Klasse deklariert werden soll, muss er innerhalb die Angaben public und static enthalten. SQLJ die Grundlagen 265 Der Iterator Cursor1 enthält drei Attribute: m_nr, m_name und m_vorname. Aus diesem Grund kann Cursor1 mit jeder Abfrage in Verbindung gebracht werden, die in ihrer SELECT-Liste diese drei Spalten (oder eine Untermenge davon) enthält. In Verbindung mit einem Funktionsaufruf innerhalb der SELECT-Anweisung muss für eine eindeutige Identifizierung des Spaltennamens und damit der Cursor-Zugriffsmethode ein Aliasname angegeben werden. Die gleichnamige Java-Klasse Cursor1 wird nach der Erstellung Folgendes enthalten: - eine next()-Methode, die Daten satzweise bearbeitet - Zugriffsmethoden m_nr(), m_name() und m_vorname(), die die Werte der entsprechenden Spalten der aktuellen Reihe zurückgeben - die Methode close(), die die Iterator-Instanz deaktiviert. Im nächsten Schritt – die Deklaration einer Iterator-Instanz – wird ein Objekt deklariert, das im Zusammenhang mit der Ergebnismenge der entsprechenden SELECTAnweisung steht. Die Anweisung Cursor1 cursor1; deklariert die Iterator-Instanz cursor1. In diesem Zusammenhang ist es wichtig zu wissen, dass jede Deklaration einer Iterator-Instanz eine Java- und nicht eine SQLJ-Deklaration ist. (Das ist ein signifikanter Unterschied zu den anderen eingebetteten SQLSchnittstellen, wo der Cursor ein SQL-Objekt darstellt.) Damit handelt es sich bei einer Iterator-Instanz um ein Java-Objekt, das genauso verwendet werden kann wie jedes andere Objekt der Java-Sprache. Die Anweisung #sql cursor1 = {SELECT m_nr, m_name, m_vorname from mitarbeiter where m_nr < :max order by m_nr }; verbindet die Iterator-Instanz mit der Ergebnismenge der Abfrage. Die Namen und Datentypen der Spalten, die in der SELECT-Liste angegeben sind, müssen den Namen und Datentypen der entsprechenden Attribute in der Deklaration eines benannten Iterators entsprechen. (Die Reihenfolge der Spalten in der SELECT-Liste ist nicht relevant.) Hinweis Die Anzahl der Spalten in der SELECT-Liste muss nicht mit der Anzahl der Attribute in der Iterator-Deklaration übereinstimmen. Einige Attribute in der IteratorDeklaration können durchaus keine entsprechenden Spalten in der SELECT-Liste haben, d.h. die Anzahl der Spalten muss kleinergleich als die Anzahl der Attribute in der Iterator-Deklaration sein. 266 7 Was ist SQLJ? Wie aus der obigen Anweisung ersichtlich, handelt es sich bei der SQLJ-Anweisung um eine Zuweisungsklausel, weil das Ergebnis der SELECT-Anweisung einer IteratorInstanz zugewiesen wird. In der while-Schleife: while ( cursor1.next() ) { ... } wird die next()-Methode verwendet, um die nächste Reihe der Ergebnismenge zu holen. Mit Hilfe der schon erwähnten Zugriffsmethoden der Java-Klasse (m_nr(), m_name() und m_vorname()) werden die Spaltenwerte der aktuellen Reihe geholt. Die Methode next() gibt den Booleschen Wert false zurück, falls keine weitere Reihe der Ergebnismenge zum Lesen vorhanden ist. Schließlich wird die Iterator-Instanz mit der Anweisung cursor1.close(); deaktiviert. Hinweis Falls mit einer SELECT-Anweisung NULL-Werte für eine gewisse Spalte geliefert werden können, ist es empfehlenswert, das zur Spalte korrespondierende Attribut mit Hilfe der Java-Klasse Integer zu definieren. In diesem Fall werden alle NULLWerte als solche ausgegeben. Falls die Spalte mit Hilfe des primitiven Datentyps int deklariert wird, wird vom System jedes Mal eine Ausnahme namens SQLNullException geliefert, wenn der NULL-Wert ausgegeben wird. (Die Integer-Klasse umfasst den primitiven Typ int.) Positionierter Iterator Beispiel 7.9 zeigt ein SQLJ-Programm mit einem positioniertem Iterator. Beispiel 7.9 DB2, INFORMIX, ORACLE, MySQL package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; // einen Cursor deklarieren mit Hilfe eines Iterators: // Positional Binding #sql iterator Cursor1 (String , String , String ) ; SQLJ die Grundlagen public class PosIter { static final int DBMS = DBUtils.ORACLE; //benötigte Variablen für DB-Zugriff*** static Connection con = null; //************************************* public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\n"); //Das Standardschema für DB2 wählen if( DBMS == DBUtils.DB2 ) #sql {SET CURRENT SCHEMA DB2ADMIN }; Cursor1 cursor1; String str1 = null; String max; //Obergrenze System.out.println("\n\nGeben Sie an, bis zu welcher m_nr sie ausgeben möchten: "); max = new BufferedReader(new InputStreamReader(System.in)).readLine(); //Positional Column-Binding: #sql cursor1 = { SELECT m_nr, m_name, m_vorname from mitarbeiter where m_nr < :max order by m_nr }; String a=null,b=null,c=null; #sql { FETCH :cursor1 INTO :a, :b, :c }; while (!cursor1.endFetch()){ System.out.println(" m_nr= "+a+" m_name= "+b+" m_vorname= "+c); #sql { FETCH :cursor1 INTO :a, :b, :c }; } cursor1.close(); //Abschlussarbeiten*************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ 267 268 7 Was ist SQLJ? System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } } }//Ende von Main Die Ausgabe des Beispiels 7.9 ist: Verbindung zu DB OK Geben Sie an, bis zu welcher m_nr sie ausgeben möchten: 15000 Mitarbeiter mit m_nr < 15000: m_nr= 10102 m_name= Huber m_vorname= Petra m_nr= 9031 m_name= Meier m_vorname= Rainer m_nr= 2581 m_name= Kaufmann m_vorname= Brigitte Obwohl die Beispiele 7.8 und 7.9 identisch sind (in Bezug auf das Problem, das sie implementieren), existieren zwischen ihnen signifikante Unterschiede, weil das erste einen benannten und das zweite einen positionierten Iterator verwendet. Folgende Liste enthält die wichtigsten Unterschiede: - die Deklaration der Iterator-Klasse ist verschieden. - die Reihen der Ergebnismenge bei einem positionierten Iterator werden mit Hilfe der SQLJ-Anweisung namens FETCH geholt. - das Endkriterium in Bezug auf die Ergebnismenge wird mit Hilfe der endFetch()Methode getestet. Die Deklaration der Iterator-Klasse bei einem positionierten Iterator beinhaltet nur die Datentypen der Attribute: #sql iterator Cursor1 (String , String , String ) ; Der Grund dafür ist, dass die Übereinstimmung zwischen den Attributen und den entsprechenden Spalten nicht mit Hilfe von Namen, sondern ausschließlich mit Hilfe ihrer Position ermittelt wird. Die einzelnen Reihen der Ergebnismenge werden bei einem positionierten Iterator mit Hilfe der SQL-Anweisung FETCH geholt (und nicht wie bei einem benannten Iterator mit der Java-Methode next()): #sql {FETCH :cursor1 INTO :a, :b, :c}; Damit hat ein SQLJ-Programm, das einen positionierten Iterator verwendet, mehr Ähnlichkeiten mit der Einbettung von SQL in Sprachen wie C und C++, weil diese die gleichnamige SQL-Anweisung (FETCH) für das Holen einzelner Reihen benutzen. SQLJ die Grundlagen 269 Hinweis Die Iterator-Instanz in einer FETCH-Anweisung muss immer »:« als Präfix haben, weil jede Iterator-Instanz eine Java-Variable ist. Innerhalb einer SQLJ-Anweisung (wie FETCH) wird sie als Host-Variable betrachtet. Eine Konsequenz der Verwendung der FETCH-Anweisung ist, dass bei einem positionierten Iterator keine Zugriffsmethoden wie bei einem benannten Iterator existieren. Diese sind auch nicht notwendig, weil die Spaltenwerte den entsprechenden JavaHost-Variablen zugewiesen werden. Die Zuweisung erfolgt auf der Basis der Position: Der erste Spaltenwert wird dem ersten Attribut zugewiesen, der zweite dem zweiten usw. Aus diesem Grund muss die Anzahl der Host-Variablen in der FETCH-Anweisung gleich der Anzahl der Spalten in der SELECT-Liste sein. Positionierte Iteratoren verwenden die Methode endFetch(), um das Ende einer Ergebnismenge zu ermitteln. Diese Methode liefert den Booleschen Wert true, falls keine weiteren Reihen der Ergebnismenge geholt werden können. Zusammenhang zwischen SQLJ-Iteratoren und JDBC-ResultSets Der enge Zusammenhang zwischen JDBC-ResultSet und Iteratoren wird besonders dadurch deutlich, dass man diese untereinander konvertieren kann. Um einen Iterator (benannt oder positioniert) in eine Instanz der ResultSet-Klasse umzuwandeln, wird die Methode getResultSet() des Iterators folgendermaßen verwendet: #sql iterator Cursor1 (String , String , String ); Cursor1 curs; ... ResultSet rs = curs.getResultSet(); Es ist bei einer derartigen Konvertierung zu beachten, dass man den Iterator nicht verwendet, sondern über die definierte Instanz der ResultSet-Klasse darauf zugreift. Sobald der Iterator geschlossen wird, wird auch die Instanz der ResultSet-Klasse geschlossen. (Die Instanz muss nicht explizit nach ihrer Verwendung geschlossen werden.) Um eine Instanz der Klasse ResultSet in einen Iterator (benannt oder positioniert) zu konvertieren, wird der CAST-Operator verwendet: ResultSet rs = pstmt.execute(); #sql curs = { CAST :rs }; Hier ist zu beachten, dass die Instanz der ResultSet-Klasse nicht verarbeitet werden darf und diese geschlossen wird, sobald der Iterator geschlossen wird. Das Schließen der Instanz wird den Iterator jedoch nicht schließen. Bei Verwendung von CAST muss 270 7 Was ist SQLJ? der Iterator als öffentlich (public) deklariert werden. Die Ergebnismenge, welche immer eine Instanz der ResultSet-Klasse sein muss, muss mindestens so viele Spalten enthalten wie der benannte Iterator. Genauso müssen die Spaltentypen der Ergebnismenge und des benannten Iterators übereinstimmen. Bei einem positioniertem Iterator müssen die Anzahl der Spalten sowie deren Typen übereinstimmen. Die Deklarationssyntax eines (benannten bzw. positionierten) Iterators kann wesentlich komplexer sein als in den beiden vorherigen Beispielen. Der nächste Abschnitt erläutert weitere Angaben, die ein Iterator enthalten kann. Die Deklarationssyntax eines Iterators Die Deklarationssyntax eines benannten Iterators sieht folgendermaßen aus: #sql [sichtbarkeit] iter_name iter_klassen_name [implements-Klausel] [with-Klausel] (attributen_liste) Die optionale Angabe sichtbarkeit kennzeichnet eine von den Angaben public, private, protected oder static. iter_name spezifiziert den Namen der Iterator-Klasse, während implements-Klausel die Java-Schnittstellen auflistet, die die Iterator-Klasse implementieren wird. (Wir erinnern noch einmal daran, dass eine Java-Schnittstelle im Unterschied zu Java-Klassen die in ihr angegebenen Methoden nicht implementiert. Diese Aufgabe wird von einer Klasse übernommen, deren Name der implementsKlausel vorangeht.) Die with-Klausel ermöglicht die Definition und die Initialisierung von Konstanten, die in der Definition der generierten Iterator-Klasse eingefügt werden. Die Syntax dieser Klausel ist: with variable1 = wert1, variable2 = wert2, ...., variablex = wertx Der Standard-SQLJ enthält eine Liste von vordefinierten Konstanten, die mit der withKlausel definiert werden können. (Die meisten von ihnen beziehen sich auf den Zustand eines Cursors.) Ein Beispiel mit der implements- und with-Klausel kann im folgenden Abschnitt (Beispiel 7.10) gesichtet werden. 7.2.9 Verwendung eines Iterators mit der UPDATE- bzw. DELETEAnweisung Für die Änderung von Reihen mit Hilfe einer UPDATE- bzw. DELETE-Anweisung ist ein Iterator im Allgemeinen nicht notwendig, weil diese Anweisungen keine Ergebnismenge erstellen und nur die Anzahl der geänderten Reihen zurückliefern. In gewissen Fällen ist die Verwendung des Iterators mit diesen beiden SQL-Anweisungen notwendig: Falls der Anwender die Entscheidung über das Ändern einzelner Reihe beim Ablauf des Programms treffen will, muss ein Iterator verwendet werden. Eine solcher Fall wird positioniertes Ändern (positioned update) genannt. SQLJ die Grundlagen 271 Beispiel 7.10 zeigt das positionierte Ändern der Reihen. (Dieses Programm kann nur mit DB2 und Informix verwendet werden, weil Oracle und MySQL zur Zeit diese Funktionalität nicht unterstützen.) Beispiel 7.10 DB2, INFORMIX package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; // Iterator für Update und Delete erstellen #sql iterator ChangeCursor implements sqlj.runtime.ForUpdate with (updateColumns="m_nr,pr_nr,aufgabe") ( String m_nr, String pr_nr, String aufgabe); public class UpdateIter { static final int DBMS = DBUtils.INFORMIX; //benötigte Variablen für DB-Zugriff*** static Connection con = null; //************************************* public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { // für Rollback Autocommit abschalten: con.setAutoCommit(false); DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\n"); ChangeCursor curs; String str1; String m_nr; //Obergrenze String antwort; //Current Schema setzen if( DBMS == DBUtils.DB2 ) #sql {SET CURRENT SCHEMA DB2ADMIN }; System.out.println("\n\nGeben Sie Nummer des Mitarbeiters ein: "); m_nr = new BufferedReader(new InputStreamReader(System.in)).readLine(); String pr_nr,aufgabe; #sql curs = { SELECT m_nr, pr_nr, aufgabe FROM arbeiten WHERE m_nr = :m_nr FOR UPDATE OF m_nr,pr_nr,aufgabe }; 272 7 Was ist SQLJ? while (curs.next()) { str1 = curs.m_nr(); System.out.print ("m_nr= " + str1); str1 = curs.pr_nr(); System.out.print (" pr_nr= " + str1); str1 = curs.aufgabe(); System.out.print (" aufgabe= " + str1); System.out.print ("\nMoechten Sie diese Reihe loeschen (j/n)?\n"); int a = new BufferedReader(new InputStreamReader(System.in)).read(); if (a=='J' || a=='j') #sql { DELETE from arbeiten WHERE CURRENT OF :curs }; } #sql { ROLLBACK work }; //Abschlussarbeiten*************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } }//Ende von Main } Die Ausgabe des Beispiels 7.10 ist: Verbindung zu DB OK Geben Sie Nummer des Mitarbeiters ein: 10102 m_nr= 10102 pr_nr= p1 aufgabe= Projektleiter Moechten Sie diese Reihe loeschen (j/n)? j m_nr= 10102 pr_nr= p2 aufgabe= Gruppenleiter Moechten Sie diese Reihe loeschen (j/n)? n Bei dem Iterator in Beispiel 7.10 handelt sich um eine spezielle Form des benannten Iterators. Aus diesem Grund werden wir nur die Unterschiede zwischen dem benannten Iterator (Beispiel 7.8) und dem Iterator für das positionierte Ändern (Beispiel 7.10) erläutern. SQLJ die Grundlagen 273 Der erste wichtige Unterschied betrifft die Iterator-Deklaration: #sql iterator ChangeCursor implements sqlj.runtime.ForUpdate with (updateColumns="m_nr,pr_nr,aufgabe") (String m_nr, String pr_nr, String aufgabe); Ein Iterator, der für das positionierte Ändern verwendet werden soll, muss die implements-Klausel enthalten, die sich in diesem Fall auf die Klasse sqlj.runtime.ForUpdate bezieht. (Für die Beschreibung aller Angaben der implements-Klausel siehe den vorherigen Abschnitt.) Die zweite Klausel heißt with-Klausel und enthält im Falle einer positionierten Änderung die updateColumns-Spezifikation mit der Liste aller änderbaren Spalten der Tabelle. Der nächste wichtige Unterschied betrifft die Erstellung der Verbindung zwischen der Ergebnismenge, die positioniert geändert werden soll und der Iterator-Instanz: #sql curs ={SELECT m_nr,pr_nr,aufgabe FROM arbeiten WHERE m_nr=:m_nr FOR UPDATE OF m_nr,pr_nr,aufgabe }; In der obigen SQLJ-Ausführungsklausel existiert eine zusätzliche Angabe – FOR UPDATE –, die generell folgende Syntax hat: FOR UPDATE [OF spalten_liste] Mit dieser Angabe werden die ausgewählten Reihen mit Hilfe eines Iterators geändert bzw. gelöscht. spalten_liste definiert die Spalten, die geändert werden können. (Falls spalten_liste ausgelassen wird, kann jede Spalte der Tabelle geändert werden.) Die Voraussetzung für das Ändern der Datenwerte bzw. das Löschen der Reihen ist das Vorhandensein der CURRENT OF-Angabe innerhalb der UPDATE- bzw. DELETEAnweisung. In Beispiel 7.10 existiert eine solche DELETE-Anweisung, weil wir positioniert die Reihen der Tabelle arbeiten löschen wollen: #sql { DELETE from arbeiten WHERE CURRENT OF :curs }; Die Syntax einer UPDATE-Anweisung für das positionierte Ändern sieht folgendermaßen aus: #sql {UPDATE tab_name SET (spalte1 = ausdruck1 ...) WHERE CURRENT OF :iter}; 7.2.10 Gespeicherte Prozeduren und SQLJ Gespeicherte Prozeduren werden innerhalb eines SQLJ-Programms mit Hilfe der CALL-Anweisung aufgerufen. Diese Anweisung ist die allgemein bekannte SQLAnweisung, die auch in prozeduralen Erweiterungen von SQL bei Oracle (PL/SQL) und Informix (SPL) verwendet wird. Beispiel 7.11 zeigt die Erstellung und den Aufruf einer gespeicherten Prozedur in einer Oracle-Umgebung. Die Prozedur wird dabei in PL/SQL implementiert. 274 7 Was ist SQLJ? Hinweis Gespeicherte Prozeduren können nicht nur mit Hilfe der prozeduralen Erweiterung des jeweiligen Datenbanksystems implementiert werden. SQLJ bzw. JDBC können genauso für ihre Implementierung verwendet werden. Kapitel 10 beschreibt diese Art der Implementierung. Beispiel 7.11 ORACLE package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; public class CallSP { static final int DBMS = DBUtils.ORACLE; //benötigte Variablen für DB-Zugriff*** static Connection con = null; static Statement stmt = null; //************************************* public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\n"); stmt = con.createStatement(); stmt.execute("CREATE OR REPLACE PROCEDURE erhoehe_mittel (prozent int DEFAULT 0) AS BEGIN update projekt set mittel = mittel + (mittel*prozent/100); END;"); int mittel = 5; #sql { CALL erhoehe_mittel(:mittel) }; System.out.println("Prozedur ausgeführt"); //Abschlussarbeiten*************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); SQLJ die Grundlagen } 275 e.printStackTrace(); } }//Ende von Main Die Ausgabe des Beispiels 7.11 ist: Verbindung zu DB OK Prozedur ausgeführt Hinweis Mit MySQL ist es nicht möglich, ein SQLJ-Programm wie in Beispiel 7.11 zu implementieren, weil dieses Datenbanksystem keine gespeicherte Prozeduren unterstützt. Mit Hilfe der Anweisung: stmt.execute("CREATE OR REPLACE PROCEDURE erhoehe_mittel(prozent int DEFAULT 0) AS BEGIN update projekt set mittel = mittel + (mittel*prozent/100); END;"); wird eine gespeicherte Prozedur im Datenbanksystem erzeugt. Der Aufruf dieser Prozedur erfolgt folgendermaßen: #sql { CALL erhoehe_mittel(:mittel) }; Mit der CALL-Anweisung wird die gespeicherte Prozedur namens erhoehe_mittel aufgerufen. Der einzige Parameter dieser Prozedur wird als Host-Variable innerhalb der SQLJ-Klausel verwendet. Das folgende Beispiel zeigt die Implementierung einer gespeicherten Funktion. Beispiel 7.12 ORACLE package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.*; public class ValuesSP { static final int DBMS = DBUtils.ORACLE; //benötigte Variablen für DB-Zugriff*** static Connection con = null; static Statement stmt = null; //************************************* public static void main(String argv[]) 276 7 Was ist SQLJ? { } if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Verbindung zu DB OK\n"); stmt = con.createStatement(); stmt.execute("CREATE OR REPLACE FUNCTION getEuro(value FLOAT) RETURNS FLOAT AS BEGIN RETURN value/1.1.955830; END;"); float DMbetrag = 5; float result=0; #sql result = { VALUES(getEuro(:DMbetrag) }; System.out.println("Prozedur ausgeführt: "+result); //Abschlussarbeiten*************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } }//Ende von Main Beispiel 7.12 ist bis auf zwei signifikante Unterschiede identisch mit dem Beispiel 7.11. Eine gespeicherte Funktion wird bei Oracle mit Hilfe der CREATE FUNCTIONAnweisung erstellt. In Beispiel 7.12 wird die Funktion getEuro(), die DM-Beträge in Euro konvertiert, mit Hilfe folgender SQL-Anweisung erstellt: stmt.execute("CREATE OR REPLACE FUNCTION getEuro(value FLOAT) RETURNS FLOAT AS BEGIN RETURN value/1.1.955830; END;"); Im Gegensatz zum Aufruf der gespeicherten Prozeduren, die mit der CALL-Anweisung erfolgt (siehe Beispiel 7.11), kann der Aufruf einer gespeicherten Funktion innerhalb eines SQLJ-Programms mit der VALUES-Klausel erfolgen. Die Syntax dieser Anweisung ist: #sql ergebnis = { VALUES( Funktionsname(<Parameterliste>) ) }; Die Variable ergebnis muss dabei dem Rückgabetyp der Funktion entsprechen. SQLJ die Grundlagen 277 Dementsprechend sieht der Aufruf der getEuro()-Funktion in Beispiel 7.12 so aus: #sql result = { VALUES(getEuro(:DMbetrag) }; Selbstverständlich kann eine gespeicherte Funktion mit einer DML-Anweisung (innerhalb oder außerhalb eines SQLJ-Programms) aufgerufen werden. Folgende Abfrage, z.B.: SELECT getEuro(mittel) FROM projekt WHERE pr_nr IN ('p1', 'p2') konvertiert die Mittel der Projekte p1 und p2 in Euro. Hinweis Bei der Verwendung einer gespeicherten Funktion mit einer SELECT-Anweisung innerhalb eines SQLJ-Programms muss man speziell bei benannten Iteratoren einen Aliasnamen angeben, damit der Zugriff auf diese Spalte möglich wird. 7.2.11 Große Objekte in SQLJ Die großen Objekte (large objects – LOB) weisen eine praktisch uneingeschränkte Größe auf. Sie werden benutzt, um Objekte wie Binärdateien, Bilder oder Unicode-Texte in einer Tabellenspalte zu speichern. (Für die Beschreibung dieser Objekte siehe Kapitel 4.) Das folgende Beispiel erlaubt die Speicherung beliebiger Bilddateien in einer Datenbanktabelle. In der Tabelle wird der Ursprungspfad der Datei als großes textuelles Objekt, die Datei selbst als großes binäres Objekt gespeichert. Als eindeutige Identifikationsnummer (ID) dient eine Spalte mit der Bildbeschreibung. Das Beispiel untersucht zuerst, ob eine Datei gespeichert werden soll und danach, ob eine Datei im Ursprungsverzeichnis wiederhergestellt werden soll. Optional kann der Inhalt der Datei direkt oder in der hexadezimalen Form ausgegeben werden. Beispiel 7.13 ORACLE package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.DefaultContext; // für CLOB und BLOB import oracle.sql.*; 278 7 public class Lob { static final int DBMS = DBUtils.ORACLE; //benötigte Variablen für DB-Zugriff*** static Connection con = null; //************************************* #sql public static iterator Iter (String picname, CLOB picpath, BLOB image); public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { // Für BLOB notwendig! con.setAutoCommit(false); DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Bildspeicherung in Datenbank (q=quit)"); System.out.println("Verbindung zu DB OK\n"); System.out.println("Bilder in DB speichern(j/n)?"); if(readLine().equals("j")) storePic(); System.out.println("Bilder aus DB wiederherstellen(j/n)?"); String output = readLine(); if(!output.equals("j")) System.exit(0); restorePic(); //Abschlussarbeiten**************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } }//Ende von Main // Einlesen vom Bildschirm public static String readLine(){ try{ return new BufferedReader(new InputStreamReader(System.in)).readLine(); }catch(Exception e){} return ""; } Was ist SQLJ? SQLJ die Grundlagen // BLOB und CLOB-Daten aus DB lesen public static void restorePic() throws SQLException, IOException, FileNotFoundException{ CLOB c; BLOB b; Iter curs; #sql curs = {select * from pictable}; FileOutputStream fo; String path,name; while(curs.next()){ name = curs.picname().trim(); c = curs.picpath(); b = curs.image(); path = c.getSubString(1,(int)c.length()); System.out.println("Datei "+name+" nach "+path+" schreiben(j/n)?"); String input = readLine(); if(!input.equals("j")) continue; fo = new FileOutputStream(path); fo.write(b.getBytes(1,(int)b.length() ) ); fo.close(); System.out.println("Ausgabe als HEX-Dump(j/n)?"); input = readLine(); if(input.equals("j")) System.out.println("Hex-Dump: "+dumpBlob(b,true)); } } // BLOB- und LOB-Daten speichern public static void storePic() throws SQLException, IOException{ FileInputStream fi = null; boolean error; String input=""; do{ error=false; System.out.println("Bitte geben Sie einen Dateinamen mit Pfad an:"); try{ input = readLine(); if(input.equals("q") )System.exit(0); fi = new FileInputStream(input); }catch(Exception e){ System.out.println("Fehler beim Lesen der Datei!"); error = true; } }while(error); System.out.println("Bitte geben Sie eine Beschreibung an:"); String picname = readLine(); if(picname.equals("q")) System.exit(0); // Tabelle mit BLOB und CLOB erstellen try{ 279 280 7 #sql { CREATE TABLE pictable (picname VARCHAR(255) primary key, picpath, CLOB image BLOB) }; }catch(Exception e){} int length = 0; BLOB blobdata; // BLOB-Locator erstellen: #sql { INSERT INTO pictable VALUES (:picname, :input, EMPTY_BLOB()) }; // BLOB für Änderung lesen #sql { SELECT image INTO :blobdata FROM pictable WHERE picname = :picname }; byte[] buffer = new byte[blobdata.getChunkSize()]; int offset=1; long bloblen = while ((length = fi.read(buffer)) != -1) { //letzte Bytesequenz evtl. kürzen if(length<buffer.length){ byte[] bt = new byte[length]; for(int t=0; t<length; t++) bt[t] = buffer[t]; buffer = bt; } blobdata.putBytes(offset,buffer); offset+=length; } fi.close(); System.out.println("Bild gespeichert\n"); } // Ausgabe eines Blobs, als Hex-Darstellung oder Zeichen // ORACLE-spezifisch public static String dumpBlob(BLOB blob, boolean hex ){ StringWriter sw = new StringWriter(); try{ InputStream instream = blob.getBinaryStream(); byte[] buffer = new byte[blob.getChunkSize()]; int length = 0; while ((length = instream.read(buffer)) != -1) { for (int i=0; i<length; i++){ byte b = buffer[i]; if(hex){ int v = ((int)b) & 0xff; if(v < 16) sw.write('0'); sw.write(Integer.toHexString(v)); } else sw.write(b); } } return sw.toString(); Was ist SQLJ? SQLJ die Grundlagen } } 281 }catch(Exception e){} return ""; Die Ausgabe sieht etwa so aus (je nach eingegebenen Daten): Bildspeicherung in Datenbank (q=quit) Verbindung zu DB OK Bilder in DB speichern(j/n)? Bilder aus DB wiederherstellen(j/n)? Ausgabe als HEX-Dump(j/n)? Das Beispiel kann nur mit Oracle verwendet werden und benutzt deswegen die spezifischen Klassen dieses Systems namens CLOB und BLOB, die eine erweiterte Schnittstelle als die Standard-Java-Klassen java.sql.Blob und java.sql.Clob bieten. Zur Verwendung dieser Klassen muss das Paket oracle.sql.* importiert werden. Die im Beispiel verwendete Tabelle pictable, welche eine Bildbeschreibung, den Bildpfad und die Bilddaten enthält, wird durch die SQL-Anweisung CREATE TABLE pictable(picname VARCHAR(255) primary key,picpath CLOB, image BLOB) erzeugt. Für die spätere Ausgabe des Tabelleninhalts wird deshalb ein benannter Iterator mit folgender Anweisung definiert: #sql public static iterator Iter (String picname,CLOB picpath,BLOB image); Das Hauptprogramm main() ist lediglich für die Verbindung zur Datenbank, die Ausgabe der Benutzeroberfläche und die Ausnahmebehandlung zuständig, die eigentlichen Datenbankzugriffe erfolgen in eigenen (statischen) Methoden. Hinweis Für die Änderung eines großen Objektes ist es im Beispiel unbedingt notwendig, AutoCommit zu deaktivieren, damit während des Einfügens von Daten in eine LOB-Spalte die Tabelle bis zum Abschluss der Änderung gesperrt wird. (Wird AutoCommit nicht deaktiviert, wird eine Fehlermeldung erscheinen, dass die Zeile mit dem LOB-Inhalt nicht gesperrt wurde.) Die Abfrage der Bildschirmeingaben erfolgt der besseren Übersichtlichkeit wegen in einer eigenen Methode – readLine() –, welche von der Standardeingabe bis zum Zeilenumbruch liest und die bereits von früher bekannten Klassen (BufferedReader und InputStreamReader) verwendet. Je nach Benutzereingabe werden im Hauptprogramm die Methoden storePic() und restorePic() aufgerufen, welche als statische Methoden der Klasse implementiert sind. 282 7 Was ist SQLJ? Die Methode storePic() speichert den Bildnamen, den Pfad und die Daten in der Tabelle. Damit nicht explizit alle möglichen Ausnahmen abgefangen werden müssen, werden diese an die main()-Methode zurückgegeben: storePic() throws SQLException, IOException Die do/while-Schleife wird so lange wiederholt, bis der Benutzer entweder mit dem Eingabezeichen q abbricht oder einen gültigen Dateipfad eingibt. Dazu wird nach der Eingabe des Pfades eine Instanz der Klasse FileInputStream auf die Datei erstellt. Falls die Datei nicht vorhanden ist, wird eine Ausnahme erzeugt und die Eingabe muss wiederholt werden. Die Variable picname nimmt die Bildbeschreibung auf. (Die gleichnamige Spalte der Tabelle wird als Primärschlüssel verwendet, damit ein Bild beim Schreiben bzw. Lesen eindeutig identifiziert werden kann.) Beim ersten Programmstart ist die Tabelle noch nicht vorhanden, weshalb in einem try/catch-Block bei jedem Programmdurchlauf versucht wird, diese zu erstellen. Wenn sie nicht vorhanden ist, wird sie erzeugt, ansonsten wird der leere Bereich des catch-Blockes ausgeführt. (Als Alternative bietet sich eine Abfrage der entsprechenden Systemtabelle an.) Bevor ein großes Objekt gelesen oder geschrieben werden kann, muss es initialisiert werden. Die Datenbank benötigt einen sogenannten Lokator (locator), der eine Verknüpfung zwischen dem großen Objekt und den eigentlich gespeicherten Daten herstellt. Falls ein solcher Lokator nicht existiert, werden keine Operationen ausgeführt. Zur Initialisierung bzw. Erstellung eines leeren großen binären Objektes wird bei Oracle die Funktion EMPTY_BLOB() angeboten. Mit Hilfe der Anweisung: #sql {INSERT INTO pictable VALUES (:picname, :input, EMPTY_BLOB())}; wird eine Zeile mit Bildbeschreibung, Bildpfad und einem BLOB-Lokator in der Tabelle erstellt. Das große textuelle Objekt wird dabei direkt als Zeichenkette in die INSERT-Anweisung übernommen. Dies ist bis zu einer sehr kurzen Datenlänge auch bei großen binären Objekten möglich, wobei diese dann als Zeichenkette in der hexadezimalen Codierung übergeben werden müssen. Im Allgemeinen werden beide Datentypen (CLOB und BLOB) gerade bei großen Objekten verwendet, weshalb die Vorgehensweise bei der BLOB-Spalte vorgezogen werden sollte. Das Füllen der Daten in ein binäres großes Objekt kann nicht bei der Initialisierung durchgeführt werden, sondern muss in einem separaten Schritt geschehen. Das initialisierte binäre Objekt wird mit Hilfe des Bildnamens aus der Tabelle gelesen, damit es gefüllt werden kann: #sql { SELECT image INTO :blobdata FROM pictable WHERE picname = :picname }; SQLJ die Grundlagen 283 Da die Daten in mehreren kleinen Stücken übertragen werden, kann bei Oracle die optimale Größe dieser Stücke (chunks) mit Hilfe der Methode getChunkSize() der BLOBKlasse ermittelt werden. (Diese Funktionalität wird von der Klasse java.sql.Blob nicht geboten). Die Verwendung der chunk-Größe ermöglicht eine optimale Geschwindigkeit beim Zugriff auf ein großes binäres Objekt. (Sie ist aber nicht vorgeschrieben.) Innerhalb der while-Schleife werden diese Datenpakete Stück für Stück über die Methode putBytes() der Klasse BLOB in die Datenbank übertragen. Damit die letzte, eventuell kürzere Byte-Sequenz korrekt gespeichert wird, verkürzen die Anweisungen im if-Block das zuletzt gelesene Datenpaket auf die korrekte Länge. (Die JDBC-Klasse Blob bietet in dieser Hinsicht keine Unterstützung. Mit dieser Klasse werden lediglich Leseoperationen unterstützt.) Mit der close()-Methode der Klasse FileInputStream wird schließlich der Eingabestrom geschlossen. Die Methode restorePic() liest die gespeicherten großen Objekte aus der Datenbank aus und speichert die Bilddaten in einer Datei. Damit die Ausnahmebehandlung nicht wiederholt in dieser Methode implementiert werden muss (und die Übersichtlichkeit gewahrt bleibt), werden alle Ausnahmen an die main()-Methode zurückgegeben. Mit der Anweisung: #sql curs = {select * from pictable}; wird der Tabelleninhalt in den zu Beginn definierten Cursor curs eingelesen. Mit Hilfe der Iterator-Methoden picname(), picpath() und image() können die jeweiligen Datentypen String, CLOB und BLOB gelesen werden. Die Zuweisung: path = c.getSubString(1,(int)c.length()); liest den kompletten Inhalt des textuellen Objektes in eine Variable vom Typ String. (Die Methode getSubString() existiert in derselben Form auch in der JDBC-Klasse Clob.) Falls man sich entscheidet, das Bild im Ursprungspfad wiederherzustellen, werden die BLOB-Daten mit: fo.write(b.getBytes(1,(int)b.length() ) ); in eine Instanz der Klasse FileOutputStream ausgegeben, andernfalls wird mit dem nächsten Bild fortgefahren. Die Methode getBytes() liest ab der angegebenen Position bis zur spezifizierten Länge aus dem großen binären Objekt in einen Vektor, der aus Ein-Byte-Elementen besteht. Hinweis Die Methode getSubString() existiert in derselben Form auch in der JDBC-Klasse Blob. Die Länge des großen binären Objektes kann dabei durch die in der Klasse java.sql.Blob ebenfalls definierte Methode length() ermittelt werden. 284 7 Was ist SQLJ? Optional kann nach der Speicherung noch gewählt werden, ob die BLOB-Daten am Bildschirm ausgegeben werden sollen. Dies geschieht mit Hilfe der Methode dumpBlob(), welche als Parameter ein großes binäres Objekt bekommt. Der zweite Parameter gibt an, ob die Ausgabe in der hexadezimalen Form oder ohne Vorbereitung (raw dump) erfolgen soll. Über die Methode getBinaryStream() der JDBC-Klasse Blob wird der Eingabestrom des Objektes ermittelt. In der while-Schleife werden mit: length = instream.read(buffer) die einzelnen Datenpakete ausgelesen und entweder direkt in eine Instanz der Klasse StringWriter geschrieben oder vorher in die hexadezimale Form umgewandelt. Die komplette Zeichenkette wird dann aus der StringWriter-Instanz gelesen und zurückgegeben. Beispiel 7.14 zeigt, wie große Objekte mit MySQL erstellt, gelesen und ausgegeben werden können. Beispiel 7.14 MySQL package SQLJ; import Utils.*; import java.io.*; import java.sql.*; import sqlj.runtime.*; import sqlj.runtime.ref.DefaultContext; public class Lob2 { static final int DBMS = DBUtils.MYSQL; //benötigte Variablen für DB-Zugriff*** static Connection con = null; static Statement stmt = null; static ResultSet rs = null; //************************************* #sql public static iterator Iter (String picname, BinaryStream picpath, Blob image); public static void main(String argv[]) { if( !DBUtils.initDatabaseDriver(DBMS)) return; if( (con = DBUtils.connectDataBase(DBMS)) == null) return; try { DefaultContext.setDefaultContext(new DefaultContext(con)); System.out.println("Bildspeicherung in Datenbank (q=quit)"); System.out.println("Verbindung zu DB OK\n"); SQLJ die Grundlagen System.out.println("Bilder in DB speichern(j/n)?"); if(readLine().equals("j")) storePic(); System.out.println("Bilder aus DB wiederherstellen(j/n)?"); String output = readLine(); if(!output.equals("j")) System.exit(0); restorePic(); //Abschlussarbeiten**************************** con.close(); } //Ausführliche JDBC-Fehlerbehandlung:------------//alle SQL-Methoden werfen evtl. eine SQLException catch(SQLException e) { DBUtils.showSQLException(e); } catch(Exception e){ System.out.println("\n--- Fehler eingetreten ---\n"); e.printStackTrace(); } }//Ende von Main public static String readLine(){ try{ return new BufferedReader(new InputStreamReader(System.in)).readLine(); }catch(Exception e){} return ""; } // Blob und Clob-Daten aus DB lesen public static void restorePic() throws SQLException, IOException, FileNotFoundException{ InputStream is; Blob b; int c; FileOutputStream fo; String name; Iter curs; #sql curs = {select * from pictable}; while(curs.next()){ StringWriter sw = new StringWriter(); name = curs.picname().trim(); b = curs.image(); // Stream vor nächster getxxx-Methode auslesen is = curs.picpath(); // Stream wird hier in String konvertiert while((c=is.read())!=-1) sw.write(c); System.out.println("Bild "+name+" nach "+sw+" schreiben(j/n)?"); String input = readLine(); 285 286 7 if(!input.equals("j")) continue; fo = new FileOutputStream(sw.toString()); fo.write(b.getBytes(1,(int)b.length() ) ); fo.close(); System.out.println("Ausgabe als HEX-Dump(j/n)?"); input = readLine(); if(input.equals("j")) System.out.println("Hex-Dump: "+dumpBlob(b,true)); } } // Blob- und LOB-Daten speichern public static void storePic() throws SQLException, IOException, Exception{ FileInputStream fi = null; boolean error; String input=""; int length=0; do{ error=false; System.out.println("Bitte geben Sie einen Dateinamen mit Pfad an:"); try{ input = readLine(); if(input.equals("q") )System.exit(0); File f = new File(input); length = (int)f.length(); fi = new FileInputStream(f); }catch(Exception e){ System.out.println("Fehler beim Lesen der Datei!"); error = true; } }while(error); System.out.println("Bitte geben Sie eine Beschreibung an:"); String picname = readLine(); if(picname.equals("q")) System.exit(0); // Tabelle mit Blob erstellen try{ #sql { CREATE TABLE pictable (picname VARCHAR(255) primary key, picpath BLOB, image LONGBLOB) }; }catch(Exception e){} BinaryStream bs = new BinaryStream(fi, length); #sql { INSERT INTO pictable VALUES (:picname, :input, :bs)}; fi.close(); System.out.println("Bild gespeichert\n"); } // Ausgabe eines Blobs, als HexDarstellung oder Zeichen public static String dumpBlob(Blob blob, boolean hex ){ StringWriter sw = new StringWriter(); try{ Was ist SQLJ? SQLJ die Grundlagen } } 287 InputStream instream = blob.getBinaryStream(); byte[] buffer = new byte[100]; int length = 0; while ((length = instream.read(buffer)) != -1) { for (int i=0; i<length; i++){ byte b = buffer[i]; if(hex){ int v = ((int)b) & 0xff; if(v < 16) sw.write('0'); sw.write(Integer.toHexString(v)); } else sw.write(b); } } return sw.toString(); }catch(Exception e){} return ""; Beispiel 7.13 hat Ähnlichkeiten mit dem Beispiel 7.12. Aus diesem Grund werden wir nur die Unterschiede zu diesem Beispiel beschreiben. Im Unterschied zu den anderen Datenbanksystemen kennt MySQL-System folgende Datentypen für binäre Objekte: - BLOB - LONGBLOB. Wie aus der obigen Liste ersichtlich, kennt MySQL keinen Datentyp CLOB. Der Datentyp BLOB kann benutzt werden, um große Objekte bis 64 KB zu speichern. LONGBLOB dagegen speichert alle Objekte bis 4 GB. (Damit entspricht LONGBLOB dem Datentyp BLOB bei den anderen Datenbanksystemen.) Zunächst zu den Änderungen mit Hilfe der Methode restorePic(), die genau in dieser Weise auch mit Oracle funktioniert. Da der CLOB-Datentyp von MySQL nicht unterstützt wird und die Pfadinformationen, also Text, vom Datentyp BLOB sein sollen, wird die Instanz der Klasse Blob direkt in eine Instanz der Klasse InputStream gelesen, und zwar über die Anweisung: is = curs.picpath(); Diese Vorgehensweise, mit der das Auslesen der großen textuellen Objekte sehr elegant gelöst ist, ist nicht spezifisch für MySQL, sondern funktioniert auch z.B. mit Oracle. 288 7 Was ist SQLJ? Nach JDBC-Konventionen darf ein derartiger Strom nur bis zur Verwendung der nächsten getXXX()-Methode verwendet werden, weshalb die Instanz der Blob-Klasse bereits vorher über die image()-Methode gelesen wurde. Mit der anschließenden while-Anweisung: while((c=is.read())!=-1) sw.write(c); wird der Strom aus der Instanz is in einen Strom der Klasse StringWriter kopiert. Die StringWriter-Klasse ist sehr effektiv beim Konkatenieren von Zeichenketten und erlaubt die Verwendung der endgültigen String-Instanz über deren toString()Methode, wie sie im Beispiel im Konstruktor des FileOutputStreams verwendet wurde. Beim Speichern eines großen binären Objektes wird die Methode storePic() anders verwendet als im Oracle-Beispiel. Die Daten der Eingabedatei werden direkt einem BinaryStream-Objekt übergeben, welches über die Anweisung: BinaryStream bs = new BinaryStream(fi, length); erzeugt wird. Die Länge des Stromes wird über die length()-Methode des File-Objektes, beim Öffnen der Eingabedatei, ermittelt. Mit Hilfe der Anweisung: #sql { INSERT INTO pictable VALUES (:picname, :input, :bs)}; kann dann dieser Stream direkt als Host-Variable angegeben werden. Die kurze Zeichenkette für den Pfadnamen, der als großes binäres Objekt gespeichert wird, kann wieder direkt als eine String-Instanz übernommen werden. 7.3 Zusammenfassung SQLJ ist nach JDBC die zweite Java-Schnittstelle, die für die Datenbankprogrammierung verwendet werden kann. Obwohl SQLJ nur die Programmierung der statischen SQL-Anweisungen ermöglicht, hat sie einige wichtige Vorteile im Vergleich zu JDBC: - SQLJ befindet sich auf einem höherem logischen Niveau als JDBC - SQLJ hat eine einfachere Syntax als JDBC - SQLJ kann die Syntax- und Semantikanalyse zur Übersetzungszeit durchführen. Das nächste Kapitel beschreibt, wie die objektorientierten Erweiterungen mit SQLJ implementiert werden können.