Datenbankzugriffe aus Java werden üblicherweise, wenn man von dem exotischen Weg über die AS/400-Toolbox unter DB2/400 absieht, über SQL ausgeführt. Für alle relevanten Datenbanken des Marktes stehen JDBC-Treiber zur Verfügung, die dazu benötigt werden, SQL Verbindungen aus Java zu verwenden. Für den SQL-Zugriff wird ein SQL-Statement aus Literalen und Variableninhalten zusammengebastelt und mit der Methode executeQuery eines Statement-Objekts an die Datenbank gesendet, die dann ein ResultSet zurück gibt. Aus diesem ResultSet müssen dann die Inhalte mittels Parsen mit get-Methoden einzeln ermittelt werden.

// Beispiel zu JDBC (Ausschnitt)
// snip
Statement stmt;
int kundeId;
// snip
try
{
ResultSet rs = stmt.executeQuery(„SELECT * FROM KUNDE “
+ „WHERE KUNDEID = “
+ kundeId);
if (rs.next())
{
String kundeName = rs.getString(„NAME“);
String kundeOrt = rs.getString(„ORT“);
// snip
}
} catch(SQLException ex) {
System.err.println(„SQLException: “ + ex.getMessage());
}
// snip

Diese Vorgehensweise ist nicht nur umständlich, sondern insbesondere auch für Fehler anfällig, und das zur Laufzeit, was besonders unangenehm ist. Rein technisch gesehen, sollte man in obigem Beispiel eigentlich ein preparedStatement-Objekt verwenden, was die Kodierung des Beispiels noch aufwendiger macht. Es ist schon fast ein wenig anachronistisch, wenn man bei der Verwendung der beiden streng typisierten Sprachen – Java und SQL in der Kombination – auf das Prinzip Hoffnung zurückfällt, und Zeichenketten zur Laufzeit zusammensetzt, um damit den kritischsten Teil der Anwendung, den Datenbankzugriff, abzuwickeln. Zudem ist das Parsen des ResultSets ungleich aufwendiger als der Datenbankzugriff in solchen fossilen Sprachen wie COBOL und RPG. Dennoch ist dieses Vorgehen heute noch die meist verwendete Variante im Bereich Java.

Untersucht man vorhandene Java-Programme, dann wird man feststellen, dass der Datenbankteil in der Qualität weit hinter die restliche Anwendung zurückfällt. In den meisten Fällen ist der Code nicht robust genug gegen Laufzeitfehler. Die Eingabe von „O’Hara“ in ein Namensfeld mit doppelten Hochkommas und einfachem Hochkomma innerhalb des Strings überfordert die meisten Java-Anwendungen, die darauf empfindlich mit einem Absturz im Datenbankteil reagieren. Hoffentlich ist das Ganze dann wenigstens Transaktions-gesichert implementiert, damit keine permanenten Fehler in die Datenbank geschrieben werden.

Diese Situation ist nur schwer verständlich angesichts der Tatsache, dass im Bereich Java und Datenbanken auf kurzem Weg gelungen ist, was im Bereich prozeduraler Sprachen nie erreicht wurde: die standardisierte Integration von SQL in die Programmiersprache selber. Am Beginn stand dabei die gemeinsame Entwicklung eines PreCompilers für embedded SQL in Java in einem Projekt unter Beteiligung der führenden Datenbankhersteller unter Federführung von Oracle. Mittlerweile ist SQLJ sogar ANSI normierter Standard für SQL Zugriffe aus Java über SQL. IBM war seit der ersten Stunde an diesem Projekt beteiligt. Auf der AS/400 wird der PreCompiler sogar seit mehreren Releases automatisch mit ausgeliefert und selbst in älteren Releases noch unterstützt. Lediglich die Dokumentation, die mitgeliefert wird, lässt sehr zu wünschen übrig. Zu wenig, von schlechter Qualität.

