Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 7. Auflage
 <<    <     >    >>   API  Kapitel 44 - Datenbankzugriffe mit JDBC

44.4 Weiterführende Themen



In diesem Abschnitt wollen wir eine Reihe von Themen ansprechen, die bei der bisherigen Darstellung zu kurz gekommen sind. Aufgrund des beschränkten Platzes werden wir jedes Thema allerdings nur kurz ansprechen und verweisen für genauere Informationen auf die JDBC-Beschreibung (sie ist Bestandteil der seit dem JDK 1.2 ausgelieferten Online-Dokumentation) und auf weiterführende Literatur zum Thema JDBC.

44.4.1 Metadaten

In Abschnitt 44.3.3 sind wir bereits kurz auf die Verwendung von Metadaten eingegangen. Neben den Datenbankmetadaten gibt es die Methode getMetaData der Klasse ResultSet:

ResultSetMetaData getMetaData()
java.sql.ResultSet

Sie liefert ein Objekt vom Typ ResultSetMetaData, das Meta-Informationen über die Ergebnismenge zur Verfügung stellt. Wichtige Methoden sind:

int getColumnCount()

String getColumnName(int column)

String getTableName(int column)

int getColumnType(int column)
java.sql.ResultSetMetaData

Mit getColumnCount kann die Anzahl der Spalten in der Ergebnismenge abgefragt werden. getColumnName und getTableName liefern den Namen der Spalte bzw. den Namen der Tabelle, zu der diese Spalte in der Ergebnismenge gehört, wenn man ihren numerischen Index angibt. Mit getColumnType kann der Datentyp einer Spalte abgefragt werden. Als Ergebnis wird eine der statischen Konstanten aus der Klasse java.sql.Types zurückgegeben.

44.4.2 Escape-Kommandos

Mit den Escape-Kommandos wurde ein Feature eingeführt, das die Portierbarkeit von Datenbankanwendungen verbessern soll. In Anlehnung an ODBC fordert die JDBC-Spezifikation dazu, dass die JDBC-Treiber in der Lage sein müssen, besondere Zeichenfolgen in SQL-Anweisungen zu erkennen und in die spezifische Darstellung der jeweiligen Datenbank zu übersetzen. Auf diese Weise können beispielsweise Datums- und Zeitliterale portabel übergeben oder eingebaute Funktionen aufgerufen werden. Die Escape-Kommandos haben folgende Syntax:

"{" <Kommandoname> [<Argumente>] "}"

Am Anfang steht eine geschweifte Klammer, dann folgen der Name des Escape-Kommandos und mögliche Argumente und am Ende wird das Kommando durch eine weitere geschweifte Klammer abgeschlossen.

Um beispielsweise unabhängig von seiner konkreten Darstellung einen Datumswert einzufügen, kann das Escape-Kommando »d« verwendet werden. Es erwartet als Argument eine SQL-Zeichenkette im Format »yyyy-mm-dd« und erzeugt daraus das zur jeweiligen Datenbank passende Datumsliteral. In Listing 44.5 haben wir dieses Kommando verwendet, um das Änderungsdatum der Datei in die Tabelle file zu schreiben.

44.4.3 Transaktionen

Die drei Methoden commit, rollback und setAutoCommit des Connection-Objekts steuern das Transaktionsverhalten der Datenbank:

void commit()

void rollback()

void setAutoCommit(boolean autoCommit)
java.sql.Connection

Nach dem Aufbauen einer JDBC-Verbindung ist die Datenbank (gemäß JDBC-Spezifikation) zunächst im Auto-Commit-Modus. Dabei gilt jede einzelne Anweisung als separate Transaktion, die nach Ende des Kommandos automatisch bestätigt wird. Durch Aufruf von setAutoCommit und Übergabe von false kann das geändert werden. Danach müssen alle Transaktionen explizit durch Aufruf von commit bestätigt bzw. durch rollback zurückgesetzt werden. Nach dem Abschluss einer Transaktion beginnt automatisch die nächste.

