Kaffeesatz für Datenbanker
Seit dem JDK 1.1 ist JDBC 1.22 Bestandteil der Java-Entwicklungs- und -Laufzeitumgebung. Die jetzt von Sun freigegebene Version 2.0 verspricht bessere Performance und allerlei neue Funktionen.
- Rainer Klute
Neben funktionalen Erweiterungen soll JDBC 2.0 bessere Performance bieten und eine Reihe von Kleinigkeiten korrigieren. Besonderes Augenmerk hat Sun darauf gerichtet, daß bestehende Anwendungen unverändert weiterlaufen können - vorausgesetzt, die nötigen Treiber sind verfügbar. Der Middleware-Hersteller OpenLink beispielsweise plant, ab August mit einem JDBC-2.0-Treiber an den Markt zu gehen.
Damit JDBC trotz neuer Funktionen schön übersichtlich bleibt, hat es Sun nicht bei dem einen Paket java.sql belassen, sondern ein zweites eingeführt: Die grundlegenden Funktionen (JDBC core API) sind nach wie vor in java.sql enthalten; das neue Paket javax.sql enthält einen Teil der Erweiterungen (JDBC standard extension API). Ein wichtiger Unterschied zwischen beiden Paketen ist - abgesehen vom Inhalt - organisatorischer Art: java.sql wird den jeweils aktuellen Java-Implementierungen beigelegt, während man javax.sql separat installieren muß. Sun verspricht immerhin, daß interessierte Entwickler sich diese Software kostenlos herunterladen können. Hier liegt auch die Spezifikation.
Inhaltlich umfaßt javax.sql diejenigen neuen JDBC-Features, die nicht zum Standardsprachumfang gehörende Java-Eigenschaften nutzen. Außerdem gibt es in javax.sql Klassen wie RowSet, die das Komponentenmodell JavaBeans unterstützen. Viel mehr läßt sich zum Erweiterungspaket leider noch nicht sagen, da es bislang keine Dokumentation gibt.
Im Basispaket java.sql bleibt es im wesentlichen bei einem Datenbankzugriff auf SQL-Niveau mit rudimentärer Objektorientierung. Der Entwickler packt eine SQL-Anweisung als String in ein Statement-Objekt ein und übermittelt es an das Datenbanksystem. Das Ergebnis eines SELECT-Statements kommt als ResultSet zurück, INSERT, UPDATE oder DELETE liefern die Anzahl der betroffenen Zeilen.
ResultSet erheblich erweitert
Ein ResultSet enthält die Zeilen der Ergebnistabelle. Hier hat sich in der neuen JDBC-Version eine Menge getan: Ergebnismengen sind künftig frei positionierbar, aktualisierbar und potentiell performanter als zuvor.
- Während eine JDBC-Anwendung ein ResultSet bislang lediglich sequentiell von vorn nach hinten lesen konnte, kann sie darin künftig beliebig positionieren: die Ergebnismenge ist 'scrollable'. Die Anwendung kann sie von hinten nach vorn durchlaufen oder auf einzelne Sätze relativ oder absolut positionieren.
- Auf Wunsch wirken sich Änderungen in der Datenbank unmittelbar auf das ResultSet aus. So liefert das Lesen desselben Datensatzes unterschiedliche Resultate, wenn zwischen zwei Lesezugriffen auf die Ergebnismenge die zugrundeliegenden Daten in der Datenbank geändert wurden.
- Eine JDBC-2.0-Anwendung kann beim Treiber ein aktualisierbares ResultSet anfordern. Sie kann die Ergebnismenge dann nicht nur lesen, sondern auch ändern und die Modifikationen in der Datenbank speichern. Bei reinen Abfragen sollte man jedoch besser ein herkömmliches ResultSet wählen: (Potentielle) Schreibzugriffe und die zugehörigen Datenbanksperren können die Ablaufgeschwindigkeit einer Applikation deutlich reduzieren.
- Eine weitere Neuigkeit soll der Performance guttun: Die Anwendung kann dem JDBC-Treiber mitteilen, wieviel Sätze er idealerweise auf einmal aus der Datenbank lesen sollte und ob die Anwendung das ResultSet vorwärts oder rückwärts durchzugehen gedenkt. Auf die Anwendungslogik haben diese Angaben keinen Einfluß. Ob und wie der JDBC-Treiber die Angaben beachtet, bleibt ihm überlassen.
Das Beispiel in Listing 1 verwendet ein Scroll-sensitives und aktualisierbares ResultSet und setzt entsprechende Parameter beim Erzeugen des Statement-Objekts. Zusätzlich gibt die Anwendung dem Treiber den Hinweis, daß er möglichst immer 25 Datensätze auf einmal lesen sollte.
Listing 1
Flexibler und schneller als zuvor: positionier- und aktualisierbare ResultSets.
Connection con = DriverManager.getConnection(
'jdbc:subprotocol:subname');
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
stmt.setFetchSize(25);
ResultSet rs = stmt.executeQuery(
'SELECT pers_nr, gehalt FROM personal');
In der ermittelten Ergebnismenge setzen die folgenden Anweisungen das Gehalt des Mitarbeiters mit der Personalnummer 4711 auf 9500 DM:
rs.first();
rs.updateString(1, '4711');
rs.updateFloat('gehalt', 9500.0f);
rs.updateRow();
rs.first() positioniert auf den ersten Satz im ResultSet, rs.updateString(1, '4711') schreibt in die erste Spalte des Satzes die Personalnummer 4711 und rs.updateFloat('gehalt', 9500.0f) setzt die Spalte mit dem Namen gehalt auf 9500. Ohne die Anweisung rs.updateRow() wäre alles sinnlos: Erst dieser Befehl schreibt den geänderten Datensatz in die Datenbank zurück.
Für das Einfügen eines neuen Datensatzes ist eine spezielle Zeile zuständig. Sie existiert nicht in der Datenbank, sondern ausschließlich im ResultSet. Mit moveToInsertRow() positioniert die Anwendung auf die Einfügezeile, mit Methoden wie updateString() oder updateInt() füllt sie die Spalten mit Werten und schreibt schließlich die Zeile mit insertRow() in die Datenbank. Die Methode moveToCurrentRow() positioniert an die Stelle im ResultSet zurück, an der die Anwendung vor dem Einfügen war.
Apropos Positionieren: Bislang gab es außer next() nichts zu diesem Thema. Nun stehen Methoden namens first(), afterLast(), previous(), relative() und absolute() dafür zur Verfügung. isFirst() und isAfterLast() dienen der Positionsbestimmung.
Natürlich lassen sich Datenbankänderungen auch ganz herkömmlich über Statement-Objekte und darin enthaltene INSERT- oder UPDATE-Anweisungen durchführen. Neu ist die Möglichkeit, mehrere Anweisungen in einem einzigen Statement zu bündeln und in einem Rutsch zur Datenbank zu schicken (siehe Listing 2).
Listing 2
In einem Statement gebündelte Kommandos werden stapelweise in der Datenbank abgearbeitet.
// Autocommit ausschalten:
con.setAutoCommit(false);
Statement stmt = con.createStatement();
stmt.addBatch('INSERT INTO employees VALUES (1000, 'Joe Jones')');
stmt.addBatch('INSERT INTO departments VALUES (260, 'Shoe')');
stmt.addBatch('INSERT INTO emp_dept VALUES (1000, 260)');
// Anweisungen ausführen:
int[] updateCounts = stmt.executeBatch();
Diese Batch-Änderungen können schneller ausgeführt werden, als wenn jede SQL-Anweisung in einem eigenen Statement-Objekt separat zum DBMS übertragen würde. Die Autocommit-Funktion dabei auszuschalten ist sinnvoll, weil die Datenbank nach einem Fehler in irgendeiner der Anweisungen in einem inkonsistenten Zustand geraten könnte, der sich nur schwer reparieren läßt. Kapselt man hingegen alle Anweisungen in einer Transaktion, läßt sich der alte Zustand durch einen Aufruf von Connection.rollback() leicht wiederherstellen. Batch-Updates sind natürlich auch mit PreparedStatement und CallableStatement möglich.
Eine Anwendung will möglicherweise nicht nur selbst Änderungen an Tabellen durchführen, sondern auch über Änderungen informiert werden, die andere Prozesse oder andere Thread desselben Java-Prozesses gemacht haben. Die ResultSet-Methoden wasUpdated(), wasDeleted() und wasInserted() liefern diese Informationen. Ganz unproblematisch sind sie allerdings nicht: Erstens bekommt das ResultSet von Änderungen, die noch in einer anderen Transaktion stecken, ohnehin nichts mit, zweitens muß es - im Unterschied zu ResultSets alten Typs - sensitiv gegen Änderungen sein, und drittens muß der JDBC-Treiber dieses Feature unterstützen. Wer sichergehen will, befragt die Metadaten der Datenbankverbindung, was möglich ist und was nicht, etwa mit dmd. deletesAreDetected(ResultSet.TYPE_SCROLL_SENSITIVE), wobei dmd vom Typ DatabaseMetaData ist.
Positionier- und Änderbarkeit in einem ResultSet sind, ebenso wie die Sensitivität gegenüber Änderungen, optionale Eigenschaften. Das heißt, Sun kann einem Treiber auch dann das Etikett 'JDBC compliant' ausstellen, wenn dieser keine positionierbaren ResultSets unterstützt. Ein Treiberhersteller hat verschiedene Möglichkeiten: Er kann für die Implementierung der Positionierfähigkeit auf entsprechende Eigenschaften des DBMS zurückgreifen. Falls dieses hier nichts bietet, kann er die notwendigen Funktionen im Treiber selbst realisieren. Schlimmstenfalls läßt er sie einfach weg.
Mit der lockeren Regelung dürfte es Sun gelingen, in relativ kurzer Zeit auf viele JDBC-2.0-verträgliche Treiber verweisen zu können. Ob die Firma das Label 'JDBC compliant' damit aufwerten kann, darf man jedoch getrost bezweifeln. Der Anwendungsentwickler ist jedenfalls stets gefordert, sich anhand der Metadaten darüber zu informieren, welche Eigenschaften sein JDBC-Treiber oder sein DBMS nicht anbieten.
Das JDBC-2.0-API wendet sich mit einer speziellen Erweiterung an sogenannte Java-relationale Datenbanksysteme. Laut Sun arbeiten einige Hersteller an DBMS, die neben den üblichen SQL-Datentypen einen neuen Typ namens JAVA_OBJECT unterstützen. Mit JDBC 1.22 konnte man schon bisher serialisierte Java-Objekte mittels setObject() in Datenbanken speichern und mit getObject() lesen. Allerdings läßt sich einer Tabellenspalte etwa vom Typ VARCHAR nicht ansehen, ob sie Java-Bytecode enthält oder irgendwelche anderen Zeichen.
Neuer Datentyp: JAVA_OBJECT
Sofern man nicht gerade unbekannte Tabellen analysiert, ist das Fehlen des Typs JAVA_OBJECT in der Praxis kein Problem: die Anwendung kennt ihre Tabellen und weiß, ob und wo Java-Objekte gespeichert sind. Ist der Datentyp JAVA_OBJECT jedoch verfügbar, kann die Applikation alle Spalten mit Java-Objekten dingfest machen. Der JDBC-Metadatenmechanismus versetzt sie außerdem in die Lage, die Klassennamen der einzelnen Objekte zu ermitteln. Mit einem herkömmlichen DBMS muß sie eine zusätzliche Spalte verwenden und dort den Klassennamen speichern.
Das kommende SQL3 wird zum einen die verbreiteten Datentypen BLOB (Binary Large Object) und CLOB (Character Large Object) standardisieren, zum anderen erfreut SQL3 die Anwender mit dem 'revolutionären' Konzept benutzerdefinierter Datentypen: Strukturen, Felder und Referenzen. JDBC 2.0 bedient die neuen Eigenschaften durch entsprechende Methoden; beispielsweise liest
Array a = rs.getArray(1);
aus Spalte 1 der aktuellen Zeile ein Array ein. Das Objekt a beziehungsweise das Interface Array wiederum verfügt über Zugriffsmethoden für die einzelnen Feldwerte. Analog liefert ein Zugriff wie
Struct struct = (Struct) rs.getObject(2);
die in Spalte 2 enthaltene SQL3-Struktur. Ebenfalls analog zu Array besitzt Struct Methoden zum Zugriff auf die Attribute der Struktur. Es wäre allerdings reichlich mühsam und umständlich, immer über diese Methoden zuzugreifen. Der Entwickler kann statt dessen eine Zuordnung zwischen benutzerdefinierten SQL-Datentypen und Java-Klassen festlegen und anschließend Java-Objekte unmittelbar aus Datenbankzellen füllen. Jede JDBC-2.0-Datenbankverbindung besitzt eine Zuordnungstabelle, die von der Anwendung abgerufen und modifiziert werden kann. Die Tabelle ist ein Objekt der Klasse java.util.Map - eine Neuigkeit der JDK-Version 1.2. Per Voreinstellung bildet sie SQL3-Strukturen auf die JDBC-Klasse Struct ab. Die Anwendung kann abweichende Zuordnungen in diese Tabelle eintragen.
Manches ist noch kompliziert
So einfach, wie sich das anhört, ist es allerdings nicht. Eine Java-Klasse mit einem SQL3-Pendant in der Datenbank muß ihre einzelnen Komponenten nach in der JDBC-2.0-Spezifikation genau festlegten Regeln einlesen und wegschreiben. Die Klasse muß das Interface SQLData und deren Methoden readSQL() und writeSQL() implementieren. Diese Methoden rufen wiederum read...()- beziehungsweise write...()-Methoden der neuen Stream-Klassen SQLInput und SQLOutput auf, die der JDBC-Treiber zur Verfügung stellt. Das ist kompliziert und fehlerträchtig. Sun geht aber davon aus, daß Entwickler Werkzeuge einsetzen werden, die aus SQL3-Datendefinitionen Java-Klassen einschließlich Implementierungen von readSQL() und writeSQL() generieren. Wer den Mechanismus erst einmal verstanden und sich mit oder ohne Werkzeug durch die Implementierung gekämpft hat, wird mit persistenten Objekten belohnt.
JDBC 2.0 beseitigt ein Ärgernis im Zusammenhang mit Ein- und Ausgabeströmen: Bislang beherrschte JDBC lediglich die Stream-Klassen, die mit dem Java-Datentyp byte arbeiten, also mit 8-Bit-Zeichen. Für Debug-Ausgaben beispielsweise konnte man lediglich DriverManager.setLogStream(out) nutzen, um den PrintStream out als Ausgabestrom anzumelden. Die flexibleren Klassen Reader und Writer gab es nicht. Sie arbeiten mit 16-Bit-Zeichen und lassen sich auch auf 8-Bit-Zeichenströme abbilden. Die neue Spezifikation erklärt setLogStream(out) für 'deprecated' (mißbilligt), führt die Methode setLogWriter(out) ein und entspricht damit dem Stand der Zeichen-Technik.
Rainer Klute
ist geschäftsführender Gesellschafter der NADS GmbH in Dortmund und Autor der Bücher 'Das World Wide Web' und 'JDBCTM in der Praxis' (beide erschienen bei Addison-Wesley).
Literatur
[1] Jörn Turner, Stefan Zorn; JDBC; Heiße Tasse; Datenbankzugriff mit Java; iX 3/97, S. 120 ff.
iX-TRACT
- Die Java-Datenbank-Schnittstelle JDBC bietet in der Version 2.0 neben verschiedenen Erweiterungen bessere Performance.
- Entwickler können in ResultSets jetzt frei positionieren und sie als aktualisierbar festlegen.
- Die neue Spezifikation unterstützt SQL3-Datentypen.
(jd)