SQLJ arbeitet ähnlich wie embedded SQL in COBOL, RPG oder einer anderen Programmiersprache. Zunächst wird eine gemischte Quelle aus Java- und SQL-Anweisungen erstellt, diese wird von einem Vorübersetzer in eine reine Java-Quelle übertragen, die dann als Java-Programm umgewandelt wird. Der Vorübersetzer, das Programm sqlj ist selber eine Java-Anwendung und damit auf allen Java-Plattformen ausführbar. Alle aktuellen Java-Installationen ab Version 1.1 sind verwendbar. Der PreCompiler ist im Verzeichnis ext unterhalb von QIBM/ProdData zu finden; die genaue Lokation hängt vom Release der AS/400 ab. SQLJ basiert auf JDBC, man benötigt also noch den JDBC Treiber, der ebenfalls mit OS/400 mitgeliefert wird. Verwendbar sind sowohl der pure Java-Treiber der Toolbox als auch der so genannte native Treiber. Der Toolbox-Treiber kann universell verwendet werden, der native Treiber nur für lokale Zugriffe von der AS/400 auf die eigene Datenbank. Benötigt werden die Archive translator.zip für den Übersetzer und runtime.zip zur Ausführung der erstellten Programme.

Übersetzung eines SQLJ Programmes

Die Programmquelle muss die Endung sqlj haben und sollte so heißen, wie die Java Klasse später heißen soll. Zum Übersetzen wird jetzt das Java-Programm Sqlj aus dem package sqlj.tools ausgeführt, dem man den Dateinamen als Parameter übergibt. Ganz zu Fuß kann das von der Befehlszeile in einem Konsolfenster erfolgen, bequemer wird es, wenn man sich den Aufruf in den Editor seiner Wahl einbindet. Im Classpath müssen dann das JDK, die runtime.zip, die translator.zip, der JDBC-Treiber und alle anderen in der Anwendung benötigten Klassen gefunden werden können.

Aufruf Pre Compiler
java -classpath F:\sqlj\translator.zip;F:\sqlj\runtime.zip;F:\sqlj\jt400Small.jar;. sqlj.tools.Sqlj MyClass.sqlj

Der PreCompiler kann die SQL-Anweisungen auch direkt gegen die Datenbank prüfen, wenn er eine Verbindung herstellen kann. Dazu benötigt er einen Login und muss wissen, wie er verbinden kann. Das wird in einer Datei sqlj.properties konfiguriert, die im Arbeitsverzeichnis mit den Quelldateien gesucht wird. Im Installationsverzeichnis ist eine Beispielkonfiguration. Alle Zeilen die mit # beginnen, sind Kommentarzeilen. Es werden vier Angaben benötigt: Benutzer, Kennwort, JDBC-Treiber und die URL der Datenbank. Diese Konfigurationsdatei wird dann bei der Umwandlung herangezogen für die Herstellung einer Verbindung zur Datenbank zur Prüfung aller SQL-Anweisungen des Programms gegen die Datenbank.

# abspeichern als sqlj.properties im Arbeitsverzeichnis

sqlj.user=bender
sqlj.password=testsqlj
sqlj.driver=com.ibm.as400.access.AS400JDBCDriver
sqlj.url=jdbc:as400://algol/tstjava

Der Vorübersetzer ersetzt nun alle gekennzeichneten SQL-Anweisungen gegen Aufrufe der Runtime aus dem entsprechenden Archiv und fügt weiteren Code ein. Resultat einer erfolgreichen SQLJ-Umwandlung sind dann eine Java-Quelldatei sowie Dateien mit der Endung ser, die ausführbaren Java Byte Code enthalten, der dann mit zu der Anwendung gehört.

Die erfolgreiche Verbindung zur Online-Prüfung testet man am einfachsten mit einem Statement, das auf eine nicht vorhandene Datei zugreift. Wenn das SELECT * FROM GIBTSNICHT nicht mit einer Fehlermeldung bei der Umwandlung quittiert wird, funktioniert die Verbindung noch nicht. Leider wird keine klare Warnung abgegeben, wenn ohne Online-Prüfung gewandelt wird. So kann es also leicht passieren, das wichtige Prüfungen unterbleiben, ohne dass dies sofort auffällt.