Wichtig ist auch der Transaction Isolation Level, mit dem der Grad der Parallelität von Datenbanktransaktionen gesteuert wird. Je höher der Level, desto weniger Konsistenzprobleme können durch gleichzeitigen Zugriff mehrerer Transaktionen auf dieselben Daten entstehen. Umso geringer ist aber auch der Durchsatz bei einer großen Anzahl von gleichzeitigen Zugriffen. Transaction Isolation Levels werden von der Datenbank üblicherweise mit Hilfe von gemeinsamen und exklusiven Sperren realisiert. JDBC unterstützt die folgenden Levels:

Mit Hilfe der beiden Methoden getTransactionIsolation und setTransactionIsolation des Connection-Objekts kann der aktuelle Transaction Isolation Level abgefragt bzw. verändert werden:

int getTransactionIsolation()

void setTransactionIsolation(int level)
java.sql.Connection

Mit der Methode supportsTransactionIsolationLevel des DatabaseMetaData-Objekts kann abgefragt werden, ob eine Datenbank einen bestimmten Transaction Isolation Level unterstützt oder nicht.

44.4.4 JDBC-Datentypen

In den meisten Fällen braucht man keine exakte Kenntnis des Datentyps einer Tabellenspalte, wenn man diese abfragt. Die oben beschriebenen get-Methoden des ResultSet-Objekts führen geeignete Konvertierungen durch. Soll dagegen mit CREATE TABLE eine neue Datenbank definiert werden, muss zu jeder Spalte der genaue Datentyp angegeben werden. Leider unterscheiden sich die Datenbanken bezüglich der unterstützten Typen erheblich und die CREATE TABLE-Anweisung ist wenig portabel. Die Klasse java.sql.Types listet alle JDBC-Typen auf und gibt für jeden eine symbolische Konstante an. Mit der Methode getTypeInfo der Klasse DatabaseMetaData kann ein ResultSet mit allen Typen der zugrunde liegenden Datenbank und ihren spezifischen Eigenschaften beschafft werden. In Tabelle 44.4 findet sich eine Übersicht der wichtigsten SQL-Datentypen.

44.4.5 Umgang mit JDBC-Objekten

Wie zuvor erwähnt, sind die JDBC-Objekte des Typs Connection und Statement möglicherweise kostspielig bezüglich ihres Rechenzeit- oder Speicherverbrauchs. Es empfiehlt sich daher, nicht unnötig viele von ihnen anzulegen.

Während das bei Connection-Objekten einfach ist, kann es bei Statement-Objekten unter Umständen problematisch werden. Wird beispielsweise in einer Schleife mit vielen Durchläufen immer wieder eine Methode aufgerufen, die eine Datenbankabfrage durchführt, so stellt sich die Frage, woher sie das dafür erforderliche Statement-Objekt nehmen soll. Wird es jedes Mal lokal angelegt, kann schnell der Speicher knapp werden. Wird es dagegen als statische oder als Klassenvariable angelegt, kann es zu Konflikten mit konkurrierenden Methoden kommen (die üblichen Probleme globaler Variablen).

Eine gut funktionierende Lösung für dieses Problem besteht darin, Statement-Objekte auf der Connection zu cachen, also zwischenzuspeichern. Das kann etwa mit einer Queue erfolgen, in die nicht mehr benötigte Statement-Objekte eingestellt werden. Anstelle eines Aufrufs von createStatement wird dann zunächst in der Queue nachgesehen, ob ein recyclebares Objekt vorhanden ist, und dieses gegebenenfalls wiederverwendet. Es sind dann zu keinem Zeitpunkt mehr Statement-Objekte angelegt, als parallel benötigt werden. Natürlich dürfen nur Objekte in die Queue gestellt werden, die nicht mehr benötigt werden; ihr ResultSet sollte also vorher möglichst geschlossen werden. Das Statement-Objekt selbst darf nicht geschlossen werden, wenn es noch verwendet werden soll.

Eine einfache Implementierung wird in dem folgendem Listing vorgestellt. Das Objekt vom Typ CachedConnection wird mit einem Connection-Objekt instanziert. Die Methoden getStatement und releaseStatement dienen dazu, Statement-Objekte zu beschaffen bzw. wieder freizugeben.

