Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 5. Auflage |
<< | < | > | >> | API | Kapitel 42 - Datenbankzugriffe mit JDBC |
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.
In Abschnitt 42.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.
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 42.5 haben wir dieses Kommando verwendet, um das Änderungsdatum der Datei in die Tabelle file zu schreiben.
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. Um so 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.
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 42.4 findet sich eine Übersicht der wichtigsten SQL-Datentypen.
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 jedesmal 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.
Ein 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 |
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.
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. |
|
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 42.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 42.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 } |
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 42.3.5 unterscheidet.
Nicht alle Datenbanken erlauben es, Tabellennamen zu parametrisieren, sondern beschränken diese Fähigkeit auf Argumente von Such- oder Änderungsausdrücken. Tatsächlich läuft unser Beispiel zwar mit InstantDB, aber beispielsweise nicht mit MS Access 95. |
|
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:
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 42.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 42.4: SQL-Datentypen
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.
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 42.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, 5. Auflage, Addison Wesley, Version 5.0.2 |
<< | < | > | >> | API | © 1998, 2007 Guido Krüger & Thomas Stark, http://www.javabuch.de |