Datenbank-Verbindung

Die SQLJ-Programmquelle bekommt den selben Namen, den das spätere Java-Programm haben soll, mit der Endung sqlj und enthält neben den Java-Anweisungen eingebettete SQL-Statements, die zusätzlich gekennzeichnet werden.

// Kopf für Datenbankschicht
package de.bender_dv.database;

import java.sql.*;
import sqlj.runtime.*;
import sqlj.runtime.ref.*;

Für die Datenbankschicht sollte ein eigenes Package eingerichtet werden, über das dann die Zugriffsmöglichkeiten aus der Anwendung auf Komponenten der Klassen differenziert werden kann. Für die Importe aus anderen Packages ist es wichtig, dass in jedem Fall das komplette Paket sqlj.runtime und das Unterpaket sqlj.runtime.ref sowie java.sql.SQLException zur Verfügung gestellt werden. Die SQLException wird in jedem Fall benötigt, da diese aus generierten SQLJ-Abschnitten immer nach oben durchgereicht wird. Das bedeutet auch, dass für alle ausführbaren PreCompiler-Blöcke diese Fehlerklasse behandelt werden muss.

Am besten stellt man java.sql mit einer generischen Import-Anweisung komplett bereit, damit alle Referenzen auf Komponenten dieses Pakets ohne Qualifizierung verwendet werden können. Es ist in jedem Fall darauf zu achten, dass die Datenbankschicht keinerlei Importe auf die Anwendungspakete machen sollte.

Datenbankprogrammierung erfordert immer eine Verbindung zum Datenbank-Server, die für SQLJ über JDBC hergestellt wird. SQLJ erweitert dies um den Begriff des Kontextes, der denjenigen bekannt sein wird, die bereits mit embedded SQL in anderen Programmiersprachen in einem verteilten Umfeld gearbeitet haben. In unserem einfachen Beispiel verwenden wir als einzigen Kontext den per Default bereit gestellten, auf den wir im Programm keine Referenz halten müssen, da wir keinen Kontextwechsel benötigen.

public Connection dbConnect = null;

protected void getJdbcConnection()
throws SQLException
{
String url = „jdbc:as400://“
+ SYSTEM + „/“
+ LIBRARY + „;“
+ „user=“ + USER + „;“
+ „password=“ + PASSWORD ;
try {
Class.forName(„com.ibm.as400.access.AS400JDBCDriver“);
}
catch(ClassNotFoundException e) { }
dbConnect = DriverManager.getConnection(url, USER, PASSWORD);
initContext();
}
DefaultContext initContext() throws SQLException
{
DefaultContext ctx = DefaultContext.getDefaultContext();
if (ctx == null)
{
ctx = new DefaultContext(dbConnect);
DefaultContext.setDefaultContext(ctx);
}
return ctx;
}

Die Connection deklariert man sich in einer Objektvariable, um sie zum Bestandteil des Objektzustandes zu machen; damit vermeidet man, dass man jedes Mal neu verbinden muss. Verbindungsaufbau ist immer eine teure Operation, weshalb man in typischen Web-Anwendungen meistens mit so genannten Connection Pools arbeitet, um Verbindungen zur Datenbank nach Möglichkeit zur Wiederverwendung zu halten.

Die Registrierung des Treibers und die Erstellung der Verbindung mit DriverManager.getConnection() unterscheidet sich nicht von JDBC ohne SQLJ.