001 /* CachedConnection.java */
002 
003 import java.sql.*;
004 import java.util.*;
005 
006 public class CachedConnection
007 {
008   private Connection             con;
009   private LinkedList<Statement>  cache;
010   private int                    stmtcnt;
011 
012   public CachedConnection(Connection con)
013   {
014     this.con     = con;
015     this.cache   = new LinkedList<Statement>();
016     this.stmtcnt = 0;
017   }
018 
019   public Statement getStatement()
020   throws SQLException
021   {
022     if (cache.size() <= 0) {
023       return con.createStatement();
024     } else {
025       return cache.poll();
026     }
027   }
028 
029   public void releaseStatement(Statement statement)
030   {
031     cache.add(statement);
032   }
033 }
CachedConnection.java
Listing 44.11: Die Klasse CachedConnection

Es ist wichtig, die JDBC-Objekte auch dann zu schließen, wenn eine Ausnahme während der Bearbeitung aufgetreten ist. Andernfalls würden möglicherweise Ressourcen nicht freigegeben und das Programm würde so nach und nach mehr Speicher oder Rechenzeit verbrauchen. Am einfachsten kann dazu die finally-Klausel der try-catch-Anweisung verwendet werden. Ab Version 7 des JDK bietet es sich an, die AutoCloseable-Funktionalität der Ressourcen zu nutzen (wie in Abschnitt 13.4 beschrieben).

Eine weitere Eigenschaft der Klasse ResultSet verdient besondere Beachtung. Bei manchen JDBC-Treibern erlauben die zurückgegebenen ResultSet-Objekte das Lesen einer bestimmten Tabellenspalte nur einmal. Der zweite Versuch wird mit einer Fehlermeldung »No data« (oder ähnlich) quittiert. Manche Treiber erfordern sogar, dass die Spalten des ResultSet in der Reihenfolge ihrer Definition gelesen werden. In beiden Fällen ist es gefährlich, einen ResultSet als Parameter an eine Methode zu übergeben, denn die Methode weiß nicht, welche Spalten bereits vom Aufrufer gelesen wurden und umgekehrt. Eine Lösung könnte darin bestehen, einen ResultSet mit integriertem Cache zu entwickeln, der sich bereits gelesene Spaltenwerte merkt. Alternativ könnte man auch einen objekt-relationalen Ansatz versuchen, bei dem jeder gelesene Satz der Ergebnismenge direkt ein passendes Laufzeitobjekt erzeugt, das dann beliebig oft gelesen werden kann. Wir wollen auf beide Varianten an dieser Stelle nicht weiter eingehen.

 Warnung 

44.4.6 Prepared Statements

Prepared Statements sind parametrisierte SQL-Anweisungen. Sie werden zunächst deklariert und zum Vorkompilieren an die Datenbank übergeben. Später können sie dann beliebig oft ausgeführt werden, indem die formalen Parameter durch aktuelle Werte ersetzt werden und die so parametrisierte Anweisung an die Datenbank übergeben wird. Der Vorteil von Prepared Statements ist, dass die Vorbereitungsarbeiten nur einmal erledigt werden müssen (Syntaxanalyse, Vorbereitung der Abfragestrategie und -optimierung) und die tatsächliche Abfrage dann wesentlich schneller ausgeführt werden kann. Das bringt Laufzeitvorteile bei der wiederholten Ausführung der vorkompilierten Anweisung.

JDBC stellt Prepared Statements mit dem Interface PreparedStatement, das aus Statement abgeleitet ist, zur Verfügung. Die Methode prepareStatement des Connection-Objekts liefert ein PreparedStatement:

public PreparedStatement prepareStatement(String sql)
  throws SQLException
java.sql.Connection

Als Argument wird ein String übergeben, der die gewünschte SQL-Anweisung enthält. Die formalen Parameter werden durch Fragezeichen dargestellt. Bei den meisten Datenbanken dürfen sowohl Änderungs- als auch Abfrageanweisungen vorkompiliert werden. Sie werden dann später mit executeQuery bzw. executeUpdate ausgeführt. Anders als im Basisinterface sind diese Methoden im Interface PreparedStatement parameterlos:

public ResultSet executeQuery()
  throws SQLException

public int executeUpdate()
  throws SQLException
java.sql.PreparedStatement

Bevor eine dieser Methoden aufgerufen werden darf, ist es erforderlich, die vorkompilierte Anweisung zu parametrisieren. Dazu muss für jedes Fragezeichen eine passende set-Methode aufgerufen und das gewünschte Argument übergeben werden. Die set-Methoden gibt es für alle JDBC-Typen (siehe beispielsweise die analoge Liste der get-Methoden in Tabelle 44.1):

public void setBoolean(int parameterIndex, boolean x)
  throws SQLException

public void setByte(int parameterIndex, byte x)
  throws SQLException

...
java.sql.PreparedStatement

Der erste Parameter gibt die Position des Arguments in der Argumentliste an. Das erste Fragezeichen hat den Index 1, das zweite den Index 2 usw. Der zweite Parameter liefert den jeweiligen Wert, der anstelle des Fragezeichens eingesetzt werden soll.

Als Beispiel wollen wir uns eine abgewandelte Form der in Abschnitt 44.3.5 vorgestellten Methode countRecords ansehen, bei der anstelle eines Statement-Objekts ein PreparedStatement verwendet wird:

001 public static void countRecords()
002 throws SQLException
003 {
004   PreparedStatement pstmt = con.prepareStatement(
005     "SELECT count(*) FROM ?"
006   );
007   String[] aTables = {"dir", "file"};
008   for (int i = 0; i < aTables.length; ++i) {
009     pstmt.setString(1, aTables[i]);
010     ResultSet rs = pstmt.executeQuery();
011     if (!rs.next()) {
012       throw new SQLException("SELECT COUNT(*): no result");
013     }
014     System.out.println(aTables[i] + ": " + rs.getInt(1));
015   }
016   pstmt.close();
017 }
Listing 44.12: Verwenden eines PreparedStatement

Das PreparedStatement enthält hier den Namen der Tabelle als Parameter. In einer Schleife nehmen wir nun für die Tabellen »dir« und »file« jeweils eine Parametrisierung vor und führen dann die eigentliche Abfrage durch. Der Rückgabewert von executeQuery entspricht dem der Basisklasse, so dass der obige Code sich prinzipiell nicht von dem in Abschnitt 44.3.5 unterscheidet.

Nicht alle Datenbanken erlauben es, Tabellennamen zu parametrisieren, sondern beschränken diese Fähigkeit auf Argumente von Such- oder Änderungsausdrücken. Unser Programm läuft beispielsweise nicht mit MS Access 95.

 Warnung 

44.4.7 SQL-Kurzreferenz

Dieser Abschnitt gibt eine kurze Übersicht der gebräuchlichsten SQL-Anweisungen in ihren grundlegenden Ausprägungen. Er ersetzt weder ein Tutorial noch eine Referenz und ist zu keinem der bekannten SQL-Standards vollständig kompatibel. Trotzdem mag er für einfache Experimente nützlich sein und helfen, die ersten JDBC-Anbindungen zum Laufen zu bringen. Für »ernsthafte« Datenbankanwendungen sollte zusätzliche Literatur konsultiert und dabei insbesondere auf die Spezialitäten der verwendeten Datenbank geachtet werden.

Die nachfolgenden Syntaxbeschreibungen sind an die bei SQL-Anweisungen übliche Backus-Naur-Form angelehnt:

Ändern von Datenstrukturen

Mit CREATE TABLE kann eine neue Tabelle angelegt werden. Mit DROP TABLE kann sie gelöscht und mit ALTER TABLE ihre Struktur geändert werden. Mit CREATE INDEX kann ein neuer Index angelegt, mit DROP INDEX wieder gelöscht werden.

CREATE TABLE TabName
  (ColName DataType [DEFAULT ConstExpr]
  [ColName DataType [DEFAULT ConstExpr]]...)

ALTER TABLE TabName
  ADD (ColName DataType
      [ColName DataType]...)

