.. .. .. .. .. Markus Zywitza Matr.-Nr. 832576 Email: [email protected] [email protected] [email protected] Markus Zywitza Ausarbeitung des Referats JDBC . . . . . . . Datenbank-Anbindung unter Java . . . JDBC 1.1 Datenbank-Anbindung unter Java Grundlegendes zu JDBC.................................................................................................................................. 3 X/Open SQL-CLI........................................................................................................................................ 3 JDBC-Versionen ......................................................................................................................................... 3 Kurzeinführung ............................................................................................................................................... 4 Beispiel ........................................................................................................................................................ 4 Erklärungen zum Beispiel .......................................................................................................................... 4 Ausnahmebehandlung..................................................................................................................................... 5 Die Ausnahme SQLException ..................................................................................................................... 5 Warnungen ................................................................................................................................................. 7 Die Ausnahme DataTruncation................................................................................................................... 7 Treiber und Treiberverwaltung........................................................................................................................ 7 Treibertypen ............................................................................................................................................... 7 Treiberregistrierung ................................................................................................................................. 10 Verbindungsaufbau ....................................................................................................................................... 12 JDBC-URL................................................................................................................................................ 12 Verbindungserstellung.............................................................................................................................. 13 Datenbankanweisungen................................................................................................................................. 13 Einfache Anweisungen.............................................................................................................................. 14 Vorkompilierte Anweisungen ................................................................................................................... 16 Prozeduraufrufe........................................................................................................................................ 17 Transaktionen................................................................................................................................................ 17 Metadaten...................................................................................................................................................... 18 Datentypen..................................................................................................................................................... 18 Anhang.......................................................................................................................................................... 19 Anmerkung: Die folgenden Schreibweisen werden im Dokument benutzt: - Kursiv für Java-Klassen und Pakete (z.Bsp. java.lang.Object) - Großbuchstaben für SQL-Schlüsselwörter (z.Bsp. CREATE) - Courier New für Beispiele und Java-Methoden und Schlüsselwörter (z.Bsp. toString()) 2 Grundlegendes zu JDBC JDBC steht für Java Database Connectivity. Es handelt sich um ein Java-API zum Datenbankanbindung. Die Vorteile von JDBC sind: - JDBC basiert auf SQL-CLI (Call Level Interface) von X/Open. Dadurch ist eine weitestgehende Datenbankunabhängigkeit gewährleistet. - Aufgrund der Eigenschaften der Sprache Java ist JDBC plattformunabhängig. Ein Programm kann ohne Änderung des Quelltextes auf verschiedene Datenbanksysteme auf unterschiedlichen Plattformen zugreifen. - JDBC unterstützt eine Mehrschichtenarchitektur. Unterstützt werden je nach Datenbanktreiber eine zweischichtige oder eine dreischichtige Architektur. Ebenso ist es möglich, Desktopdatenbanken zu benutzen. X/Open SQL-CLI Bei CLI (Call Level Interface) handelt es sich um eine Datenbankschnittstelle, die von X/Open definiert wurde. CLI ist eine Schnittstelle auf Aufrufebene. D.h. SQL-Aufrufe werden im Klartext an den entsprechenden Treiber übergeben. Dieser wandelt die Aufrufe in das native Datenbankprotokoll um. Neben JDBC ist die wohl bekannteste SQL-CLI-Implementation ODBC der Fa. Microsoft. ODBC ist mittlerweile bereits auch auf UNIX-Systemen verfügbar. JDBC-Versionen Die vorliegende Ausarbeitung behandelt JDBC in der Version 1.1. JDBC 1.0 wurde zuerst mit dem Java SDK 1.1 ausgeliefert. Für das JDK 1.0 ist JDBC gesondert erhältlich. JDBC wurde zunächst auf Basis von 1.0 ausgebaut. Diese Versionen sind mit den Nummern 1.1 bis 1.2 bezeichnet. Mit Erscheinen von Java2 (JDK 1.2) wurde JDBC 2.0 entwickelt. JDBC 2.0 wird in zwei Teile aufgeteilt: - Die Klassen und Interfaces des Pakets java.sql werden als Core API bezeichnet. Es handelt sich dabei um die Weiterentwicklung der JDBC 1.x API. Neu hinzugekommen sind beispielsweise navigierbare Ergebnistabellen und Unterstützung für CLOBs und BLOBs (Binary / Character Large Object). - Das Paket javax.sql enthält die Optional API. Dabei handelt es sich vor allem um Funktionalität zur Ermöglichung verteilter Transaktionen. Die Unterschiede zwischen den JDBC 1.x-Versionen sind marginal. Obwohl die gängigen JDKs mittlerweile alle über das JDBC 2.x verfügen, sind viele JDBC-Treiber noch auf die 1.x-API ausgelegt und unterstützen die Funktionen von JDBC 2 nicht. 3 Kurzeinführung Beispiel package example; import java.sql.*; public class Example1 { public static void main(String args[]) throws Exception { Class.forName("org.enhydra.instantdb.jdbc.idbDriver"); Connection c = DriverManager.getConnection ( "jdbc:idb:DataBaseURL"); Statement s=c.createStatement(); ResultSet r=s.executeQuery("SELECT * FROM Table"); while (r.next()) { System.out.println(r.getString("Spalte1")+" "+ r.getString("Spalte2")); } c.close(); } } Erklärungen zum Beispiel Laden des Datenbanktreibers Der Befehl Class.forName("com.package.SampleDriver") lädt, instanziiert und registriert die Instanz des Datenbanktreibers im Treibermanager. Der registrierte Datenbanktreiber wird in einem statischen Treiberregister gehalten. Auf diese spezielle Methode der Objektinitialisierung wird in dem Abschnitt über Treiber und Treiberverwaltung näher eingegangen. Verbindungserstellung Die Datenbankverbindung wird mit der statischen Methode getConnection(String url) der Klasse DriverManager erstellt. Um die Datenbank zu identifizieren, wird der Methode eine spezielle URL übergeben. Auf diese wird im entsprechenden Abschnitt noch eingegangen. Ist die Verbindung einmal erstellt, ist das Programm für die Verwaltung der Verbindung zuständig. Da die Erstellung einer Verbindung sehr zeitaufwändig, sollte sie möglichst lange im Speicher gehalten werden. Erstellen einer Datenbankanfrage Mit dem Befehl createStatement() wird ein Statement-Objekt instanziiert. Dieses kann benutzt werden, um Anfragen an die Datenbank zu erstellen. Ausführen der Anfrage Durch die Methode executeQuery(String query) wird die Anfrage ausgeführt. Die Methode liefert eine Instanz der Klasse ResultSet zurück. Dabei handelt es sich um eine temporäre Tabelle mit den Ergebnissen der Anfrage. Auswerten der Anfrage Die Auswertung der SQL-Anfrage erfolgt innerhalb einer while-Schleife. Die Methode next() von ResultSet verschiebt den Cursor der Ergebnistabelle jeweils eine Reihe nach unten. Die Rückgabe ist solange true, wie der Cursor nach der Verschiebung noch auf eine gültige Reihe zeigt. Vor dem ersten Aufruf von next() ist der Cursor noch nicht initialisiert. Ein Auslesen zu diesem Zeitpunkt liefert eine Ausnahme. 4 Beim Umgang mit ResultSet-Objekten müssen die folgenden Regeln beachtet werden: - Zu jedem Statement kann es nur ein gültiges ResultSet geben. - Auf jede Zeile kann nur einmal zugegriffen werden. Die Navigation erfolgt stets vorwärts. - Die Datenelemente können nur einmal abgerufen werden. Dies muss in der Reihenfolge geschehen, in der die Spalten in der SQL-Abfrage angegeben werden. Schließen der Verbindung Das Schließen einer Verbindung erfolgt mit close(). Dies kann auch von der VM übernommen werden, es ist allerdings nicht zu empfehlen. Durch den Aufruf von close() werden alle Resourcen geschlossen. Dies schließt noch offene Statement- und ResultSet-Objekte mit ein, wodurch weitaus mehr Speicherplatz für die tatsächliche Anwendung zur Verfügung steht. Da das erneute Öffnen einer Verbindung jedoch erhebliche Rechenzeit beansprucht (bei Desktopdatenbanken) bzw. zu Verzögerungen führt (bei Datenbanken in verteilten Umgebungen), sollte eine Verbindung nicht zu früh geschlossen werden. Daher ist der Zeitpunkt zum Schließen einer Verbindung und der damit einhergehenden Freigabe der Ressourcen je nach Anwendung zu entscheiden; eine allgemein gültige Regelung kann hier nicht getroffen werden. Ausnahmebehandlung Im oben behandelten Einführungsbeispiel wurde bewußt die schlechteste Methode der Ausnahmebehandlung gewählt – die Weitergabe an den Aufrufer, in diesem Fall die VM. Eine tatsächliche Anwendung sollte eine dezidierte Behandlung auftretender Ausnahmen implementieren. Daher sollte nicht einfach die Oberklasse Exception abgefangen werden. // Schlechtes Beispiel // Anwendungscode... try { // JDBC-Anweisungen } catch(Exception e) { e.printStackTrace(); } // Weiterer Anwendungscode... Insbesondere im Fall von JDBC ist diese Vorgehensweise nicht zu empfehlen. Dies hat zwei Gründe: - Die Ausnahme SQLException bietet mehr Informationen als Exception. - Es können Mehrfachausnahmen auftreten. Die Ausnahme SQLException Diese Ausnahme ist die am meisten benutzte Ausnahme im JDBC-Paket. Fast alle Methoden der einzelnen Klassen können SQLExceptions erzeugen. Die Anzahl der verschiedenen Ausnahmen in JDBC 1.x ist hingegen sehr gering: es existieren nur drei Klassen, die von Exception abgeleitet sind. Die wichtigste von diesen ist SQLException. Zum Vergleich: das Paket java.io enthält 16 verschiedene Ausnahmen (Stand: jdk 1.3). 5 Informationen über SQL-Fehler Aufgrund der geringen Mengen an Ausnahmeklassen und der Vielzahl an Fehlermöglichkeiten beim Umgang mit Datenbanken enthält SQLException mehr Informationen als die Oberklasse Exception: - Die Fehlernachricht (analog zu Exception) - Die SQL-Fehlernummer (nach ANSI-92) - Den Fehlercode des Datenbankherstellers Der folgende Codeausschnitt zeigt, wie man diese Informationen erhält: // Beispiel 3 // JDBC-Ausnahmebehandlung try { // JDBC-Anweisungen } catch(SQLException e) { System.err.println("Datenbank -Fehler:"); System.err.println(e.getMessage()); System.err.println("SQL -Fehlerzustand:"+e.getSQLState()); System.err.println("Hersteller -Code:"+e.getErrorCode()); } Mehrfachausnahmen Da vollständige SQL-Anweisungen übergeben werden, kann es zu Mehrfachausnahmen kommen. Als Beispiel sei folgende Situation gegeben: Eine fehlerhafte Datenbankanfrage wird gestellt. Einerseits liefert ein inneres Select keine Daten; außerdem existiert die entsprechende Tabelle des äußeren Selects nicht. Das Datenbanksystem erkennt beide Fehler erst bei der Ausführung der Anweisung und gibt zwei Fehler zurück – den SQL-State 02000 (No Data) und den SQL-State 51000 (Invalid SQL Descriptor Name). Da jedoch nur eine Ausnahme geworfen werden kann, muß es eine Möglichkeiten geben, mehrere Ausnahmen zu behandeln. Daher werden mehrere Instanzen von SQLException erzeugt. Weitere Ausnahmen werden in Form einer einfach verketten Liste an das zuerst erzeugte Objekt angehängt. Dieses wird schließlich ausgeworfen. Das folgende Beispiel zeigt die komplette Fehlerbehandlung mit JDBC: // Beispiel 4 // JDBC-Ausnahmebehandlung try { // JDBC-Anweisungen } catch(SQLException e) { while (e!=null) { System.err.println("Datenbank-Fehler:"); System.err.println(e.getMessage()); System.err.println("SQL -Fehlerzustand:"+e.getSQLState()); System.err.println("Hersteller -Code:"+e.getErrorCode()); e=e.getNextException(); } } 6 Warnungen SQL-Datenbanken kennen auch das Konzept der Warnmeldungen. Dies geschieht immer, wenn eine Ausnahme auftritt, die nicht schwerwiegend genug ist, um den Programmablauf zu unterbrechen. Hierbei handelt es sich um die SQL-States 01xxx. Dies wird durch die Klasse SQLWarning realisiert. Es handelt sich um eine Unterklasse von SQLException, die jedoch nicht geworfen wird. Statt dessen wird eine Warnung an das die Warnung erzeugende Objekt angehängt. Diese kann mit der Methode public SQLWarning getWarnings() abgefragt werden. Eine SQLWarning kann an Instanzen der folgenden Klassen angehängt werden: - java.sql.Connection - java.sql.Statement - java.sql.ResultSet Alle diese Klassen verfügen über die Methode getWarnings(), um Warnungen abzufragen. Das folgende Beispiel zeigt die Behandlung von Warnungen: // Beispiel 5 // JDBC-Warnungen public void printWarnings(SQLWarning w) { while (w!=null) { System.err.println("Datenbank -Warnung:"); System.err.println(w.getMessage()); System.err.println("SQL -Fehlerzustand:"+w.getSQLState()); System.err.println("Hersteller-Code:"+w.getErrorCode()); w=w.getNextWarning(); } } Diese Methode kann nun benutzt werden, um Warnungen auszugeben. Die Ausnahme DataTruncation Die letzte Ausnahmeklasse behandelt den Sonderfall, dass Daten abgeschnitten werden. Geschieht dies beim Lesen eines Datensatzes, wird DataTruncation als Warnung behandelt. Kommt dies beim Schreiben von Datensätzen vor, wird hingegen eine Ausnahme erzeugt und geworfen. DataTruncation verfügt über weitere Methoden, um die genaue Position des Fehlers zu bestimmen. Treiber und Treiberverwaltung Treibertypen JDBC-Treiber werden in vier Klassen eingeteilt: - JDBC-ODBC Brücke - Hybrid-Treiber (Java-C/C++) - Middleware-Treiber - Java-Treiber 7 Typ 1: JDBC-ODBC-Brücke Hierbei handelt es sich um eine Implementation von Sun. JDBC-ODBC Treiber kommen vor allem im Windows-Umfeld zum Einsatz. Perfomantere Treiber dieses Typs sind kommerziell erhältlich. Außerhalb des Windowsumfelds sind JDBC-ODBC-Treiber praktisch bedeutungslos. Es handelt sich um die langsamste Methode, Datenbanken unter Java anzusprechen. Java-Programm Datenbank ODBC-Treiber JDBC ODBC JDBC-Treiber Typ 2: Hybrid-Treiber Treiber diesen Typs werden unter Benutzung von JNI (Java Native Interface) implementiert. Diese Treiber setzen die JDBC-Aufrufe in die native API des Datenbankherstellers um. Diese Treiber sind vor allem im Unix-Umfeld verbreitet. Java-Programm Datenbank JDBC-Treiber JDBC 8 Nativer Treiber Typ 3: Middleware-Treiber Diese Treiber sprechen eine Middleware an, die den Zugriff auf die Datenbanken vermittelt. Treiber diesen Typs sind meist komplett in Java implementiert. Dies bietet Vorteile, da die Treiberdateien bspw. von Applets geladen werden können. Ein weiterer Vorteil von Treibern diesen Typs ist, dass eine kommerzielle Middleware zumeist mehrere Datenbanken in heterogenen Umgebungen ansprechen kann. Java-Programm Datenbank Datenbank Datenbank JDBC Middleware JDBC-Treiber Typ 4: Java Treiber Treiber diesen Typs sind komplett in Java implementiert. Diese Treiber stehen zur Verfügung, wenn die entsprechende Datenbank selbst in Java implementiert ist oder Java-Aufrufe selbstständig verarbeiten kann. Diese Treiber haben den Vorteil, dass sie zumeist die Möglichkeiten von JDBC komplett ausnutzen. Des weiteren bieten in Java implementierte Datenbanken z.T. auch die Möglichkeit, Java Objekte abzuspeichern. Java-Programm Datenbank JDBC JDBC-Treiber 9 Treiberregistrierung Die ausgewählten Datenbanktreiber müssen vor der ersten Benutzung initialisiert werden. Hierfür gibt es drei Möglichkeiten. - Die Klassen des Treibers können direkt initialisiert werden. - Es werden Interfaces benutzt und nur die Treiberklasse direkt initialisiert. - Es wird der Registrierungsmechanismus der Klasse DriverManager verwendet. Direkte Initialisierung Die Treiberklassen können direkt initialisiert werden, wie im folgenden Beispiel geziegt wird: package example; import java.sql.*; import org.enhydra.instantdb.jdbc.*; public class Example6 { public static void main(String args[]) throws Exception { idbDriver dr=new idbDriver(); // Erstellen der Datenbankverbindung idbConnection c = (idbConnection) dr.connect( "jdbc:idb:DataBaseURL ",null); idbStatement s= (idbStatement) c.createStatement(); idbResultSet r= (idbResultSet) s.executeQuery("SELECT * FROM Table"); while (r.next()) { System.out.println(r.getString("Spalte1")+" "+ r.getString("Spalte2")); } c.close(); } } Durch eine solche Vorgehensweise wird die Datenbankunabhängigkeit von JDBC untergraben. Da diese weitestgehend durch die Benutzung der Interfaces ermöglicht wird, müssen zusätzlich bei jedem Befehl Typkonvertierungen vorgenommen werden. Dennoch hat die Vorgehensweise in bestimmten Fälle seine Berechtigung: Beispielsweise können JDBC-Treiber herstellerabhängige Erweiterungen enthalten. Als Beispiele seien hier Oracle- und instantDB-Treiber genannt. 10 Nutzung der Interfaces Ein besserer Ansatz ist jedoch zumeist, direkt mit den entsprechenden Interfaces zu arbeiten. Das vorherige Beispiel sieht dann folgendermaßen aus: package example; import java.sql.*; public class Example7 { public static void main(String args[]) throws Exception { Driver dr=new org.enhydra.instantdb.jdbc.idbDriver(); Connection c = dr.connect( "jdbc:idb:DataBaseURL",null); Statement s= c.createStatement(); ResultSet r= s.executeQuery("SELECT * FROM Table"); while (r.next()) { System.out.println(r.getString("Spalte1")+" "+ r.getString("Spalte2")); } c.close(); } } Dadurch entfällt zunächst die Notwendigkeit zur Typumwandlung. Wichtiger ist jedoch, dass der Quellcode nun weitestgehend unabhängig vom verwendeten Treiber ist. Allerdings muss jedoch noch immer die Treiberklasse im Quellcode angegeben werden. Die Benutzung der automatischen Registrierung der Klasse DriverManager setzt an diesem Problem an. Benutzung von DriverManager Statt die Treiberklasse mit new zu initialisieren, verfolgt JDBC einen anderen Ansatz: mit dem Befehl Class.forName(“org.enhydra.instantdb.jdbc.idbDriver“); wird die Klasse dynamisch geladen. Der obige Befehl initialisiert jedoch eigentlich nicht die Klasse, sondern weist lediglich den Klassenlader an, die Klasse in den Speicher zu laden. Damit der Treiber nun initialisiert und beim Treibermanager registriert wird, muss der Treiber aussehen wie folgt: package example; import java.sql.*; public class TreiberBeispiel implements Driver { static { try { new TreiberBeispiel(); } catch(SQLException e) { //... } } public TreiberBeispiel(){ DriverManager.registerDriver(this); //... } //... } 11 Bei Benutzung der Registrierung sieht der vollständige Code nun aus wie in Beispiel 1: package example; import java.sql.*; public class Example1 { public static void main(String args[]) throws Exception { Class.forName("org.enhydra.instantdb.jdbc.idbDriver"); Connection c = DriverManager.getConnection ( "jdbc:idb:DataBaseURL"); Statement s=c.createStatement(); ResultSet r=s.executeQuery("SELECT * FROM Table"); while (r.next()) { System.out.println(r.getString("Spalte1")+" "+ r.getString("Spalte2")); } c.close(); } } Es fällt auf, dass die datenbankspezifischen Teile nunmehr lediglich zwei Strings sind, die übergeben werden. Diese Zeichenketten können problemlos als Properties eingelesen werden. Dadurch kann ohne Neukompilierung die Datenbank gewechselt werden. Verbindungsaufbau JDBC-URL Um eine Verbindung zu einer Datenbank aufzubauen, gibt es zwei Möglichkeiten: Über die jeweilige Treiberklasse oder den DriverManager. In jedem Fall wird aber die JDBC-URL der Datenbank benötigt. Diese besteht aus drei Teilen: Protokoll, Subprotokolle und Adresse der Datenquelle Die Zusammensetzung ist: protokoll:subprotokoll:adresse Die folgenden Beispiele zeigen die verschiedenen möglichen Zusammensetzung der JDBC-URL: jdbc:odbc:BeispielDB;autocommit=true jdbc:idb:C:/dbhome/beispieldb.prp jdbc:oracle:thin:@localhost:9000:BeispielDB Hierbei sind die verschiedenen Subprotokolle fett dargestellt und die Adressen unterstrichen. Festgelegt ist lediglich das Hauptprotokoll, dass immer jdbc lauten muss. Die Subprotokolle und Adressen der Datenquellen sind abhängig vom gewählten Treiber. Die Angabe der Subprotokolle dient dem Datenbanktreiber zur Identifikation einer JDBC-URL. Dieser erkennt am gewählten Subprotokoll, ob er die gewünschte Datenbankverbindung herstellen kann. Die Adresse der Datenquelle gibt an, welche Datenbank benutzt werden soll. 12 Verbindungserstellung Verbindungen können auf zwei Wegen erstellt werden: - Direkt über die Treiberklasse - Über den Treibermanager Die letztere Methode ist hierbei vorzuziehen, bei manchen Datenbanksystemen ist jedoch die erste Methode notwendig. Wird eine Verbindung direkt über die Treiberklasse erstellt, wird dessen Methode connect() aufgerufen. Der Methode müssen die JDBC-URL und eine Objekt der Klasse java.util.Properties übergeben werden. Die Eigenschaften, die in diesem Objekt festgelegt sein müssen, sind treiberabhängig. Zumeist müssen zumindest die Eigenschaften „user“ und „password“ gesetzt werden. Dies verdeutlicht das folgende Beispiel: Driver odbcbridge=new sun.jdbc.odbc.JdbcOdbcDriver(); String url="jdbc:odbc:Beispie l"; Properties p=new Properties(); p.put("user","guest"); p.put("password","default"); Connection odbcconn=odbcbridge.connect(url,p); Diese Methode wird vor allem dann benutzt, wenn Eigenschaften an die Datenbank übergeben werden müssen, die nicht innerhalb der JDBC-URL gesetzt werden können. Vorzuziehen ist jedoch die Erstellung von Verbindungen über die Klasse DriverManager. Hierzu wird dessen statische Methode getConnection() aufgerufen. Der Methode muss die JDBCURL übergeben werden. Optional können Username und Passwort übergeben werden. String url="jdbc:odbc:Beispiel"; Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection odbcconn=DriverManager.getConnection (url,"guest","default"); Die Benutzung von DriverManager hat einen weiteren Vorteil: Es können mehrere Treiber zur selben Zeit registriert sein. In dem Fall wird connect() aller Treibern in der Reihenfolge ihrer Registrierung aufgerufen. Der Treiber erkennt am übergebenen Subprotokoll, ob er diese Verbindung aufbauen kann und gibt null zurück, wenn dem nicht der Fall ist. So können von einer Anwendung Verbindungen zu mehreren Datenbanken aufgebaut werden. Datenbankanweisungen Wurde eine Datenbankverbindung erstellt, kann sie benutzt werden, um Anweisungen (Statement) zu erstellen. Es gibt drei mögliche Arten von Anweisungen: - einfache Anweisungen (Statement) - vorkompilierte Anweisungen (PreparedStatement) - Prozeduraufrufe (CallableStatement) Die Benutzung der ersten beiden Arten werden in diesem Dokument näher erläutert. CallableStatement wird nur kurz angerissen. 13 Einfache Anweisungen Anhand von einfachen Anweisungen lassen sich gut die Grundzüge der JDBC-Anwendungsentwicklung zeigen. Einfache Anweisungen werden vor allem dann verwendet, wenn bis zur tatsächlichen Ausführung keine Aussage über die Datenbankanfrage getroffen werden kann. Ein Statement-Objekt wird erstellt, in dem die Methode createStatement() von Connection aufgerufen wird. // Erstellung der Datenbankverbindung Statement st=conn.createStatement(); Das erstellte Objekt kann nun benutzt werden, um eine Anfrage auszuführen. Das Interface Statement vereinbart hierzu drei Methoden: - boolean execute(String sql); - ResultSet executeQuery(String sql); - int executeUpdate(String sql); Ist vor Ausführung der Anweisung bekannt, dass es sich um eine Anfrage handelt, die eine Ergebnistabelle liefert (z.Bsp. eine SELECT-Anweisung), kann die Methode executeQuery() benutzt werden. Gleiches gilt für Anweisungen, die wie UPDATE oder INSERT lediglich die Anzahl der veränderten Zeilen der Tabelle zurückliefern. In diesem Fall wird executeUpdate() benutzt. Wie oben bereits erwähnt, wird Statement jedoch zumeist benutzt, um Anfragen auszuführen, über deren Natur keine Aussagen zur Kompilationszeit getroffen werden können. Hierfür ist die Methode execute() geeignet. Sie liefert einen booleschen Wert zurück. Ist dieser wahr (true), handelt es sich beim Ergebnis der Anfrage um ein ResultSet, ansonsten wird ein Integerwert bereitgestellt, der der Anzahl der veränderten Tabellenzeilen entspricht. Ein weiterer Vorteil von execute() ist, dass die Anweisung in der Lage ist, mehrere ResultSets zu liefern. Dabei handelt es sich um ein eher ungewöhnliches Verhalten, dass jedoch bei der Verarbeitung dynamischer SQL-Anweisungen in Betracht gezogen werden muß. 14 Das folgende Beispiel benutzt alle Möglichkeiten von execute(): package examples; import java.sql.*; /** * Beispiel 10. * Eine einfache SQL -Shell. */ public class Shell { public static void main(String[] args) { if (args.length==0) { System.out.println("Aufruf:"); System.out.println("java examples.Shell SQL -Anweisung"); System.exit(-1); } String sql=new String(); for (int i=0;i<args.length;i++) { sql += args[i]+" "; } try { Class.forName("org.enhydra.i nstantdb.jdbc.idbDriver"); Connection conn=DriverManager.getConnection( "jdbc:idb:C:/projekt/jdbcref/rsc/example.prp"); Statement st=conn.createStatement(); if (st.execute(sql)) { do { ResultSet rs=st.getResultSet(); int numCols=rs.getMetaData().getColumnCount(); while (rs.next()) { for (int i=1;i<=numCols;i++) { if (i!=numCols) System.out.print(rs.getString(i)+", "); else System.out.println( rs.getString(i)); } } rs.close(); } while (st.getMoreResults()); } int updateCount; while ((updateCount=st.getUpdateCount())!= -1) { System.out.println("Veraenderte Zeilen: "+updateCount); st.getMoreResults(); } st.close(); conn.close(); } catch(ClassNotFoundException cnfe) { System.err.println("Treiber nicht gefunden!"); cnfe.printStackTrace(); } catch(SQLException sqle) { System.out.println("Datenbankfehler: "); System.out.println(sqle.getSQLState()+"("+sqle.getMessage()+")"); } } } Zunächst wird eine Instanz von Statement erstellt. Die Ausführung erfolgt in der nächsten Zeile mit execute(), wobei das Ergebnis mittels if abgefragt wird. Ist dieser Wert wahr, erfolgt die Abarbeitung der übermittelten Ergebnistabellen. Dazu wird zunächst eine dieser Tabellen angefordert und verarbeitet. Die Anweisung getMoreResults() schaltet zum nächsten Ergebnis weiter und liefert einen boolschen Wert zurück, der genau dann wahr ist, wenn weitere Ergebnistabellen vorhanden sind. Das Ergebnis false bedeutet, dass entweder keine Ergebnisse mehr oder nur noch weitere Update-Zähler vorliegen. Da es Anweisungen gibt, die sowohl Ergebnistabellen als auch Update-Zähler zurück liefern, wurde kein else-Teil eingefügt. Der Update-Zähler wird in jedem Fall ausgelesen. Erst wenn dieser gleich –1 ist, liegen keinerlei weitere Ergebnisse mehr vor. 15 Ergebnisauswertung Das obige Beispiel zeigt auch die grundsätzliche Vorgehensweise zur Auswertung von Ergebnistabellen. Um die Tabelle zu initialisieren, wird die Methode next() des ResultSets aufgerufen. Diese Methode schaltet bei späteren Aufrufen je eine Reihe weiter. Die Abfrage der einzelnen Ergebnisse, erfolgt dann mit Methoden vom Typ getXXX(int spaltenNummer) bzw. getXXX(String spaltenName) . XXX steht hierbei für eine zum SQL-Typ korrespondierende Javaklasse bzw. Datentyp. Da die meisten dieser Methoden skalare Datentypen zurückgeben, sollte die Methode wasNull() von Result Set aufgerufen werden, wenn ein Erkennen des SQL-Werts null notwendig ist. Ob eine Spalte den Wert null zuläßt, kann über das ResultSet-Metadatenobjekt abgefragt werden. Auf dieses wird weiter unten eingegangen. Bei der Abfrage der Spalten ist unbedingt zu beachten, dass diese in der Reihe des Aufrufs abgefragt werden. Es ist auch nicht möglich, im ResultSet zu navigieren, Kopien des ResultSet zu erstellen. Bei mehreren ResultSets als Ergebnis einer Anfrage ist auch darauf zu achten, dass beim Öffnen eines weiteren ResultSets des gleichen Statements das Schließen des ersten ResultSet automatisch geschieht. Vorkompilierte Anweisungen Anweisungen können auch vorkompiliert werden, um eine bessere Ausführungsgeschwindigkeit zu erreichen. Allerdings entsteht dadurch ein erhöhter Aufwand bei der Initialisierung. Daneben ist es möglich, Datenfelder nach der Kompilierung dynamisch zu besetzen und so paramtrisierbare Anweisungen zu schaffen. Durch diese Eigenschaften ist der Einsatz von PreparedStatements vor allem bei Webapplikationen sinnvoll. Hier werden vor allem statische Anweisungen mit variablen Daten benutzt. Beispiele sind sogenannte Einkaufskörbe und komplexe Abfragen. Die Benutzung von vorkompilierten Anweisungen entspricht der von einfachen Anweisungen. Unterschiede werden im folgenden dargestellt. Vorbereiten der Anweisung Ein PreparedStatement wird folgendermaßen erstellt: PreparedStatement ps=conn.prepareStatement(sql); wobei der Parameter sql eine Anweisung (als String) ist. Dabei können Parameter verwendet werden. Diese werden in der übergebenen SQL-Anweisung als ? (Fragezeichen) übergeben. Beispiele hierfür sind: SELECT * FROM Tabelle1 WHERE ID=? INSERT INTO Tabelle1 (ID,Name,Vorname) VALUES ?,?,? nicht möglich hingegen ist es, Tabellen- oder Spaltennamen zu parametrisieren. Folgende Beispiele sind fehlerhaft: SELECT * FROM ? SELECT * FROM Tabelle1 WHERE ?<0 FALSCH FALSCH Setzen der Parameter Bevor die vorbereitete Anweisung ausgeführt werden kann, müssen die vereinbarten Parameter noch gesetzt werden. Hierfür stehen die Methoden void setXXX(int index, XXX value) zur Verfügung, wobei XXX für eine zum SQL-Typ kompatible Javaklasse steht. Zu beachten ist, dass im Gegensatz zur üblichen Zählweise in Java der Index mit eins beginnt. 16 Eine Übersicht die möglichen Javaklassen sind in der JDBC-Dokumentation zu finden. Jeder SQLDatentyp kann jedoch durch eine Stringrepräsentation ersetzt werden. Ausführen der Anweisung Die Ausführung erfolgt wie bei einfachen Anweisungen. Jedoch wird den Methoden kein Parameter übergeben. Prozeduraufrufe Wenn die Datenbank Prozeduren und Funktionen unterstützt, kann mit der Klasse CallableStatement eine solche aufgerufen werden. Bei CallableStatements können Ein- und Ausgabeparameter verwendet werden. Transaktionen JDBC unterstützt zwei Arten der Transaktionssteuerung: Auto-Commit und eine programmatische Steuerung über Methoden-Aufrufe. Alle Transaktionen sind an das jeweilige Connection-Objekt gebunden. Für die meisten Anwendungen ist eine automatische Transaktionssteuerung ausreichend. Hierbei wird ein Commit nach jeder erfolgreichen Ausführung einer SQL-Anweisung ein Commit signalisiert. Die automatische Transaktionssteuerung wird mit folgenden Methoden aktiviert: - void setAutoCommit(boolean ac) - boolean getAutoCommit() Sollen komplexe Transaktionen vorgenommen werden, die sich über mehrere SQL-Aufrufe erstrecken (z.Bsp. Auslesen, Verändern und Zurückschreiben eines Datensatzes), können Transaktionen mit folgenden Befehlen gesteuert werden: - void commit() - void rollback() Der Beginn einer Transaktion ist stets die erste Ausführung einer SQL-Anweisung im Programm bzw. nach einem commit(). Alle diese Methoden sind im Interface Connection vereinbart. Wenn Transaktionen innerhalb des Java-Programms weierexistieren (z.Bsp. aufgrund einer Interaktion mit dem Benutzer), kann dies zu Konflikten führen, weil ein Benutzer versucht auf einen Datensatz zuzugreifen, der gerade durch einen anderen Benutzer bearbeitet wird. Ob eine Sperrung des Datensatzes notwendig ist oder nicht, ist stark von der jeweiligen Applikation abhängig. Daher gibt es in JDBC die Möglichkeit, verschiedene Isolationsstufen für Transaktionen zu setzen. Diese Stufen sind im Interface Connection als Zahlkonstanten vereinbart und können mit setTransactionIsolation(int taLevel) gesetzt werden. Folgende Stufen stellt JDBC zur Verfügung: - TRANSACTION_NONE: Transaktionen werden nicht unterstützt. - TRANSACTION_READ_UNCOMMITED: Minimale Transaktionsunterstützung, bei der es erlaubt ist, nicht mit Commit gesicherte Daten zu lesen. Dies kann bei einem Rollback zu Dateninkonsistenzen führen. - TRANSACTION_READ_COMMITED: Das Lesen nicht gesicherter Daten wird von der Datenbank unterbunden. - TRANSACTION_REPEATABLE_READ: wie oben, zusätzlich wird jedoch wiederholtes Lesen unterbunden. Bsp: Zwei Transaktionen TA1 und TA2 bearbeiten den gleichen Datensatz im Zustand 0 (vor beiden TA). TA2 verändert den Datensatz und signalisiert Commit. Der Datensatz hat nun Zustand 1. TA1 liest den Datensatz erneut und erhält ihn im Zustand 0 . Die Änderung des Datensatzes wird für TA1 erst nach einem Commit sichtbar. 17 - TRANSACTION_SERIALIZABLE: wie oben, zusätzlich wird das Verbot wiederholten Lesens auch auf neu hinzugefügte Tabellenzeilen ausgedehnt. Die Datenbank behandelt gleichzeitig stattfindende Transaktionen, als ob sie nacheinander stattgefunden hätten. Beim Verändern der Isolationsstufe ist zu beachten, dass die Unterstützung für die einzelnen Stufen abhängig von Datenbank als auch von dem verwendeten Treiber sind. Metadaten Die JDBC-API enthält zwei Klassen für Metadaten von Datenbankobjekten. Dabei handelt es sich um Metadaten über die Datenbank selbst (DatabaseMetaData) und über eine zurückgegebene Ergebnistabelle (ResultSetMetaData). Hier soll vor allem auf Letztere eingegangen werden. Die Klasse DatabaseMetadata erhält man durch Aufruf der entsprechenden get-Methode des Interface Connection. Sie enthält sämtliche Daten zur unterliegenden Datenbank und deren Treiber. Kurz erwähnt sei, dass u.a. Methoden vorhanden sind, um die Unterstützung der oben erwähnten Transaktions-Isolationsstufen abzufragen. Die Klasse ResultSetMetaData hingegen ist von weit wichtigerer Bedeutung: Sie enthält vor allem Methoden, die benötigt werden, um Ergebnistabellen in korrekter Weise auslesen zu können. Man erhält das Objekt durch Aufruf der in ResultSet vereinbarten Methode getMetaData(). ResultSetMetaData verfügt über die folgenden gebräuchlichen Methoden: - int getColumnCount() liefert die Anzahl der Spalten des ResultSet zurück. - String getColumnName(int spalte) liefert den internen Namen der Spalte. - String getColumnLabel(int spalte) liefert den vorgeschlagenen Ausgabenamen der Spalte. - String zurück. - int getColumnType(int spalte) wird benutzt, um den Typ der Spalte zu bestimmen. Sämtliche Datentypen sind in der Klasse java.sql.Types aufgeführt. - boolean isCurrency() und boolean isNullable() werden benötigt, um Spalten richtig interpretieren zu können. getColumnTypeName(int spalte) liefert den Namen des SQL-Typs Datentypen Um Daten richtig weiterverarbeiten zu können, ist es meist notwendig, ein Typmapping vorzunehmen, da Datenbanken andere Datentypen verwenden als Java (und die meisten anderen Programmiersprachen). Für die Ausgabe ist eine Stringrepräsentation der meist ausreichend. Diese Umwandlung wird von allen SQL-Datentypen unterstützt. Die ganzahligen Typen TINYINT, SMALLINT, INTEGER und BIGINT werden durch die JavaDatentypen byte, short, int und long am besten umgesetzt. Für Fließkommazahlen besteht folgende Umsetzung: REAL wird zu float, FLOAT und DOUBLE zu double. Die Fixpunkttypen NUMERIC, DECIMAL und CURRENCY werden als java.math.BigDecimal dargestellt. BIT entspricht boolean, die verschiedenen BINARY-Typen byte[]. Lediglich für drei SQL-Datentypen konnte keine korrespondierende Javaklasse gefunden werden. Es handelt sich um die Typen DATE, TIME und TIMESTAMP. Die Klasse java.util.Date entspricht keinem der drei Typen; daher wurden von ihr drei weitere Klassen abgeleitet. Es handelt 18 sich um java.sql.Date, java.sql.Time und java.sql.Timestamp. Alle drei Klassen implementieren durch die Oberklasse java.util.Date die Interfaces Serializable, Cloneable und Comparable. Den verschiedenen SQL-Datentypen sind in der Klasse java.sql.Types Integerkonstanten zugeordnet. So kann ein Mapping mit Hilfe des switch-Befehls dargestellt werden. Anhang Die für dieses Dokument benutzten Quellen sind: - Wolfgang Dehnhardt: Anwendungsprogrammierung mit JDBC, Hanser Verlag 1998 - David Flanagan u.a.: Java Enterprise in a Nutshell, O’Reilly 1999 - Java-API Dokumentation von SUN (www.java.sun.com) 19