In Produktionsprogrammen empfiehlt es sich, ein wenig mehr Augenmerk auf die Properties der Connection zu richten. In diesen optionalen Angaben beim Verbinden mit der Datenbank können wichtige Einstellungen vorgenommen werden, die insbesondere für die Performance wichtig sind. In jedem Fall sollte man den extended dynamic package support aktivieren und dafür sorgen, dass die Datenbankzugriffe unter Commit-Steuerung erfolgen. Ohne Commit-Steuerung ist sicheres Arbeiten mit SQL nicht möglich, da keine Satzsperren gehalten werden, auch nicht nach Lesen zum Fortschreiben. Es lässt sich also nicht ausschließen, dass eine andere Anwendung nach dem Lesen eines Satzes mit einer Satzänderung schneller ist und dieser Update dadurch verloren geht.

Die Erstellung des Kontextes ist hier in einer kleinen Methode initContext() ausgelagert, die man für Programme, die nur einen Kontext verwenden, dann so standardmäßig abschreiben kann. Die SQLException wird hier im Beispiel hochgereicht und nicht abgefangen. Hier muss man darauf achten, dass in keinem Fall SQLExceptions bis zur Anwendung hochgereicht werden, das wäre ein ernsthafter Design-Mangel. Im Beispiel sind die oberen Methoden nicht public, also in der Anwendung nicht sichtbar. Die Methoden, die public und damit aus der Anwendungsschicht erreichbar sind, müssen die SQLExceptions in jedem Fall abfangen und gegebenenfalls gegen anwendungsspezifische Fehlermeldungen austauschen.

PreCompiler-Anweisungen

Anweisungen für den Precompiler werden immer mit der Kennung #sql versehen und müssen mit einem Semikolon abgeschlossen werden. Der SQL-Teil wird in geschweifte Klammern gesetzt. Damit kann man bereits einfache SQL-Statements ohne weiteres absetzen, sobald man eine Verbindung zur Datenbank hergestellt hat.

try{
#sql { DROP TABLE AUSWERTUNG};
#sql { ROLLBACK};
}
catch(SQLException e) {/* Fehlerbehandlung…*/}

Alle SQL-Anweisungen müssen die SQLException mit Java-Fehlerbehandlung entweder weiter hochreichen oder abfangen. Es können alle SQL-Anweisungen mit sqlj verwendet werden, die die Datenbank und der Treiber zulassen. Im Falle von Online-Prüfung bei der Umwandlung wird dies zur Umwandlungszeit gegen die Datenbank geprüft.

Innerhalb der SQL-Statements können selbstverständlich auch so genannte Host-Variablen verwendet werden, die ähnlich wie in embedded SQL in RPG oder COBOL mit einem vorangestellten Doppelpunkt gekennzeichnet werden. Diese Host-Variablen werden dann innerhalb von Java deklariert und im generierten Code dann automatisch in die Statements eingesetzt und die Ergebnisse entsprechend automatisch zurück geliefert. Notwendige Konvertierungen werden bei der Verwendung konformer Typen automatisch vorgenommen. Typ-Inkonsistenzen werden vom PreCompiler zur Umwandlungszeit geprüft und angemahnt. Lediglich bei dem Aufruf von Stored Procedures treten da auch unter aktuellen Releases noch Unglattheiten auf. Unter Nutzung der erweiterten SELECT Anweisung des SQL hat das kleine JDBC-Beispiel vom Anfang dann folgendes Gesicht:

int kundeId;
String kundeName;
String kundeOrt;
try
{
if(dbConnect == null)
getJdbcConnection();

#sql { SELECT NAME, ORT
INTO :kundeName
, :kundeOrt
FROM KUNDE
WHERE KUNDE_ID = :kundeId};
} catch(SQLException e) {
System.err.println(„SQLException: “ + ex.getMessage());
}

Im direkten Vergleich zueinander ist der Quelltext einfacher und besser lesbar geworden, zumindest für all diejenigen, die bereits mit embedded SQL in einer anderen Programmiersprache gearbeitet haben. Mit der Abfrage auf die Existenz des Connection-Objektes wird sichergestellt, dass nur dann verbunden wird, wenn die Verbindung nicht bereits besteht. Das ist nicht nur für die Verarbeitungsgeschwindigkeit wichtig, sondern auch dafür, dass Satzsperren, Dateizeiger und Commit-Steuerung korrekt funktionieren können.