CREATE [UNIQUE] INDEX IndexName
  ON TabName
  (ColName [ASC|DESC]
  [, ColName [ASC|DESC]]...)

DROP TABLE TabName

DROP INDEX IndexName

TabName, ColName und IndexName sind SQL-Bezeichner. ConstExpr ist ein konstanter Ausdruck, der einen Standardwert für eine Spalte vorgibt. DataType gibt den Datentyp der Spalte an, die gebräuchlichsten von ihnen können Tabelle 44.4 entnommen werden.

Bezeichnung Bedeutung
CHAR(n) Zeichenkette der (festen) Länge n
VARCHAR(n) Zeichenkette variabler Länge mit max. n Zeichen
SMALLINT 16-Bit-Ganzzahl mit Vorzeichen
INTEGER 32-Bit-Ganzzahl mit Vorzeichen
REAL Fließkommazahl mit etwa 7 signifikanten Stellen
FLOAT Fließkommazahl mit etwa 15 signifikanten Stellen; auch als DOUBLE oder DOUBLE PRECISION bezeichnet
DECIMAL(n,m) Festkommazahl mit n Stellen, davon m Nachkommastellen; ähnlich NUMERIC
DATE Datum (evtl. mit Uhrzeit). Verwandte Typen sind TIME und TIMESTAMP.

Tabelle 44.4: SQL-Datentypen

Ändern von Daten

Ein neuer Datensatz kann mit INSERT INTO angelegt werden. Soll ein bestehender Datensatz geändert werden, ist dazu UPDATE zu verwenden. Mit DELETE FROM kann er gelöscht werden.

INSERT INTO TabName
  [( ColName [,ColName] )]
  VALUES (Expr [,Expr]...)

UPDATE TabName
  SET ColName = {Expr|NULL}
    [,ColName = {Expr|NULL}]...
  [WHERE SearchCond]

DELETE FROM TabName
  [WHERE SearchCond]

TabName und ColName sind die Bezeichner der gewünschten Tabelle bzw. Spalte. Expr kann eine literale Konstante oder ein passender Ausdruck sein. SearchCond ist eine Suchbedingung, mit der angegeben wird, auf welche Sätze die UPDATE- oder DELETE FROM-Anweisung angewendet werden soll. Wird sie ausgelassen, wirken die Änderungen auf alle Sätze. Wir kommen im nächsten Abschnitt auf die Syntax der Suchbedingung zurück. Wird bei der INSERT INTO-Anweisung die optionale Feldliste ausgelassen, müssen Ausdrücke für alle Felder angegeben werden.

Lesen von Daten

Das Lesen von Daten erfolgt mit der SELECT-Anweisung. Ihre festen Bestandteile sind die Liste der Spalten ColList und die Liste der Tabellen, die in der Abfrage verwendet werden sollen. Daneben gibt es eine Reihe von optionalen Bestandteilen:

SELECT [ALL|DISTINCT] ColList
  FROM  TabName [,TabName]...
  [WHERE  SearchCond]
  [GROUP BY ColName [,ColName]...]
  [HAVING SearchCond]
  [UNION SubQuery]
  [ORDER BY ColName [ASC|DESC]
            [,ColName [ASC|DESC]]...]

Die Spaltenliste kann entweder einzelne Felder aufzählen oder es können durch Angabe eines Sternchens »*« alle Spalten angegeben werden. Wurde mehr als eine Tabelle angegeben und sind die Spaltennamen nicht eindeutig, kann ein Spaltenname durch Voranstellen des Tabellennamens und eines Punkts qualifiziert werden. Zusätzlich können die Spaltennamen mit dem Schlüsselwort »AS« ein (möglicherweise handlicheres) Synonym erhalten. Die Syntax von ColList ist:

ColExpr [AS ResultName]
[,ColExpr AS ResultName]]...

Zusätzlich gibt es einige numerische Aggregatfunktionen, mit denen der Wert der als Argument angegebenen Spalte über alle Sätze der Ergebnismenge kumuliert werden kann:

Bezeichnung Bedeutung
COUNT Anzahl der Sätze
AVG Durchschnitt
SUM Summe
MIN Kleinster Wert
MAX Größter Wert

Tabelle 44.5: SQL-Aggregatfunktionen

Die WHERE-Klausel definiert die Suchbedingung. Wurde sie nicht angegeben, liefert die Anweisung alle vorhandenen Sätze. Der Suchausdruck SearchCond kann sehr unterschiedliche Formen annehmen. Zunächst kann eine Spalte mit Hilfe der relationalen Operatoren <, <=, >, >=, = und <> mit einer anderen Spalte oder einem Ausdruck verglichen werden. Die Teilausdrücke können mit den logischen Operatoren AND, OR und NOT verknüpft werden, die Auswertungsreihenfolge kann in der üblichen Weise durch Klammerung gesteuert werden.

Mit Hilfe des Schlüsselworts LIKE kann eine Ähnlichkeitssuche durchgeführt werden:

Expr LIKE Pattern

Mit Hilfe der Wildcards »%« und »_« können auch unscharf definierte Begriffe gesucht werden. Jedes Vorkommen von »%« passt auf eine beliebige Anzahl beliebiger Zeichen, jedes »_« steht für genau ein beliebiges Zeichen. Manche Datenbanken unterscheiden zwischen Groß- und Kleinschreibung, andere nicht.

Mit Hilfe der Klauseln IS NULL und IS NOT NULL kann getestet werden, ob der Inhalt einer Spalte den Wert NULL enthält oder nicht:

ColName IS [NOT] NULL

Mit dem BETWEEN-Operator kann bequem festgestellt werden, ob ein Ausdruck innerhalb eines vorgegebenen Wertebereichs liegt oder nicht:

Expr BETWEEN Expr AND Expr

Neben den einfachen Abfragen gibt es eine Reihe von Abfragen, die mit Subqueries (Unterabfragen) arbeiten:

EXISTS (SubQuery)

Expr [NOT] IN (SubQuery)

Expr RelOp {ALL|ANY} (SubQuery)

Die Syntax von SubQuery entspricht der einer normalen SELECT-Anweisung. Sie definiert eine separat definierte Menge von Daten, die als Teilausdruck in einer Suchbedingung angegeben wird. Der EXISTS-Operator testet, ob die Unterabfrage mindestens ein Element enthält. Mit dem IN-Operator wird getestet, ob der angegebene Ausdruck in der Ergebnismenge enthalten ist. Die Ergebnismenge kann auch literal als komma-separierte Liste von Werten angegeben werden. Schließlich kann durch Angabe eines relationalen Operators getestet werden, ob der Ausdruck zu mindestens einem (ANY) oder allen (ALL) Sätzen der Unterabfrage in der angegebenen Beziehung steht. Bei den beiden letzten Unterabfragen sollte jeweils nur eine einzige Spalte angegeben werden.

Die GROUP BY-Klausel dient dazu, die Sätze der Ergebnismenge zu Gruppen zusammenzufassen, bei denen die Werte der angegebenen Spalten gleich sind. Sie wird typischerweise zusammen mit den oben erwähnten Aggregatfunktionen verwendet. Mit HAVING kann zusätzlich eine Bedingung angegeben werden, mit der die gruppierten Ergebnissätze »nachgefiltert« werden.

Mit dem UNION-Operator können die Ergebnismengen zweier SELECT-Anweisungen zusammengefasst werden. Das wird typischerweise gemacht, wenn die gesuchten Ergebnissätze aus mehr als einer Tabelle stammen (andernfalls könnte der OR-Operator verwendet werden).

Die ORDER BY-Klausel kann angegeben werden, um die Reihenfolge der Sätze in der Ergebnismenge festzulegen. Die Sätze werden zunächst nach der ersten angegebenen Spalte sortiert, bei Wertegleichheit nach der zweiten, der dritten usw. Mit Hilfe der Schlüsselwörter ASC und DESC kann angegeben werden, ob die Werte auf- oder absteigend sortiert werden sollen.


 Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0
 <<    <     >    >>   API  © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de