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.