Wesentlich wichtiger als die bessere Lesbarkeit ist allerdings die ungleich größere Robustheit der SQLJ-Variante. Zur Umwandlungszeit finden umfangreiche Prüfungen statt, die Laufzeitfehler so weitgehend ausschließen, wie das möglich ist. Das SQL-Statement wird gegen die Datenbank darauf geprüft, ob es die Tabelle gibt, ob die Felder vorhanden sind, ob die Syntax des SQL-Statements korrekt ist und ob alle SQL-Komponenten in Vergleichen und Zuweisungen typverträglich sind. Des Weiteren werden die Host-Variablen einer Prüfung unterzogen, ob sie korrekt deklariert sind, ob sie aus Java-Sicht typkorrekt verwendet werden und ob sie im SQL-Kontext typverträglich verwendet werden. Der erstellte SQLJ-Code im Beispiel ist in realen Anwendungen bei mehrfacher Ausführung sogar schneller als die JDBC-Variante, da der generierte SQLJ-Code preparedStatement Objekte verwendet.

Ergebnis der Vorumwandlung

Der SQLJ PreCompiler erstellt bei erfolgreicher Umwandlung eine reine Java-Quelle. Dazu werden alle PreCompiler-Sektionen, die ja alle mit # sqlj eingeleitet werden, als Kommentar gekennzeichnet und alle generierten Blöcke mit entsprechenden Anfangs- und End-Kommentaren kenntlich gemacht.

Die entsprechenden SQL-Sektionen werden in Aufrufe von Methoden der SQLJ Runtime umgesetzt. Der resultierende Code ist auf den ersten Blick schwer erkennbar, aber es ist doch leicht feststellbar, dass durchweg preparedStatement-Objekte verwendet werden, was für die Performance im Schnitt von Vorteil ist. Insgesamt gesehen kann man den zusätzlichen Overhead des generierten Codes eher vernachlässigen; dieser wird allenfalls bei optimiertem JDBC-Code als Gegenstück messbar. In den meisten praktischen Fällen dürfte SQLJ sogar schneller sein, da fehlerärmer.

Im Code ist ebenfalls sichtbar, dass SQLJ auf der Ebene von Einzelstatements Thread safe arbeitet. Die SQL-Zugriffe werden auf den verwendeten Kontext synchronisiert.

/*@lineinfo:generated-code*//*@lineinfo:111^3*/

// ************************************************************
// #sql { select KURS
// , TEILNEHMERNAME
// , VORNAME
//
// from TEILNEHMER
// where TEILNEHMER_ID =
// :neu.teilnehmerId };
// ************************************************************

