7 Was ist SQLJ?

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