J2EE Developer`s Guide

Werbung
Mark
Marc Wutka
J2EE
ÜBERSETZUNG:
Reder Translations
Developer’s Guide
new technology
Markt+Technik Verlag
Kapitel 3
JDBC: Javas
Datenbank-API
74
3.1
Kapitel 3: JDBC: Javas Datenbank-API
Was bedeutet JDBC?
Das API Java Database Connectivity (JDBC) ist eines der wichtigsten APIs für die Entwicklung auf Unternehmensebene, da Sie dabei meist auf eine Datenbank zugreifen müssen.
Mit JDBC haben Sie ein Standard-API, das zum Großteil datenbankunabhängig ist, bei
Bedarf aber dennoch den Zugriff auf spezielle Eigenschaften Ihrer Datenbank erlaubt.
Das API JDBC besteht eigentlich aus zwei Teilen. Das Kern-JDBC (java.sql.*) wird mit
dem Standard-Java Development Kit geliefert. J2EE enthält außerdem das optionale JDBCPaket javax.sql.*, das einige Eigenschaften enthält, die in der J2EE-Entwicklung (besonders im Bereich von Enterprise JavaBeans) häufiger gebraucht werden.
Die meisten Datenbanken haben für die Kommunikation mit der Datenbank unterschiedliche
APIs. Auf einer Windows-Plattform und sogar einigen Unix-Plattformen liefert Ihnen das
API ODBC (Open Database Connectivity) ein Standard-Datenbank-API, das mit vielen verschiedenen Datenbanken funktioniert. JDBC löst dasselbe Problem wie ODBC, da es ebenfalls ein Standard-Datenbank-API liefert.
Ebenso wie ODBC weiß auch das Paket JDBC selbst nicht, wie es eine Verbindung zu
irgendeiner Datenbank herstellen soll. Es ist eine API-Architektur, die sich bei der Bereitstellung der Implementierung auf andere Pakete stützt. Unter www.oracle.com oder
www.informix.com können Sie JDBC-Treiber herunterladen, die mit Oracle- respektive
Informix-Datenbanken arbeiten. Unabhängig davon, welche Datenbank Sie verwenden, ist
es sehr wahrscheinlich, dass es für Ihre Datenbank bereits einen JDBC-Treiber gibt.
Es gibt vier Arten von JDBC-Treibern, die als Typ 1, Typ 2, Typ 3 und Typ 4 bezeichnet
werden. Sie sollten unbedingt die verschiedenen Typen kennen, bevor Sie einen Treiber
wählen. Manchmal kann die Wahl des Treibers Auswirkungen auf Ihren Anwendungsentwurf haben, besonders wenn Sie Java-Applets entwickeln.
Der erste Unterschied zwischen den vier Typen besteht darin, dass Typ-1- und Typ-2-Treiber
native Bibliotheken enthalten, also nicht reines Java sind. Dies bedeutet, dass es schwieriger
sein kann, einen Treiber für Ihre Hardware-Plattform zu finden und dass Sie den Treiber normalerweise nicht aus einem Java-Applet heraus verwenden können. Genauer gesagt können
Sie den Treiber nicht von einem Unsigned-Applet aus verwenden, aber ein Signed-Applet
kann den Treiber möglicherweise doch verwenden. Im Kapitel 45 werden Sie mehr über das
Signieren von Applets erfahren.
3.1.1
JDBC-Treiber vom Typ 1
Ein JDBC-Treiber vom Typ 1 verwendet eine native Bibliothek mit einem allgemeinen Interface. Dies bedeutet, dass die native Bibliothek nicht datenbankspezifisch ist. Das häufigste
Beispiel für einen Treiber vom Typ 1 ist die JDBC-ODBC-Brücke, die mit dem JDK geliefert
wird. Diese Brücke braucht nicht jede Art von Datenbank zu kennen, sondern muss nur wissen, wie das API ODBC verwendet wird.
Die Abbildung 3.1 zeigt eine typische Konfiguration eines Treibers vom Typ 1.
Was bedeutet JDBC?
75
JDBC-Ebene
JNI-Ebene
generisch native
Bibliothek
Datenbank-spezifische
native Bibliothek
Datenbank
Abbildung 3.1: Ein Treiber vom Typ 1 kommuniziert über eine native Bibliothek mit einem datenbankunabhängigen API.
Treiber vom Typ 1 verwenden zwar nativen Code, sind aber immer noch relativ langsam, da
die Daten so viele Ebenen durchlaufen müssen. ODBC benötigt z.B. noch immer einen
datenbankspezifischen Treiber, sodass die Daten einen datenbankspezifischen Treiber, den
ODBC-Treiber und den JDBC-Treiber durchlaufen müssen, ehe sie zu Ihnen kommen.
3.1.2
JDBC-Treiber vom Typ 2
Ein Treiber vom Typ 2 greift über eine native Bibliothek auf einen datenbankspezifischen
Treiber zu. Da er eine native Bibliothek verwendet, ist er häufig relativ schnell, obwohl es in
der Schnittstelle zwischen Java und dem nativen API noch immer zu einer Verlangsamung
kommt. Ebenso wie beim Treiber vom Typ 2 beschränkt auch hier die native Bibliothek Ihre
plattformübergreifenden Optionen, da Sie möglicherweise für Ihre Hardware-Plattform keinen Treiber finden können.
Die Abbildung 3.2 zeigt eine typische Konfiguration für einen Treiber vom Typ 2.
JDBC-Ebene
JNI-Ebene
Datenbank-spezifische
native Bibliothek
Datenbank
Abbildung 3.2: Ein Treiber vom Typ 2 verwendet eine datenbankspezifische native Bibliothek.
76
Kapitel 3: JDBC: Javas Datenbank-API
3.1.3
JDBC-Treiber vom Typ 3
Ein JDBC-Treiber vom Typ 3 ist reines Java und kommuniziert über ein datenbankunabhängiges Protokoll mit einem Datenbankportal. Normalerweise verwendet man einen Treiber
vom Typ 3 und ein Datenbankportal, wenn man Java-Applets entwickelt, da das Portal den
Umgang mit einigen Applet-Sicherheitsrestriktionen erleichtert. Die Abbildung 3.3 zeigt
eine typische Konfiguration eines Treibers vom Typ 3.
JDBC-Ebene
Datenbankportal
Datenbank
Abbildung 3.3: Ein Treiber vom Typ 3 kommuniziert mit einem Datenbankportal.
Wegen des Datenbankportals kann die Verwendung eines Treibers vom Typ 3 zu den langsamsten Arten gehören, auf Daten zuzugreifen. Das Portal muss die Daten aus der Datenbank
lesen und dann an Sie weitersenden. Dies verdoppelt das Volumen des Netzwerkverkehrs,
und das Netzwerk kann leicht zu einem der langsamsten Teile der Anwendung werden.
3.1.4
JDBC-Treiber vom Typ 4
Ein Treiber vom Typ 4 ist reines Java und kommuniziert direkt mit der Datenbank. In der
Frühzeit von Java, ehe es Just-In-Time(JIT)-Compiler gab, waren Treiber vom Typ 2 wegen
ihrer Schnelligkeit am beliebtesten. Heute sind Treiber vom Typ 4 am beliebtesten, da der
Treiber dank JIT eine Leistungsebene erreicht, die mit der des nativen Treibers vergleichbar
ist. Und da die Daten nicht die JNI-Ebene durchlaufen müssen (d.h., der Treiber braucht
Daten nicht in Java-Objekte zu übersetzen), sind Treiber vom Typ 4 normalerweise leistungsstärker als Treiber vom Typ 2. Außerdem funktionieren Treiber vom Typ 4 auf jeder
Java-Plattform. Natürlich sind sie datenbankspezifisch, sodass Sie für jede Datenbankplattform einen anderen Treiber benötigen. Ein Oracle-Treiber vom Typ 4 kann z.B. nicht auf
eine Informix-Datenbank zugreifen. Die Abbildung 3.4 zeigt eine typische Konfiguration
eines Treibers vom Typ 4.
JDBC-Kernkomponenten
77
Vergessen Sie nicht, dass die Wahl des Treibers nichts daran ändert, wie Sie Ihren Code
schreiben. Bei vielen Anwendungen können Sie zur Laufzeit einen JDBC-Treiber angeben.
Das API interessiert sich nicht dafür, welchen Treibertyp Sie verwenden.
JDBC-Ebene
Datenbank
Abbildung 3.4: Ein Treiber vom Typ 4 kommuniziert direkt mit der Datenbank.
3.2
JDBC-Kernkomponenten
Die Abbildung 3.5 zeigt die wichtigsten Klassen des JDBC-API und ihre Beziehungen.
Statement
Driver Manager
Driver
Connection
Prepared Statement
Result Set
Callable Statement
Abbildung 3.5: Hier sehen Sie die wesentlichen Komponenten des API JDBC.
Von den wichtigen JDBC-Komponenten ist nur DriverManager eine konkrete Java-Klasse.
Die übrigen Komponenten sind Java-Interfaces, die durch verschiedene Treiberpakete implementiert werden.
3.2.1
DriverManager
Die Klasse DriverManager verfolgt die verfügbaren JDBC-Treiber und stellt Datenbankverbindungen her. Obwohl der JDBC-Treiber selbst die Datenbankverbindung herstellt, durchläuft man normalerweise den DriverManager, um die Verbindung zu holen. Auf diese Weise
brauchen Sie sich nie um die eigentliche Treiberklasse zu kümmern.
Die Klasse DriverManager besitzt eine Reihe nützlicher statischer Methoden, von denen getConnection am häufigsten verwendet wird:
public static Connection getConnection(String url)
public static Connection getConnection(String url,
String username, String password)
78
Kapitel 3: JDBC: Javas Datenbank-API
public static Connection getConnection(String url,
Properties props)
Der Parameter url in der Methode getConnection ist eine der Schlüsseleigenschaften von
JDBC. Er gibt an, welche Datenbank verwendet wird. Die allgemeine Form des JDBC-URL
sieht folgendermaßen aus:
jdbc:drivertype:driversubtype://params
Der Abschnitt :driversubtype des URL ist optional. Sie können auch einfach einen URL der
folgenden Form haben:
jdbc:drivertype://params
Für eine ODBC-Datenbankverbindung nimmt der URL die folgende Form an:
jdbc:odbc:datasourcename
Das genaue Format des URL Ihres JDBC-Treibers können Sie der Dokumentation zu diesem
Treiber entnehmen. Es wird aber auf jeden Fall mit jdbc: beginnen.
Die Klasse DriverManager kennt alle verfügbaren JDBC-Treiber oder zumindest diejenigen,
die für Ihr Programm zur Verfügung stehen. Es gibt zwei Möglichkeiten, DriverManager auf
einen JDBC-Treiber hinzuweisen. Erstens können Sie die Treiberklasse laden. Die meisten
JDBC-Treiber registrieren sich automatisch selbst bei DriverManager, sobald ihre Klasse
geladen wird (für die Registrierung verwenden sie einen statischen Initialisierer). Mit der
folgenden Anweisung können Sie z.B. den JDBC-ODBC-Treiber laden, der mit dem JDK
geliefert wird:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Dieses Verfahren erscheint zwar etwas eigenartig, ist aber sehr weit verbreitet. Denken Sie
dran, dass die Methode Class.forName eine ClassNotFoundException auslöst, wenn sich der
Treiber nicht in Ihrem Klassenpfad befindet.
Und zweitens können Sie die verfügbaren Treiber angeben, indem Sie die Systemeigenschaft jdbc.drivers setzen. Diese Eigenschaft besteht aus einer Liste von Treiberklassennamen, die durch Punkte voneinander getrennt sind. Sie könnten z.B. die folgende
Eigenschaft einfügen, wenn Sie Ihr Programm ausführen:
java -Djdbc.drivers=sun.jdbc.odbc.JdbcOdbcDriver:
[ccc]COM.cloudscape.core.JDBCDriver MyJDBCProgram
Die Klasse DriverManager führt auch einige andere nützliche Funktionen aus. Viele Datenbankprogramme müssen Daten protokollieren – insbesondere Datenbankanweisungen. In
einem Produktionssystem protokollieren Sie zwar nur selten Datenbankanweisungen, in
einer Entwicklungsumgebung aber häufig. Mit der Methode setLogWriter in der Klasse DriverManager können Sie einen PrintWriter angeben, mit dem alle Informationen protokolliert
werden, die mit JDBC zu tun haben. Die Klasse DriverManager stellt außerdem die Methode
println, mit der Sie in das Datenbankprotokoll schreiben können, und die Methode getLogWriter bereit, die Ihnen den direkten Zugriff auf das LogWriter-Objekt ermöglicht.
JDBC-Kernkomponenten
3.2.2
79
Driver
Das Interface Driver ist vor allem für die Herstellung von Datenbankverbindungen zuständig. Die Methode connect gibt ein Connection-Objekt zurück, das eine Datenbankverbindung
repräsentiert:
public Connection connect(String url, Properties props)
3.2.3
Connection
Das Interface Connection ist das Kernstück des API JDBC. Ein Großteil der Methoden dieses
Interfaces lassen sich zu den folgenden drei Hauptkategorien zusammenfassen:
앫 Datenbankinformationen holen
앫 Datenbankanweisungen erstellen
앫 Datenbanktransaktionen verwalten
Datenbankinformationen holen
Die Methode getMetaData im Interface Connection gibt ein DatabaseMetaData-Objekt zurück,
das die Datenbank beschreibt. Sie können alle Datenbanktabellen auflisten und die Definition jeder einzelnen Tabelle prüfen. Wahrscheinlich werden Sie feststellen, dass Metadaten
in einer typischen Datenbankanwendung nicht allzu nützlich sind, aber beim Schreiben von
Datenbank-Explorer-Tools, die Informationen über die Datenbank sammeln, sind sie sehr
hilfreich.
Datenbankanweisungen erstellen
Mit Anweisungen können Sie Datenbankbefehle ausführen. Es gibt drei Arten von Anweisungen: Statement, PreparedStatement und CallableStatement. Jede davon funktioniert etwas
anders, aber am Ende führen sie immer einen Datenbankbefehl aus.
Mit den folgenden Methoden können Sie eine Anweisung des Typs Statement erstellen:
public Statement createStatement()
public Statement createStatement(int resultSetType,
int resultSetConcurrency)
Wenn Sie irgendeine Art von Anweisung erstellen, können Sie für die Ergebnismenge einen
bestimmten Typ und eine bestimmte Nebenläufigkeit angeben. Diese Werte bestimmen, wie
die Verbindung die von der Anfrage zurückgegebenen Ergebnisse behandelt. Insbesondere
können Sie mit dem Parameter resultSetType eine Ergebnismenge erzeugen, in der Sie sich
durch Scrollen vorwärts und rückwärts zwischen den einzelnen Ergebnissen bewegen können. In der Standardeinstellungen können Sie die Ergebnisse nur vorwärts durchgehen. Mit
dem Parameter resultSetConcurrency können Sie angeben, ob es möglich sein soll, die
Ergebnismenge zu aktualisieren. In der Standardeinstellung ist sie schreibgeschützt.
80
Kapitel 3: JDBC: Javas Datenbank-API
Mit den folgenden Methoden können Sie eine Anweisung des Typs PreparedStatement erstellen:
public PreparedStatement prepareStatement(String sql)
public PreparedStatement prepareStatement(String sql,
int resultSetType, int resultSetConcurrency)
Mit den folgenden Methoden können Sie eine Anweisung des Typs CallableStatement erstellen:
public CallableStatement prepareCall(String sql)
public CallableStatement prepareCall(String sql,
int resultSetType, int resultSetConcurrency)
Datenbanktransaktionen verwalten
Normalerweise ist jede Anweisung, die Sie ausführen, eine eigene Datenbanktransaktion.
Manchmal ist es aber sinnvoll, mehrere Anweisungen zu einer einzigen Transaktion zusammenzufassen. Die Klasse Connection besitzt ein auto-commit-Flag, das anzeigt, ob sie Transaktionen automatisch durchführen soll oder nicht. Wenn Sie eigene Transaktionen definieren
möchten, setzen Sie den folgenden Aufruf ab:
public void setAutoCommit(boolean autoCommitFlag)
Sobald Sie auto-commit deaktiviert haben, können Sie mit der Ausführung eigener Anweisungen beginnen. Wenn Sie das Ende Ihrer Transaktion erreicht haben, rufen Sie die
Methode commit auf, um die Transaktion abzuschicken (sie fertig zu stellen):
public void commit()
Wenn Sie die Transaktion doch nicht fertig stellen möchten, rufen Sie die Methode rollback
auf, um die Änderungen rückgängig zu machen, die Sie in der aktuellen Transaktion vorgenommen haben:
public void rollback()
Wenn Sie mit einer Verbindung fertig sind, rufen Sie die Methode close auf, um sicherzustellen, dass die Verbindung geschlossen wird:
public void close()
3.2.4
Statement
Wie Sie bereits wissen, gibt es drei Arten von JDBC-Anweisungen: Statement, PreparedStatement und CallableStatement.
Das Interface Statement definiert Methoden, mit denen Sie eine SQL-Anweisung ausführen
können, die in einer Zeichenkette enthalten ist. Die Methode executeQuery führt eine SQLZeichenkette aus und gibt ein ResultSet-Objekt zurück, während die Methode executeUpdate
eine SQL-Zeichenkette ausführt und die Anzahl der Zeilen zurückgibt, die von der Anweisung aktualisiert wurden:
JDBC-Kernkomponenten
81
ResultSet executeQuery(String sqlQuery)
Normalerweise verwenden Sie executeQuery, wenn Sie die SQL-Anweisung SELECT ausführen,
und executeUpdate, wenn Sie die SQL-Anweisungen UPDATE, INSERT oder DELETE ausführen:
int executeUpdate(String sqlUpdate)
Vergessen Sie nicht, die Anweisung zu schließen, sobald Sie damit fertig sind. Anderenfalls
könnten Ihnen bald die verfügbaren Anweisungen ausgehen. Eine Datenbankverbindung
erlaubt normalerweise eine bestimmte Anzahl geöffneter Anweisungen. Wenn der Speicherbereiniger die alten Verbindungen, die Sie nicht mehr verwenden, nicht gelöscht hat, kann es
geschehen, dass Sie die Anzahl der maximal zulässigen Anweisungen überschreiten. Um
eine Anweisung zu schließen, rufen Sie einfach die Methode close dieser Anweisung auf:
public void close()
Hinweis
Wenn Sie eine Connection schließen, schließt diese automatisch alle offenen Anweisungen
und Ergebnismengen.
Obwohl das Interface Statement viele Methoden für den Zugriff auf Ergebnisse und das Setzen verschiedener Parameter enthält, werden Sie wahrscheinlich feststellen, dass Sie in den
meisten Anwendungen nur die beiden execute-Methoden und die Methode close verwenden.
Obwohl das Interface Statement das einfachste der drei Anweisungs-Interfaces ist, kann es
beim Programmieren häufig Probleme bereiten. Meist führen Sie nicht genau dieselbe
Anweisung aus, sondern erstellen auf der Grundlage der Daten, die Sie suchen oder ändern
möchten, eine Anweisung. Anders ausgedrückt: Sie suchen nicht jedes Mal, wenn Sie eine
Anfrage absetzen, nach allen Personen mit dem Nachnamen Smith. Sie suchen einfach nach
allen Personen mit einem bestimmten Nachnamen.
Wenn Sie einfach das Interface Statement verwenden, sieht Ihre Anfrage häufig folgendermaßen aus:
ResultSet results = stmt.executeQuery(
"select * from Person where last_name = '"+
lastNameParam+"'");
Dieser Code sieht etwas unschön aus, da die einzelnen Anführungszeichen für die SQL-Zeichenkette innerhalb der doppelten Anführungszeichen für die Java-Zeichenkette stehen. Was
geschieht nun, wenn lastNameParam O'Brien ist? Ihre SQL-Zeichenkette sieht dann folgendermaßen aus:
select * from Person where last_name = 'O'Brien'
Die Datenbank gibt eine Fehlermeldung aus, da Sie in O'Brien eigentlich zwei Anführungszeichen benötigen, also O''Brien. Manchmal schreibt man eine Routine namens escapeQuotes, die nach allen einzelnen Anführungszeichen in einer Zeichenkette sucht und sie durch
zwei einzelne Anführungszeichen ersetzt. Damit sieht der Aufruf von executeQuery dann folgendermaßen aus:
82
Kapitel 3: JDBC: Javas Datenbank-API
ResultSet results = stmt.executeQuery(escapeQuotes(
"select * from Person where last_name = '"+
lastNameParam+"'"));
Dies können Sie mit einer vorbereiteten Anweisung wesentlich besser behandeln. Eine PreparedStatement ist eine SQL-Anweisung mit Parametern, die Sie jederzeit ändern können.
Mit dem folgenden Aufruf können Sie z.B. eine vorbereitete Anweisung für die Suche nach
dem Nachnamen erstellen:
PreparedStatement pstmt = myConnection.prepareStatement(
"select * from Person where last_name = ?");
Wenn Sie jetzt die Anfrage durchführen, speichern Sie mit einer der set-Methoden im Interface PreparedStatement den Wert für den Parameter (das ? in der Anfragezeichenkette). In
einer vorbereiteten Anweisung kann es mehr als einen Parameter geben. Mit den folgenden
Aufrufen können Sie nun alle Personen abfragen, deren Nachname O'Brien lautet:
pstmt.setString(1, "O'Brien");
ResultSet results = pstmt.executeQuery();
Das Interface PreparedStatement hat für die meisten Java-Datentypen set-Methoden und
ermöglicht es Ihnen, mit verschiedenen Datenströmen BLOBs und CLOBs zu speichern. Der erste
Parameter ist bei jeder set-Methode der Parameter number. Der erste Parameter ist immer 1
(nicht 0, wie es bei Arrays und anderen Datenfolgen der Fall ist). Um einen NULL-Wert zu speichern, müssen Sie den Datentyp der Spalte angeben, die Sie auf NULL setzen möchten. Mit dem
folgenden Aufruf können Sie z.B. einen ganzzahligen Wert auf NULL setzen:
pstmt.setNull(1, Types.INTEGER);
Die Klasse Types enthält Konstanten, die die verschiedenen von JDBC unterstützten SQLDatentypen darstellen.
Vorbereitete Anweisungen sind wiederverwendbar. Nachdem Sie die Anweisung ausgeführt
haben, können Sie sie erneut ausführen. Oder Sie können zuerst einige der Parameterwerte
ändern und sie dann wieder ausführen. Einige Anwendungen erstellen im Voraus bereits
eigene vorbereitete Anweisungen, obwohl viele sie noch immer erst bei Bedarf erstellen.
Normalerweise können Sie Anweisungen nur dann im Voraus erstellen, wenn Sie eine einzige Datenbankverbindung verwenden, da Anweisungen immer mit einer bestimmten Verbindung assoziiert sind. Wenn Sie einen Pool von Datenbankverbindungen haben, müssen
Sie die vorbereitete Anweisung dann erstellen, wenn Sie sie tatsächlich benötigen, da Sie
möglicherweise jedes Mal eine andere Datenbankverbindung aus dem Pool erhalten.
Mit dem Interface CallableStatement greift man auf gespeicherte SQL-Prozeduren zurück,
d.h. auf SQL-Code, der in der Datenbank gespeichert ist. Mit dem Interface CallableStatement können Sie gespeicherte Prozeduren aufrufen und von einer gespeicherten Prozedur
zurückgegebene Ergebnisse abrufen. Mit gespeicherten Prozeduren können Sie Anfragen
schreiben, die sehr schnell laufen und einfach aufzurufen sind. Häufig ist es einfacher, eine
Anwendung zu aktualisieren, indem man einige gespeicherte Prozeduren ändert. Der Nach-
JDBC-Kernkomponenten
83
teil besteht allerdings darin, dass jede Datenbank eine andere Syntax für gespeicherte Prozeduren hat. Wenn Sie also von einer Datenbank zu einer anderen wechseln, müssen Sie alle
gespeicherten Prozeduren neu schreiben.
Tipp
Für eine Oracle-Datenbank können Sie gespeicherte Prozeduren in Java schreiben! Sie
brauchen keine neue Programmiersprache zu lernen, um für Oracle gespeicherte Prozeduren schreiben zu können.
JDBC hat eine Standardsyntax für die Ausführung gespeicherter Prozeduren, die eine der
folgenden beiden Formen hat:
{ call procedurename param1, param2, param3 … }
{ ?= call procedurename param1, param2, param3 … }
Die Parameter sind optional. Wenn Ihre Prozedur keine Parameter entgegennimmt, kann der
Aufruf auch folgendermaßen aussehen:
{ call myprocedure}
Wenn die gespeicherte Prozedur einen Wert zurückgibt, verwenden Sie die Form, die mit ?=
beginnt. Auch für alle Parameter im Aufruf der gespeicherten Prozedur können Sie das Fragezeichen verwenden und Sie können sie ebenso setzen wie Parameter in PreparedStatement.
Tatsächlich erweitert das Interface CallableStatement das Interface PreparedStatement.
Bei einigen gespeicherten Prozeduren gibt es so genannte »out-Parameter«. Hierbei übergeben Sie einen Parameter, die Prozedur ändert den Wert des Parameters und Sie müssen den
neuen Wert prüfen. Wenn Sie den Wert eines Parameters abrufen möchten, müssen Sie dem
Interface CallableStatement dies vorab mitteilen, indem Sie registerOutParameter aufrufen:
public void registerOutParameter(int whichParameter, int sqlType)
public void registerOutParameter(int whichParameter, int sqlType,
int scale)
public void registerOutParameter(int whichParameter, int sqlType,
String typeName)
Nachdem Sie eine gespeicherte Prozedur ausgeführt haben, können Sie die Werte der outParameter abrufen, indem Sie eine der vielen get-Methoden aufrufen. Ebenso wie bei den
set-Methoden beginnt auch die Nummerierung der Parameter der get-Methoden bei 1, nicht
bei 0. Nehmen wir z.B. an, Sie haben eine gespeicherte Prozedur namens findPopularName,
die in der Tabelle Person nach dem beliebtesten Vornamen sucht. Außerdem hat die gespeicherte Prozedur einen einzigen out-Parameter, der der beliebteste Name ist. Diese Prozedur
rufen Sie nun folgendermaßen auf:
CallableStatement cstmt = myConnection.prepareCall(
"{ call findPopularName ?} ");
cstmt.registerOutParameter(1, Types.VARCHAR);
cstmt.execute();
System.out.println("The most popular name is "+
cstmt.getString(1));
84
3.2.5
Kapitel 3: JDBC: Javas Datenbank-API
ResultSet
Mit dem Interface ResultSet können Sie auf Daten zugreifen, die von einer SQL-Anfrage
zurückgegeben wurden. Am häufigsten wird das Interface ResultSet dafür verwendet, Daten
einfach zu lesen, obwohl Sie seit der JDBC-Version 2.0 auch Zeilen aktualisieren und
löschen können. Wenn Sie einfach nur Ergebnisse lesen, gehen Sie mit der Methode next zu
der nächsten Zeile und rufen die Daten dann mit einer der zahlreichen get-Methoden ab.
Hierzu ein Beispiel:
ResultSet results = stmt.executeQuery(
"select last_name, age from Person);
while (results.next())
{
String lastName = results.getString("last_name");
int age = results.getInt("age");
}
Mit jeder get-Methode im Interface ResultSet können Sie das gewünschte Element nach
dem Spaltennamen oder der Position in der Anfrage angeben. Wenn Sie z.B. nach last_name,
age fragen, befindet sich die Spalte last_name an der ersten und age an der zweiten Position.
Mit
String lastName = results.getString(1);
können Sie den Wert von last_name abrufen.
Das Abrufen eines Ergebnisses nach dem Index geht schneller als das Abrufen nach dem
Spaltennamen. Wenn Sie ein Ergebnis nach dem Spaltennamen abrufen, muss die Ergebnismenge zuerst den Index ermitteln, der mit dem Spaltennamen übereinstimmt. Der Nachteil
des Abrufens nach dem Index besteht darin, dass der Code ein wenig schwerer zu pflegen
ist. Wenn Sie die Reihenfolge der Spalten ändern oder neue Spalten einfügen, müssen Sie
daran denken, die Indizes zu aktualisieren. Wenn Sie Spaltennamen verwenden und dann
neue Spalten zu der Anfrage hinzufügen oder die Reihenfolge der Spalten ändern, brauchen
Sie vorhandenen Code nicht zu ändern. Verwenden Sie Spaltennamen, wenn dies möglich
ist, aber verwenden Sie einen Index, wenn Sie die Leistung verbessern möchten.
Tipp
Sie können die Methode findColumn in der Ergebnismenge verwenden, um den Index für
einen bestimmten Spaltennamen herauszufinden. Wenn Sie eine Anfrage haben, die eine
große Anzahl von Zeilen zurückgibt, dann sollten Sie vielleicht zuerst mit findColumn die
Indizes ermitteln und dann mit dem Index an Stelle des Spaltennamens die Werte abrufen. Ihr Programm wird dadurch schneller und gleichzeitig stehen Ihnen noch immer alle
Vorteile der Verwendung von Spaltennamen zur Verfügung.
Ein einfaches Programm für Datenbankanfragen
3.3
85
Ein einfaches Programm für
Datenbankanfragen
Das Listing 3.1 zeigt ein einfaches Programm für JDBC-Anfragen. In diesem Beispiel verwenden wir die Datenbank Cloudscape, eine reine Java-Datenbank, die Sie unter www.
cloudscape.com erhalten. Das Programm setzt die verschiedenen Konzepte zusammen, die
wir bisher in diesem Kapitel vorgestellt haben.
Beispiel
package usingj2ee.jdbc;
import java.sql.*;
public class SimpleQuery
{
public static void main(String[] args)
{
try
{
// Stelle sicher, dass DriverManager den Treiber kennt
Class.forName("COM.cloudscape.core.JDBCDriver");
// Stelle eine Datenbankverbindung her
Connection conn = DriverManager.getConnection(
"jdbc:cloudscape:j2eebook");
// Erstelle eine Anweisung
Statement stmt = conn.createStatement();
// Führe die Anfrage aus
ResultSet results = stmt.executeQuery(
"select * from person");
// Stelle die Ergebnisse in eine Schleife
while (results.next())
{
// Hole die Werte aus der Ergebnismenge.
String firstName = results.getString("first_name");
String middleName = results.getString("middle_name");
String lastName = results.getString("last_name");
int age = results.getInt("age");
// Gib die Werte aus
System.out.println(firstName+" "+middleName+" "+lastName+
"
"+age);
}
86
Kapitel 3: JDBC: Javas Datenbank-API
}
conn.close();
}
catch (Exception exc)
{
exc.printStackTrace();
}
}
Listing 3.1: Quellcode für SimplyQuery.java
Tipp
Vergessen Sie nicht, den JDBC-Treiber in den Klassenpfad zu stellen, bevor Sie das Beispiel ausführen. Wenn Sie nicht Cloudscape, sondern eine andere Datenbank verwenden,
müssen Sie außerdem sowohl den Treibernamen als auch den Datenbank-URL ändern.
3.4
Daten einfügen, aktualisieren und löschen
Nachdem Sie nun die SQL-Befehle kennen, ist es nicht mehr schwierig, die Datenbank zu
aktualisieren. Einfügen, Aktualisieren und Löschen haben im Wesentlichen dasselbe Muster.
Sie können entweder Statement oder PreparedStatement verwenden, je nachdem, ob Sie die
Daten in die SQL-Zeichenkette einfügen oder parameterisierte Daten verwenden möchten.
Das Listing 3.2 zeigt ein Programm, das eine Zeile einfügt, aktualisiert und dann löscht. Das
Beispiel verwendet die ursprüngliche Definition der Tabelle Person aus dem Kapitel 2.
Beispiel
package usingj2ee.jdbc;
import java.sql.*;
public class InsUpdDel
{
public static void main(String[] args)
{
try
{
// Stelle sicher, dass DriverManager den Treiber kennt.
Class.forName("COM.cloudscape.core.JDBCDriver");
// Stelle eine Datenbankverbindung her.
Connection conn = DriverManager.getConnection(
"jdbc:cloudscape:j2eebook");
// Erstelle eine vorbereitete Anweisung, um Daten
// einzufügen.
// Falls Sie sich über die Effizienz einer Verkettung
Daten einfügen, aktualisieren und löschen
//
//
//
//
87
von Zeichenketten zur Laufzeit wundern: Wenn Sie
einfach eine Folge konstanter Zeichenketten ohne
Variablen dazwischen haben, dann kombiniert der
Compiler diese Zeichenketten automatisch.
PreparedStatement pstmt = conn.prepareStatement(
"insert into SSN_Info (first_name, middle_name, last_name, "+
"ssn) values (?,?,?,?)");
// Speichere die Spaltenwerte für die neue
// Tabellenzeile.
pstmt.setString(1, "argle");
pstmt.setString(2, "quinton");
pstmt.setString(3, "bargle");
pstmt.setInt(4, 1234567890);
// Führe die vorbereitete Anweisung aus.
if (pstmt.executeUpdate() == 1)
{
System.out.println("Row inserted into database");
}
// Schließe die alte vorbereitete Anweisung.
pstmt.close();
// Erstelle eine neue vorbereitete Anweisung.
pstmt = conn.prepareStatement(
"update SSN_Info set ssn=ssn+1 where "+
"first_name=? and middle_name=? and last_name=?");
// Speichere die Spaltenwerte für die aktualisierte
// Zeile.
pstmt.setString(1, "argle");
pstmt.setString(2, "quinton");
pstmt.setString(3, "bargle");
if (pstmt.executeUpdate() == 1)
{
System.out.println("The entry has been updated");
}
// Schließe die alte vorbereitete Anweisung.
pstmt.close();
// Erstelle eine weitere vorbereitete Anweisung.
pstmt = conn.prepareStatement(
"delete from SSN_Info where "+
"first_name=? and middle_name=? and last_name=?");
// Speichere die Spaltenwerte für die aktualisierte
88
Kapitel 3: JDBC: Javas Datenbank-API
// Zeile.
pstmt.setString(1, "argle");
pstmt.setString(2, "quinton");
pstmt.setString(3, "bargle");
if (pstmt.executeUpdate() == 1)
{
System.out.println("The entry has been deleted");
}
}
}
conn.close();
}
catch (Exception exc)
{
exc.printStackTrace();
}
Listing 3.2: Quellcode für InsUpdDel.java
3.5
Daten aus einer Ergebnismenge aktualisieren
Es gibt eine weitere interessante Möglichkeit, Daten zu aktualisieren. Sie können Elemente
in der Ergebnismenge aktualisieren und dann die Aktualisierung wieder in die Datenbank
speichern. Das Interface ResultSet enthält Methoden für die Aktualisierung von Elementen
verschiedener Datentypen. Das Format der verschiedenen Aktualisierungmethoden gleicht
dem der get-Methoden insofern, als die update-Methoden entweder eine numerische Spaltennummer oder einen Zeichenketten-Spaltennamen entgegennehmen. Der zweite Parameter ist bei jeder update-Methode der Wert, den Sie in der Spalte speichern möchten. Mit der
folgenden Anweisung können Sie z.B. den Wert der Spalte first_name ändern:
results.updateString("first_name", "MyName");
Wenn Sie eine Anfrageanweisung erstellen, müssen Sie für die Ergebnismenge einen Typ
angeben, damit der Treiber weiß, dass Sie die Werte der Ergebnismenge aktualisieren möchten. Die drei Methoden für die Erstellung von Anweisungen – createStatement, prepareStatement und prepareCall – ermöglichen es Ihnen, für die Ergebnismenge einen Typ und
eine Nebenläufigkeit anzugeben. Der Typ kann TYPE_FORWARD_ONLY, TYPE_SCROLL_INSENSITIVE
oder TYPE_SCROLL_SENSITIVE sein. Der Typ TYPE_FORWARD_ONLY zeigt an, dass Sie sich in der
Ergebnismenge nur vorwärts bewegen und nicht zu einer früheren Ergebnismenge springen
können. Bei den beiden anderen Typen können Sie zu jeder beliebigen Position in der Ergebnismenge gehen. Die Varianten sensitive/insensitive besagen, ob die Ergebnismenge externe
Änderungen einer Zeile berücksichtigt oder nicht.
Für die Nebenläufigkeit gibt es die Optionen CONCUR_READ_ONLY und CONCUR_UPDATABLE. Wenn
Sie mit der Ergebnismenge Zeilen aktualisieren, neue Zeilen hinzufügen oder Zeilen löschen
möchten, dann müssen Sie die Nebenläufigkeit auf CONCUR_UPDATABLE setzen.
Daten aus einer Ergebnismenge aktualisieren
89
Das Listing 3.3 zeigt ein Programm, das Zeilen liest und mit der Ergebnismenge aktualisiert.
Hinweis
Im Listing 3.3 verwenden wir Oracle statt Cloudscape, um einige der neueren Eigenschaften von JDBC 2.0 zu nutzen. Sie werden feststellen, dass nicht alle Server und/oder
Treiber alle Eigenschaften von JDBC 2.0 unterstützen.
Beispiel
package usingj2ee.jdbc;
import java.sql.*;
public class UpdateResultSet
{
public static void main(String[] args)
{
try
{
// Stelle sicher, dass DriverManager den Treiber kennt.
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
// Stelle eine Datenbankverbindung her.
Connection conn = DriverManager.getConnection(
"jdbc:oracle:thin:@flamingo:1521:j2eebook",
"j2eeuser", "j2eepass");
// Erstelle eine Anweisung für das Abrufen und
// Aktualisieren von Daten.
Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
stmt.executeQuery("select * from Person");
// Führe die Anfrage aus.
ResultSet results = stmt.getResultSet();
while (results.next())
{
// Hole die Namenswerte.
String firstName = results.getString("first_name");
String lastName = results.getString("last_name");
// Ändere die Namenswerte.
results.updateString("first_name", firstName.toUpperCase());
results.updateString("last_name", lastName.toUpperCase());
90
Kapitel 3: JDBC: Javas Datenbank-API
// Aktualisiere die Zeile.
results.updateRow();
}
}
}
conn.close();
}
catch (Exception exc)
{
exc.printStackTrace();
}
Listing 3.3: Quellcode für UpdateResultSet.java
Um die aktuelle Zeile zu löschen, rufen Sie die Methode deleteRow auf:
public void deleteRow()
Um eine neue Zeile einzufügen, setzen Sie die Ergebnismenge auf eine spezielle Zeile, die
so genannte Einfügezeile, indem Sie moveToInsertRow aufrufen:
public void moveToInsertRow()
Den Inhalt der neuen Zeile ändern Sie noch immer mit den update-Methoden, aber wenn Sie
die Änderungen speichern möchten, rufen Sie insertRow auf:
public void insertRow()
3.6
Das optionale JDBC-Paket
Das JDBC-Kernpaket ist Bestandteil des Standard-Java Development Kit. Außerdem gibt es
ein zusätzliches JDBC-Paket, das zu J2EE gehört und einige der Verwendungen von JDBC
auf Unternehmensebene behandelt. Diese Erweiterungen gehören nicht zum Paket java.sql,
sondern zu javax.sql.
3.6.1
Datenquellen
Eine ärgerliche Seite von JDBC ist die Art, wie Treiber geladen werden. Es kann schwierig
sein, eine Anwendung so neu zu konfigurieren, dass eine andere Datenbank verwendet wird.
Mit dem Standard-JDBC-API müssen Sie mit einem besonderen JDBC-URL auf eine Datenbank zugreifen. Das optionale JDBC-Paket enthält für die Herstellung von Datenbankverbindungen eine Alternative zu der Klasse DriverManager.
Ein DataSource-Objekt funktioniert wie die Klasse DriverManager und hat die Methoden getConnection und getLogWriter. Der große Unterschied besteht darin, dass eine Datenquelle
normalerweise in einem Namensdienst gespeichert wird und man darauf über Javas
Namens-API (JNDI) zugreift. Der Namensdienst bietet eine größere Flexibilität: Sie brauchen den JDBC-URL nicht in Ihrer Anwendung zu codieren und nicht jede Anwendung neu
Das optionale JDBC-Paket
91
zu konfigurieren, wenn Sie den Datenbank-URL ändern. Statt dessen kann Ihre Anwendung
immer beim Namensdienst nach einem bestimmten Namen fragen. Wenn Sie Ihren Anwendungs-Server konfigurieren, können Sie die Datenbank ändern, auf die ein bestimmtes
benanntes DataSource-Objekt referiert. Sie brauchen den Namen also nicht überall zu ändern,
wo die Datenbank verwendet wird, sondern nur an einer einzigen Stelle.
3.6.2
Verbindungs-Pools
Ein Verbindungs-Pool ist eigentlich einfach eine spezielle Datenquelle, die eine Sammlung
von Datenbankverbindungen pflegt. In einer typischen Anwendung müssen Sie viele Datenbankoperationen gleichzeitig durchführen, sodass Sie normalerweise mehrere Datenbankverbindungen benötigen. Das Problem dabei ist, dass Sie nicht wissen, wann Sie eine
bestimmte Datenbankverbindung benötigen werden. Am besten ist es also, alle Verbindungen in einen gemeinsamen Pool zu platzieren und bei Bedarf eine Verbindung herauszuholen.
In der Vergangenheit haben Programmierer das Problem mit den Verbindungs-Pools gelöst,
indem sie einen eigenen Verbindungs-Pool eingerichtet haben. Eines der Probleme, auf die
man bei selbst erstellten Verbindungs-Pools häufig trifft, besteht darin, dass die Benutzer der
Verbindungen sehr höflich miteinander umgehen müssen. Wenn Sie eine Verbindung aus
dem Pool verwenden, können Sie die Verbindung nicht trennen, da andere sie noch benötigen. Sie müssen jedoch alle Ihre geöffneten Anweisungen und Ergebnismengen schließen.
Außerdem müssen Sie alle wartenden Datenbanktransaktionen entweder abschicken oder
rückgängig machen.
Verbindungs-Pools hüllen eine besondere Klasse um eine physische Datenbankverbindung.
Diese besondere Klasse verhält sich ebenso wie eine Datenbankverbindung, aber Sie können
die Verbindung trennen und brauchen sich nicht um möglicherweise wartende Transaktionen
zu kümmern. Die Hüllenklasse führt die gesamte erforderliche Bereinigung durch.
3.6.3
RowSets
Ein RowSet-Objekt gleicht einem ResultSet, da das Interface RowSet eine Erweiterung von
ResultSet ist. Das Interface RowSet enthält zusätzlich zu den get-Methoden auch set-Methoden (denken Sie daran, dass das Interface ResultSet an Stelle von set-Methoden updateMethoden verwendet). Die set-Methoden verwenden ausschließlich indizierte Namen, keine
Spaltennamen. Infolgedessen ist RowSet eine Java Bean mit les- und schreibbaren Eigenschaften. Dahinter steht der Gedanke, dass Sie ein RowSet an ein Client-Objekt zurückgeben
können, wo es verändert und an den Server zurückgesandt werden kann. Dadurch, dass RowSet eine Bean ist, können Sie es zudem mit einem GUI-Designtool verwenden.
Das API JDBC enthält keine Angaben dazu, wie man aus einem ResultSet ein RowSet enthält.
Jede Implementierung von RowSet muss eine eigene Art finden, ResultSet-Daten in ein RowSet zu kopieren. Da RowSet ein ResultSet ist, können Sie ein RowSet natürlich überall verwenden, wo Sie auch ein ResultSet verwenden können.
92
Kapitel 3: JDBC: Javas Datenbank-API
3.7
Problembehebung
3.7.1
JDBC verwenden
Warum teilt mir DriverManager mit, es gebe keinen geeigneten Treiber?
Dies geschieht normalerweise aus einem der folgenden drei Gründe:
앫 Sie haben den JDBC-URL nicht richtig geschrieben und DriverManager erkennt den
Datenbanktyp nicht.
앫 Sie haben die JDBC-Treiberklasse nicht mit Class.forName geladen und zur Systemeigenschaft jdbc.drivers hinzugefügt.
앫 Die Treiberklasse befindet sich nicht im Klassenpfad.
Mein Treiber befindet sich im Klassenpfad, warum kann ich also keine Verbindung zur
Datenbank herstellen?
Möglicherweise stimmt der Datenbank-URL nicht oder Sie verwenden den falschen Treiber.
Warum erhalte ich, sobald mein Programm einige Minuten läuft, Fehlermeldungen, die besagen, ich könne keine weiteren Datenbankanweisungen mehr zuweisen?
Wenn Sie die Statement-Objekte nicht ausdrücklich schließen, werden diese erst dann geschlossen, wenn der Speicherbereiniger sie holt, was eine ganze Weile dauern kann. Da
Datenbankanweisungen auch Ressourcen in der Datenbank oder zumindest im Treiber verbrauchen, ist die Anzahl der Anweisungen, die gleichzeitig geöffnet sein dürfen, normalerweise beschränkt. Bei einer hohen Belastung weisen Sie vielleicht mehr Anweisungen zu, als
die Datenbank behandeln kann. Der Garbage Collector versteht nur den Speichermangel, ist
aber nicht intelligent genug, um alte Anweisungen zu bereinigen, wenn die Datenbank keine
weiteren mehr behandeln kann.
Herunterladen