{
sqlj.runtime.profile.RTResultSet __sJT_rtRs;
sqlj.runtime.ConnectionContext __sJT_connCtx = DefaultContext.getDefaultContext();
if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
sqlj.runtime.ExecutionContext __sJT_execCtx = __sJT_connCtx.getExecutionContext();
if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
Integer __sJT_1 = neu.teilnehmerId;
synchronized (__sJT_execCtx) {
sqlj.runtime.profile.RTStatement __sJT_stmt = __sJT_execCtx.registerStatement(__sJT_connCtx, TeilnehmerModel_SJProfileKeys.getKey(0), 1);
try
{
// snip

Neben der Java-Source wird auch noch ein .ser-Objekt erstellt, das ebenfalls lauffähigen Code enthält, also mit der Anwendung verteilt werden muss.

Die Umwandlung der generierten Source erstellt dann schließlich die Java-Class, die den auszuführenden Byte-Code enthält.

Leistungsfähigkeit von SQLJ

In SQLJ-Statements können außer Host-Variablen auch Ausdrücke mit Host-Variablen verwendet werden, sogar die Verwendung von SQL Bestandteilen in Java-Ausdrücken ist möglich. Host-Variablen und Ausdrücke können als Eingabevariablen oder als Ausgabevariablen oder als kombinierte Variablen gekennzeichnet werden.

SQLJ lässt sich auch zum Schreiben von Stored Procedures verwenden und Stored Procedures und User Defined Functions können in SQLJ Ausdrücken verwendet werden.

Mit den zuletzt erwähnten Techniken ist auch eine relativ elegante Einbindung vorhandener RPG- und COBOL-Programme möglich, wenn denn diese Komponenten entsprechend modular programmiert worden sind.

Die Verarbeitung von ResultSet-Objekten wird mit so genannten Iterator-Objekten abgebildet. Dabei gibt es zwei Varianten; die Variante mit benannten Iterator-Objekten erinnert eher an JDBC mit komfortablen generierten Parsing-Methoden, während die Positions Iterator eher an die FETCH-Anweisung erinnert, wie man sie von embedded SQL in RPG oder COBOL kennt.

Die Iterator-Objekte werden in Precompiler-Anweisungen deklariert und bei der Umwandlung werden daraus eigene Java-Klassen generiert. Im Rahmen dieser kurzen Einführung kann nur ein kleiner Überblick über SQLJ gegeben werden und es würde den Beitrag überfordern, alle Möglichkeiten von SQLJ mit Beispielen darzustellen.

Wie weiter mit SQLJ?

Wer bereits mit embedded SQL in anderen Programmiersprachen gearbeitet hat, der kennt das Phänomen, dass der PreCompiler weniger Komfort bietet als der gewohnte Compiler; dies gilt auch für Java. Solange man die Vorumwandlung nicht erfolgreich übersteht, sieht man sich mit kryptischen Fehlermeldungen konfrontiert, insbesondere, wenn der Parser-Strukturfehler im Java-Code antrifft. In solchen Fällen empfiehlt es sich, alle #sql-Sektionen als Kommentar zu kennzeichnen und anschließend zunächst eine Java-Umwandlung vorzunehmen. Damit wird es meist einfacher, die Syntaxfehler zu eliminieren. Dann nimmt man die #sql-Statements wieder hinzu und wandelt die so veränderte sqlj-Quelle erneut.

Die Dokumentationslage ist für die AS/400 nicht berauschend. Es wird nur sehr wenig an Dokumentation angeboten. Die AS/400 stellt wieder einmal das Stiefkind von Big Blue dar, aber diese Rolle hat sie ja häufiger. Einstiegsdokumentationen findet man im Web unter SQLJ.ORG; hier gibt es auch einige Links zu anderen Web-Ressourcen. Die IBM-Webseiten haben im DB2-Bereich einiges über SQLJ, das aber für die AS/400 nicht immer zugeschnitten ist. Die offizielle Referenz zu SQLJ kann man sich problemlos von Oracle.com runter laden. Literatur wird erst zaghaft zu SQLJ angeboten, ebenso sind noch nicht alle Anbieter von Schulungen auf SQLJ eingestellt.

Viele Entwicklungsumgebungen bieten bereits Unterstützung für SQLJ und in den meisten stellt es kein Problem dar, eventuell fehlende Unterstützung nachträglich selber als Plug In einzubinden.
SQLJ hat bereits die Hürde zur ANSI-Norm übersprungen und ist von daher als kommender Standard zu erwarten. Von der Konzeption sieht SQLJ vor, dass bei der Umwandlung auch Server-seitiger Code erstellt werden kann und die Datenbankhersteller arbeiten an entsprechenden Modulen, die dann zum Beispiel SQL Packages auf einer AS/400 erzeugen werden. Spätestens zu diesem Zeitpunkt ist SQLJ dann für static SQL schneller als alle anderen Datenbankzugriffe aus Java.

Den Autor Dieter Bender erreichen Sie per eMail unter:
dieter.bender@midrangemagazin.de