Kurzübersicht JDBC Marc Monecke [email protected] 15. Januar 2003 Zusammenfassung Über JDBC-Schnittstellen können Anwendungsprogramme auf Datenbanken zugreifen, ohne daß dabei hersteller- und datenbankspezifische Besonderheiten berücksichtigt werden müssen. In diesem Lehrmodul werden die nötigen Grundlagen von JDBC 1.0 erläutert, um Anfragen auf einer Datenbank ausführen und die Anfrageergebnisse verarbeiten zu können. Inhaltsverzeichnis 1 Einleitung und Motivation 2 2 Datenbankzugriff mit JDBC 2 3 JDBC verwenden 3.1 JDBC-Treiber laden . . . . . . . . . . . . 3.2 Verbindung zur Datenbank aufbauen . . . 3.3 SQL-Anweisung erzeugen und ausführen . 3.4 Ergebnis verarbeiten . . . . . . . . . . . . 3.4.1 Struktur des Ergebnisses ermitteln 3.4.2 Spezielle SQL-Anweisungen . . . . 3.5 Meta-Daten . . . . . . . . . . . . . . . . . 3.6 Fehler und Warnungen . . . . . . . . . . . 3.6.1 Warnungen . . . . . . . . . . . . . 3.7 Transaktionen . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 5 5 6 6 7 7 8 8 9 9 1 Einleitung und Motivation Relationale DBMS verschiedener Hersteller haben viele Gemeinsamkeiten: Den gleichen Einsatzzweck, gleiche Datenbankmodelle und (weitgehend) kompatible Anfragesprachen. Anwendungsprogramme nutzen allerdings Programmierschnittstellen, um auf die Datenbank zuzugreifen. Diese Schnittstellen unterscheiden sich bei verschiedenen DBMS, so daß bei der Entwicklung DBMS-spezifische Besonderheiten berücksichtigt und beim Wechsel des DBMS die Anwendungen angepaßt werden müssen. Die von Microsoft definierten ODBC-Schnittstellen (Open Database Connectivity) machen die Anwendung unabhängig vom verwendeten DBMS. Allerdings funktioniert dieser Ansatz nur auf einer Plattform. JDBC hingegen ist plattformunabhängig und definiert Schnittstellen, über die in Java geschriebene Programme auf eine große Zahl von SQL-Datenbanken zugreifen können. JDBC besteht aus einer kleinen Anzahl Java-Klassen, die grundlegende DBMS-Funktionen kapseln. Sie ermöglichen es, Anfragen und Änderungsoperationen auszuführen, Anfrageergebnisse zu verarbeiten und Informationen über die Konfiguration der verwendeten Datenbank zu ermitteln. Als Anfragesprache wird eine Teilmenge von SQL (normalerweise SQL-92) verwendet. Die beschriebenen Klassen sind im Paket java.sql zusammengefaßt. 2 Datenbankzugriff mit JDBC Abbildung 1 zeigt, wie eine Java-Anwendung per JDBC auf Datenbanken zugreift. Der jeweilige Treiber (driver ) implementiert die Schnittstelle zu einem liefert Statement ResultSet führt aus Connection Java−Anwendung stellt bereit DriverManager verwaltet Postgres Driver MySQL Driver ... Abbildung 1: Datenbankzugriff per JDBC bestimmten DBMS. JDBC-Treiber werden von den Datenbank-Herstellern selbst oder von Dritten angeboten. 2 Der DriverManager verwaltet die geladenen Treiber und leitet Anfragen an den jeweils richtigen weiter. Er stellt Connection-Objekte bereit, über die Anwendungen auf die Datenbank zugreifen. Statement-Objekte kapseln Anfragen oder Änderungsoperationen, die in SQL formuliert werden. Das Ergebnis einer Anfrage wird in einem ResultSet gekapselt. Über die Operationen des ResultSet kann die Anwendung auf seine Bestandteile zugreifen. 3 JDBC verwenden Um von einem Java-Programm aus auf eine JDBC-Datenbank zuzugreifen, sind folgende Schritte nötig: 1. JDBC-Treiber laden: Welcher Treiber zu verwenden ist, hängt vom verwendeten DBMS ab. Soll das DBMS gewechselt werden können, ohne das Programm zu ändern, kann der Name des Treibers per Parameter übergeben oder in einer Ressourcen-Datei gespeichert werden. 2. Verbindung zur Datenbank aufbauen: Hier wird das Connection-Objekt erzeugt, über das die Anwendung mit der Datenbank kommuniziert. 3. SQL-Statement erzeugen: Ein SQL-Statement enthält den auszuführenden SQL-Code. Neben einfachen Anweisungen können auch vorübersetzte Anweisungen verwendet werden. Über einen dritten Typ kann die Anwendung direkt auf gespeicherte Prozeduren in der Datenbank zugreifen. 4. Statement ausführen und Ergebnis verarbeiten: Das Ergebnis wird als ResultSet zurückgeliefert. Zur Ermittlung der Struktur des Ergebnisses und für den Zugriff auf seine Bestandteile stehen Operationen zur Verfügung. Die einzelnen Schritte werden im folgenden erläutert. Abbildung 2 enthält ein Beispielprogramm, das eine SQL-Anfrage als Kommandozeilen-Parameter akzeptiert und das Ergebnis ausgibt. Der Quelltext zur Aufbereitung des Ergebnisses ist in Abbildung 3 dargestellt. 3 import java.sql.*; public class JDBCDemo { final static int FIXEDWIDTH = 15; final static int COMMANDROWS = 8; final static int RESULTROWS = 25; final final final final final final static static static static static static String int String String String String private static Connection Statement ResultSet FILL = " "; DISPLAY_SIZE = 15; DRIVER_CLASS = "org.postgresql.Driver"; URL_PREFIX = "jdbc:postgresql://"; REMOTEHOST = "pi53.informatik.uni-siegen.de/dbs_1"; USERNAME = "cgi"; void execute(String con; st; result; sql_string) { try { con = establishConnection(); st = con.createStatement(); result = st.executeQuery(sql_string); showResult(result); st.close (); con.close (); } catch ( SQLException e ) { System.err.println("Error: " + e.getMessage()); } } private static Connection establishConnection() { Connection con = null; try { Class.forName(DRIVER_CLASS); } catch (ClassNotFoundException e) { System.err.println("JDBC driver " + DRIVER_CLASS + " not found!"); } try { con = DriverManager.getConnection(URL_PREFIX + REMOTEHOST, USERNAME, ""); } catch (SQLException e) { System.err.println("Error " + e.getErrorCode() + " " + e.getMessage()); } return con; } private static void showResult ( ResultSet { /* ... siehe Abbildung 3 ... */ } result ) throws SQLException public static void main(String args[]) { execute(args[0]); } } Abbildung 2: Beispielprogramm: Verbindung zur Datenbank aufbauen und SQLAnweisung ausführen 4 private static void showResult ( ResultSet { ResultSetMetaData rsm; int num_cols; int i; int row_count; result ) throws SQLException rsm = result.getMetaData(); num_cols = rsm.getColumnCount(); row_count = 0; for(i = 1; i <= num_cols; i++) printColumn(rsm.getColumnName(i)); System.out.println (); for(i = 1; i <= num_cols; i++) printColumn("=========="); System.out.println (); while ( result.next() ) { for(i = 1; i <= num_cols; i++) printColumn(result.getString(i)); System.out.println (); row_count++; } System.out.println("----------------------------"); + System.out.println(row_count + " rows retrieved"); } private static void printColumn(String s) { if(s.length() <= DISPLAY_SIZE ) s += FILL.substring(0, DISPLAY_SIZE-s.length()); else s = s.substring(0, DISPLAY_SIZE-1); System.out.print(s + " "); } Abbildung 3: Beispielprogramm: Anfrageergebnis verarbeiten 3.1 JDBC-Treiber laden Der zu ladende Datenbank-Treiber wird über seinen Paket- und Klassennamen identifiziert. In Abbildung 2 ist dies der Treiber für PostgreSQL: org.postgresql.Driver Damit der Treiber geladen werden kann, muß natürlich der Klassenpfad passend gesetzt sein. Es können auch mehrere Treiber geladen werden. Beim Aufbau der Verbindung zur Datenbank wird dann geprüft, welcher Treiber benutzt werden soll. 3.2 Verbindung zur Datenbank aufbauen Die Datenbank wird durch eine JDBC-URL identifiziert, die grundsätzlich die Form jdbc: driver : databasename hat. Die URL kann aber datenbankspezifisch variieren. Die Operation getConnection der Klasse DriverManager akzeptiert neben der URL auch den Benutzernamen und das Paßwort als Parameter. Es gibt Varianten dieser Operation mit anderen Parametern. 5 Connection con; con = DriverManager.getConnection( "jdbc:postgresql://pi53.informatik.uni-siegen.de/dbs_1", "Meier", "geheim"); ... con.close(); Der DriverManager prüft nun nacheinander, welcher der geladenen Treiber die URL versteht und ein Connection-Objekt zurückliefert. Da diese Prüfung Zeit benötigt, sollten jeweils nur die benötigten Treiber geladen werden. Offene Verbindungen belegen Speicher und andere Datenbank-Ressourcen. Sie sollten daher mit close() geschlossen werden, sobald sie nicht mehr benötigt werden. 3.3 SQL-Anweisung erzeugen und ausführen SQL-Anweisungen werden in Objekten gekapselt. Diese Objekte müssen Operationen der Schnittstelle Statement oder einer davon abgeleiteten Schnittstelle implementieren. Anweisungen werden von der Connection erzeugt: Statement stmt = con.createStatement(); Der SQL-Code wird beim Aufruf von executeQuery() übergeben. Die Operation liefert das Ergebnis in einem ResultSet zurück. ResultSet result = stmt.executeQuery("SELECT..."); Beim Aufruf von Änderungsoperationen wie INSERT, UPDATE und DELETE wird die Operation executeUpdate() verwendet. Sie liefert die Zahl der geänderten Sätze zurück. int num = stmt.executeUpdate("DELETE..."); Ist der Typ der Anweisung nicht bekannt, etwa wenn der Benutzer die SQLAnweisung eingibt, oder wird eine Anfrage ausgeführt, die mehrere ResultSets liefert, wird die Operation execute() verwendet. Über weitere Operationen der Klasse Statement kann die Struktur des Ergebnisses ermittelt und auf seine Bestandteile zugegriffen werden. 3.4 Ergebnis verarbeiten Das Ergebnis einer SQL-Anfrage hat üblicherweise eine Tabellenform, die schlecht im Programm verarbeitet werden kann. Bei Verwendung von JDBC wird das Ergebnis in einem ResultSet gekapselt, das Operationen zur Navigation durch das Ergebnis anbietet. Die Operation next() positioniert einen Zeiger auf der jeweils nächsten Zeile des Ergebnisses. Auf die einzelnen Spalten dieser Zeile wird über getXXX()Operationen zugegriffen, denen der Spaltenname oder der Spaltenindex (beginnend bei 1) übergeben wird: 6 String name; int plz; while(result.next()) { name = result.getString("Name"); plz = result.getInt(5); ... } Nach der Verarbeitung können ResultSet und Statement jeweils mit close() geschlossen werden, um die belegten Ressourcen freizugeben. Um herauszufinden, ob eine Spalte einen (SQL-) Nullwert enthält, wird wasNull() verwendet. Allerdings muß zunächst mit einer getXXX()-Operation auf die Spalte zugegriffen werden: name = result.getString("Name"); if(!result.wasNull()) System.out.println("Name: " + name); 3.4.1 Struktur des Ergebnisses ermitteln Häufig ist die Struktur des Anfrage-Ergebnisses dem Anwendungsprogramm nicht bekannt, etwa, wenn die Anfrage vom Benutzer eingegeben wird oder das Programm mit beliebigen Datenbankschemata arbeiten soll. Für diese Fälle liefert die Operation getMetaData() Informationen über die Struktur eines ResultSet zurück: ResultSetMetaData rsm; rsm = result.getMetaData(); for(int i = 0; i < rsm.getColumnCount(); i++) { System.out.println("Spalte " + rsm.getColumnName(i) + ", Typ: " + rsm.getColumnTypeName(i)); } 3.4.2 Spezielle SQL-Anweisungen Neben dem einfachen SQL-Statement gibt es zwei weitere Formen: – Ein PreparedStatement ist eine vorübersetzte Anweisung. Sie wird bei SQLAnfragen mit Parametern verwendet. In der Anfrage werden die Parameter durch den Platzhalter ? dargestellt. Sie können einzelnen mit set-Operationen gesetzt werden (z.B. setInt bei einem integer -Wert). Die Position wird als erster Parameter der set-Operation übergeben. PreparedStatement pstmt = con.prepareStatement( "INSERT INTO Kunden (Name, Plz) VALUES(?, ?)"); 7 pstmt.clearParameters(); pstmt.setString(1, "Müller-Wattenscheid"); pstmt.setInt(2, 57068); pstmt.executeUpdate(); – Mit einem CallableStatement können in der Datenbank gespeicherte Prozeduren (stored procedures) direkt ausgeführt werden. Ein CallableStatement wird mit der Operation prepareCall() erzeugt: CallableStatement cstmt = con.prepareCall(...); ... cstmt.execute(); 3.5 Meta-Daten Die Operation getMetaData von Connection liefert Informationen über die Datenbank zurück, etwa über die vorhandenen Tabellen, gespeicherten Prozeduren und die unterstützten SQL-Befehle. DatabaseMetaData db_meta_data; db_meta_data = con.getMetaData(); System.out.println("DBMS: " + db_meta_data.getDatabaseProductName()); System.out.println("JDBC-Treiber: " + db_meta_data.getDriverName()); 3.6 Fehler und Warnungen Alle Datenbankzugriffe müssen in try/catch-Blöcken gekapselt werden, um mögliche Ausnahmen behandeln zu können. Die Zugriffsoperationen werfen exceptions vom Typ SQLException. Gegenüber normalen exceptions bieten SQLExceptions folgende zusätzliche Operationen: – getErrorCode() liefert einen Hersteller-spezifischen Fehler-Code zurück. – getSQLState() liefert einen Fehlercode gemäß ANSI-92-Standard zurück. – Oft führt eine SQL-Anweisung zu mehreren Fehlern. Mehrere Fehler können einer SQLException zugeordnet und dort in einer Liste verwaltet werden. Mit getNextException() wird auf die jeweils nächste zugegriffen; setNextException() fügt eine SQLException an das Ende der Liste an. Wenn eine Datenbank-Operation fehlschlägt, muß die Verarbeitung nicht gleich abgebrochen werden. Manche Ereignisse können eher als Hinweise verstanden werden, daß etwas nicht so funktioniert hat, wie es sollte. 8 3.6.1 Warnungen In JDBC können solche Hinweise als Warnungen (warnings) vom Anwendungsprogramm abgefragt werden. Objekte der Klassen Connection, Statement und ResultSet können Warnungen liefern. Sie bieten dazu die Operation public SQLWarning getWarnings() throws SQLException an. getWarnings() liefert die erste Warnung zurück, sofern welche vorhanden sind. Die Klasse SQLWarning ist ein Subklasse von SQLException, sie bietet also die gleichen Operationen zum Abfragen der Fehlercodes an. Sind mehrere Warnungen vorhanden, können diese mit getNextWarning() abgefragt werden. Zu beachten ist dabei, daß beim Ausführen der nächsten Anweisung die Warnungen gelöscht werden – sie müssen also sofort verarbeitet werden. Der folgende Code-Ausschnitt zeigt, wie die Warnungen einer Connection und eines Statements verarbeitet werden können: Statement stmt = con.createStatement(); ResultSet result = stmt.executeQuery("SELECT..."); SQLWarning warning; while (result.next()) { String name = result.getString("Name"); System.out.println("Name: " + name); warning = stmt.getWarnings(); if (warning != null) { System.out.println("---Warning---"); while (warning != null) { System.out.println("Message: " + warning.getMessage()); //... warning = warning.getNextWarning(); } } warning = result.getWarnings(); if (warning != null) { System.out.println("---Warning---"); while (warning != null) { System.out.println("Message: " + warning.getMessage()); //... warning = warning.getNextWarning(); } } 3.7 Transaktionen JDBC enthält auch Schnittstellen zur Steuerung von Transaktionen. Das Connection-Objekt ist für die Verwaltung der Transaktionen zuständig. Wird kein spezieller Modus angegeben, wird der auto commit-Modus verwendet. Er führt alle SQL-Anweisungen als separate Transaktionen aus. Sollen mehrere Anweisungen zu einer Transaktion zusammengefaßt werden, kann der auto commit-Modus ausgeschaltet werden: con.setAutoCommit(false); stmt.executeUpdate(...); stmt.executeUpdate(...); con.commit(); Die Transaktion muß dann explizit mit commit() (oder rollback()) abgeschlossen werden – andernfalls gehen alle Änderungen verloren. 9 Eine Connection bietet verschiedene Modi zur Isolation von Transaktionen. Sie werden mit setTransactionIsolation() durch Angabe einer der folgenden Konstanten gesetzt: – TRANSACTION NONE: Transaktionen werden deaktiviert oder nicht unterstützt. – TRANSACTION READ UNCOMMITTED: Andere Transaktionen können auf geschriebene Werte auch vor dem commit der schreibenden Transaktion zugreifen – dirty reads sind also möglich. – TRANSACTION READ COMMITTED: Transaktionen können keine Werte lesen, die von einer Transaktion geschrieben wurden, die noch kein commit() ausgeführt hat – dirty reads werden verhindert. – TRANSACTION REPEATABLE READ: Dirty reads werden verhindert, eine Transaktion erhält bei aufeinanderfolgenden Lesezugriffen stets den gleichen Attributwert, auch wenn das Attribut von einer parallelen Transaktion geändert und committed wurde. – TRANSACTION SERIALIZABLE: Transaktionen werden vollständig isoliert, scheinen also nacheinander ausgeführt zu werden. Der Modus schließt die Eigenschaften des vorhergehenden ein und sichert zusätzlich, daß eine Transaktion stets die gleiche Menge von Sätzen einliest, auch wenn diese durch parallele Transaktionen geändert wurde. 10