Im Linux-Magazin 9/01 (siehe [1]) haben wir IBMs relationale Datenbank DB2 im Detail beschrieben. Schwerpunkte waren dabei Funktionsumfang und Installation. Die Sicht der Anwendungsentwickler kam hier zum Zug. Nach einer Beschreibung der Pflichtübung, dem Zugriff über JDBC, sehen wir uns jetzt Techniken wie SQLJ, User Defined Functions und Stored Procedures an.
Die Schritte zur Installation sind im Kasten "Die Installation von DB2" nochmals kurz dargestellt, die detaillierte Beschreibung finden Sie in der vorigen Ausgabe des Linux-Magazins. Als Beispiel dient eine Testdatenbank mit dem Namen Jdbctest. Der Kasten "Erstellen einer DB2-Datenbank" erklärt die notwendigen Schritte. Dabei wurde auf Tuning verzichtet, um die Sache nicht unnötig kompliziert zu machen.
Variationen in JDBC
IBM bietet zwei Treiber für den Zugriff mittels JDBC auf DB2-Datenbanken an, den so genannten Application-Driver und den Net-Driver. Ersterer verwendet das Java Native Interface (JNI), um auf die lokale Client-Runtime von DB2 zuzugreifen. Die Abbildung 1 zeigt schematisch, wie es funktioniert. Der Nachteil dieser Architektur ist - außer dem Implementierungsaufwand für eine neue Plattform -, dass auf dem Client eine komplette DB2-Runtime installiert sein muss. Der Vorteil ist, dass der Zugriff auf DB2 mit genau dieser hoch optimierten Runtime erfolgt, also auch ihre Funktionalitäten vorhanden sind und nicht nur ein nach Java portiertes Subset.
Der JDBC-Treiber heißt »COM.ibm.db2. jdbc.app.DB2Driver«, die entsprechende URL für die Testdatenbank lautet »jdbc: db2:jdbctest«. Der Name der Datenbank ist in diesem Fall ein Aliasname, wie er im lokalen Database-Directory definiert ist. Die erforderliche Java-Funktionalität ist in der Datei »db2java.zip« zusammengefasst, die sich im CLASSPATH befinden muss.
Der Net-Driver »COM.ibm.db2.jdbc.net. DB2Driver« dagegen braucht keine lokale DB2-Installation, sondern greift über einen speziellen Server-Daemon (»db2jd«) auf die Datenbank zu (siehe Abbildung 2). Der Serverprozess wird auf dem Datenbankserver unter Angabe der Portnummer mittels
# db2jstrt 5555
gestartet. Der Daemon »db2jd« verwendet die DB2-Client-Runtime für den Serverzugriff, er könnte also auch auf einem dedizierten Rechner laufen, solange die eigentliche Datenbank in seinem Directory katalogisiert ist.
Im Gegensatz zum App-Driver ist die Funktionalität des Net-Drivers etwas eingeschränkt, so wird etwa das Java Transaktion API (JTA) nicht unterstützt oder der Aufbau einer Connection verlangt unbedingt einen User mit einem Passwort. Der App-Driver ermöglicht im Gegensatz dazu auch einen quasi anonymen Zugriff. Bei Verwendung des Net-Drivers sollten die Lizenzbestimmungen beachtet werden: Obwohl nur ein Prozess auf die Datenbank zugreift, ist eine Lizenz pro Client oder die Enterprise-Edition erforderlich.
Der Net-Driver ist insbesondere für die Verwendung innerhalb von Java-Applets aus Browsern heraus gedacht. Hierbei können auch die notwendigen Klassen zusammen mit dem Applet heruntergeladen werden:
<applet code="MyApplet.class" archive=Ó "db2java.zip">
</applet>
Ein Applet darf ja keine Verbindung zu einem anderen Server als dem Quell-Webserver aufbauen, in diesem Fall läuft also »db2jd« auf demselben Server wie der Web-Daemon.
Mein generisches Testprogramm [2], das ich für den Vergleich der Java-SQL-Datenbanken InstantDB und Hypersonic entwickelt und im letzten Coffee-Shop [3] beschrieben habe, ist inzwischen für beide DB2-Driver-Varianten erweitert worden. Der Java-Code musste dafür im Prinzip nicht geändert werden, einzig ein Konfigurationsskript mit den Driver- und URL-Namen wurde angepasst.
Abbildung 1: Die DB2-App-Driver-Architektur.
Abbildung 2: Die DB2-Net-Driver-Architektur.
Abbildung 3: Umwandlung von SQLJ-Code.
Die Alternative: SQLJ
JDBC verwendet dynamisches SQL, das heißt: Die Statements werden erst bei der Programmausführung an die Datenbank geschickt und dort ausgeführt. Das erlaubt auch ein weitgehend datenbankunabhängiges Programmieren, wenn auf Spezialfunktionen des DB-Herstellers verzichtet wird.
Dynamisches SQL ist zwar einfach zu programmieren, hat aber einen großen Nachteil: Die Datenbank kann nicht im Voraus den Zugriff auf die Daten optimieren. Für klassische Sprachen stellen die DB-Hersteller deshalb Präprozessoren bereit, die proprietären Quellcode gleichzeitig in normalen Quellcode und in datenbankspezifische Packages verwandeln (siehe Abbildung 3). Das Package wird anschließend automatisch oder manuell an die Datenbank gebunden. Das Anwendungsprogramm greift später über dieses Package auf die Datenbank zu.
Da in vielen Programmen oft statisches SQL verwendet wird (etwa »INSERT«-Statements gefüllt mit Werten aus einer Maske), bietet hier dynamisches SQL keine Vorteile. Es ist nur notwendig, wenn tatsächlich die Inserts, Updates oder Queries dynamisch erstellt werden. Der Nachteil des zusätzlichen Präprozessor-Schritts ist bei Verwendung eines Makefile leicht zu verkraften.
Statisches SQL ist aber nicht notwendigerweise immer performanter, insbesondere wenn sich die Daten seit dem Binden stark verändert haben, könnten die dynamisch aufbereiteten Statements sogar bessere Zugriffspfade haben.
Das statische SQL heißt unter Java SQLJ [4]. Hierbei sind Datenbankhersteller und nicht Sun die treibenden Kräfte. SQLJ ist ein Ansi-Standard, damit sollte es einfach möglich sein, den Quellcode auf verschiedene SQLJ-fähige Datenbanken zu portieren.
In Listing 1 ist ein Ausschnitt aus einem SQLJ-Programm zu sehen. Hier wird ein Insert durchgeführt, wobei das normale »INSERT«-Statement über so genannte Host-Variablen gefüllt wird, sie sind durch den Doppelpunkt gekennzeichnet. Der SQLJ-Präprozessor (unter DB2 naheliegenderweise »sqlj« genannt) wandelt den Code in normalen Java-Quellcode um, der anschließend ganz normal kompiliert werden kann (siehe Listing 2). Gleichzeitig wird ein serialisiertes Objekt einer Hilfsklasse generiert (im Beispiel in der Datei »SQLJTest_SJProfile0.ser«). Sie dient als Grundlage für die Erzeugung des Packages, muss aber auch zur Laufzeit im richtigen Java-Package vorliegen, im Beispiel also im »de/bablokb /jdbc/«-Verzeichnis.
Bei statischem SQL ist noch zu beachten, dass alle verwendeten Datenbank-Objekte auch existieren. Das »INSERT«-Statement in Listing 1 verweist zum Beispiel auf die Tabelle »FileInfo«. Existiert sie nicht, kann der SQLJ-Installer den Code nicht an die Datenbank binden. »CREATE«- und »DROP-Statements sind hier unproblematisch, da sie auf Systemtabellen operieren.
Für den Java-Aufruf in Listing 2 gibt es auch einen Wrapper (»db2profc«), der allerdings nicht die Classic-VM selektiert. Und die Hotspot-Engine steigt bei mir mit der Bitte aus, einen Bugreport an Sun zu schicken.
Anstatt der Angabe »package« in den »prepoptions« kann auch »bind« angegeben werden. Dann wird eine so genannte Bind-Datei erzeugt, die später in einem separaten Schritt an die Datenbank gebunden wird. Dieses Vorgehen ist realistischer, da während der Entwicklung gegen eine Test-DB gearbeitet und das Bind-File später von einem Administrator an die produktive Datenbank gebunden wird.
Listing 1: SQLJTest.sqlj
|
021: package de.bablokb.jdbc;
023: import java.io.*;
024: import java.sql.*;
025: import sqlj.runtime.*;
026: import sqlj.runtime.ref.*;
027:
035: public class SQLJTest {
148:
149: private void insertStatic(String filename, int size, Timestamp modtime,
150: String username, String groupname, int links, String perms)
151: throws Exception {
152: #sql { INSERT INTO FileInfo
153: (filename,size,modtime,username,groupname,links,perms)
154: VALUES(:filename,:size,:modtime,:username,:groupname,:links,:perms)
155: };
156: }
169: }
|