22.5 Mit Java an eine Datenbank andocken 

Zum Aufbau einer Datenbankverbindung und zur Herstellung einer Connection gibt es zwei Möglichkeiten:
- Direkt über den DriverManager: Verbindungsdaten bestimmen und eine Verbindung aufbauen.
- Über einen zentralen Namensdienst: Im JNDI ist eine vorkonfigurierte Datenquelle (DataSource) abgelegt, die wir entnehmen und über die wir eine Verbindung aufbauen.
Im Java-Enterprise-Bereich ist das übliche Vorgehen über eine DataSource. Wir wollen uns doch zunächst mit dem DriverManager beschäftigen, bevor wir zur DataSource und JNDI kommen.
Alle verwendeten Klassen und Schnittstellen für den Datenbankteil liegen unter java.sql.*. Im Fall des Namensdienstes sind Klassen/Schnittstellen aus dem Paket javax.naming nötig.
22.5.1 Der Treiber-Manager 

Alle Datenbanktreiber werden an einer zentralen Stelle, dem Treiber-Manager, gesammelt. Die Zentrale ist in Java durch die Klasse DriverManager gegeben. Die Methoden der Klasse sind statisch, da sich ein Exemplar dieser Klasse nicht erzeugen lässt; der Konstruktor ist privat. Die wichtigste Methode des Treiber-Managers ist getConnection(), mit der wir eine Verbindung zur Datenbank aufbauen können. Es lassen sich aber auch alle angemeldeten Treiber erfragen.
22.5.2 Den Treiber laden 

Vor der Ausführung der JDBC-Befehle muss ein passender Datenbanktreiber geladen werden. Der Datenbanktreiber ist eine Java-Klasse, die beim Treiber-Manager angemeldet sein muss.
|
Vor Java 6 und nicht vorbereiteten Datenbanken ist die Treiber-Klasse von Hand einzubinden. Zwei Möglichkeiten sind populär:
- Die Property jdbc.drivers enthält den Namen des Datenbanktreibers. Auf der Kommandozeile lässt sich die Variable mit dem Schalter -D einfach setzen:
$ java -Djdbc.drivers=org.hsqldb.jdbcDriver <Javaklasse>
- Die zweite Möglichkeit bietet die Funktion Class.forName(), die eine Treiber-Klasse lädt. Sie trägt sich automatisch beim Treiber-Manager ein.
final class java.lang.Class<T>
implements Serializable, GenericDeclaration, Type, AnnotatedElement |
- static Class forName( String className ) throws ClassNotFoundException Sucht, lädt und bindet die Klasse mit dem qualifizierten Namen className ins Laufzeitsystem ein. Die Methode liefert ein Class-Objekt zurück, falls sie die Klasse laden kann, andernfalls quittiert sie einen Fehler mit einer ClassNotFoundException.
Die Programmzeilen für das manuelle Laden der Klasse org.hsqldb.jdbcDriver sind somit:
Listing 22.2 com/tutego/insel/jdbc/DriverManagerDemo.java, Ausschnitt
try { Class.forName( "org.hsqldb.jdbcDriver" ); } catch ( ClassNotFoundException e ) { // OO! Treiber konnte nicht geladen werden. e.printStackTrace(); }
Da wir die Klasse nur laden, aber die Referenz auf den Klassen-Deskriptor nicht benötigen, belassen wir es bei einem Aufruf und beachten den Rückgabewert nicht. Diese Methode löst eine ClassNotFoundException aus, falls die Klasse nicht gefunden wurde, der Treiber also nicht geladen werden konnte.
|
Datenbank | Klassenname für den JDBC-Treiber |
Adabas D |
de.sag.jdbc.adabasd.Adriver |
Borland JDataStore |
com.borland.datastore.jdbc.DataStoreDriver |
Borland Interbase |
interbase.interclient.Driver |
DB2/Derby |
com.ibm.db2.jcc.DB2Driver |
Informix |
com.informix.jdbc.IfxDriver |
IDS Server |
ids.sql.IDSDriver |
Microsoft SQL Server |
com.microsoft.jdbc.sqlserver.SQLServerDriver |
mSQL |
COM.imaginary.sql.msql.MsqlDriver |
MySQL |
com.mysql.jdbc.Driver4 |
Oracle |
oracle.jdbc.driver.OracleDriver |
Pointbase |
com.pointbase.jdbc.jdbcUniversalDriver |
PostgreSQL |
org.postgresql.Driver |
Sybase |
com.sybase.jdbc2.jdbc.SybDriver |
22.5.3 Eine Aufzählung aller Treiber 

Die statische Funktion getDrivers() der Klasse DriverManager liefert eine Aufzählung der angemeldeten Treiber. Die folgenden Zeilen geben einfach den Klassennamen aus – die Treiber implementieren nicht unbedingt eine sinnvolle toString()-Methode, sodass wir uns mit dem Klassennamen begnügen.
Listing 22.3 com/tutego/insel/jdbc/DriverManagerDemo.java, Ausschnitt
for ( Enumeration<Driver> e = DriverManager.getDrivers(); e.hasMoreElements(); ) System.out.println( e.nextElement().getClass().getName() );
Die Elemente, die durch die Enumeration ausgelesen werden, sind Treiber-Objekte vom Typ Driver. Jeder Datenbanktreiber implementiert diese Schnittstelle. Mit dem manuell geladenen Treiber org.hsqldb.jdbcDriver und dem Standard-JDBC-ODBC-Treiber verbunden ist die Ausgabe:
sun.jdbc.odbc.JdbcOdbcDriver org.hsqldb.jdbcDriver
22.5.4 Log-Informationen 

Zu Testzwecken bietet es sich an, Informationen des Treibers und der Datenbank in einen speziellen Ausgabekanal zu schreiben. Wir können die Log-Informationen so umlenken, dass sie in den Standardausgabestrom geschrieben werden. Das macht die statische Methode setLogWriter(), die einen PrintWriter als Parameter erwartet. Folgende Zeile gibt alle Informationen auf dem Bildschirm aus, die in das Logbuch geschrieben werden:
DriverManager.setLogWriter( new PrintWriter(System.out) );
Ladevorgang der Treiber protokolliert ausgeben
Eine solche Ausgabe ist hilfreich, da interessante Aussagen über die Funktionsweise der Treiber offenbart werden. Zur Set-Methode existiert die passende getLogWriter()-Methode, die den PrintWriter zurückgibt. Eine Anfrage an getLogWriter() gibt null zurück, wenn standardmäßig keine Ausgabe stattfindet; es wird keine versteckte Datei erzeugt.
Setzen wir die Ausgabe auf den Standardausgabekanal:
Listing 22.4 com/tutego/insel/jdbc/DriverManagerDemo.java, Ausschnitt
DriverManager.setLogWriter( new PrintWriter(System.out) );
Da die Treiberklasse für die JDBC-ODBC-Brücke automatisch geladen wird, aber auch unser HSQLDB-Treiber, ist die Ausgabe aus System.out:
JdbcOdbcDriver class loaded registerDriver: driver[className=sun.jdbc.odbc.JdbcOdbcDriver, sun.jdbc.odbc.JdbcOdbcDriver@8a0d5d] DriverManager.initialize: jdbc.drivers = null JDBC DriverManager initialized registerDriver: driver[className=org.hsqldb.jdbcDriver, org.hsqldb.jdbcDriver@1e4457d] sun.jdbc.odbc.JdbcOdbcDriver org.hsqldb.jdbcDriver
Die Methode setLogWriter() ist die erste Methode, die die Klasse DriverManager benutzt. Daher bekommt der Klassenlader die Aufgabe, die Klasse DriverManager zu laden. setLogWriter() speichert dann das PrintWriter-Objekt in einer privaten Variable und macht sonst nichts.
Erst das Laden eines Treibers führt zum Aufruf der statischen initialize()-Methode. Sie führt die private Methode loadInitialDrivers() aus, die zur ersten Ausgabezeile führt. Hier sind noch keine Treiber angemeldet, da in den Eigenschaften »jdbc.drivers« nichts eingetragen ist. Diese Eigenschaft wird in der Regel dann gesetzt, wenn von außen über den Schalter »-D« eine Klasse angesprochen wird. Nach dem Suchen in den Eigenschaften folgt die Ausgabe »JDBC DriverManager initialized«. Nun hat der Treiber die DriverManager-Klasse hochgefahren, und der Treiber-Manager kann den Treiber anmelden. Er ist vom Typ Driver. Dieser wird zusammen mit dem zugehörigen Class-Objekt und einem Namen in der internen Klasse DriverInfo in einem internen Vector gespeichert. Die Ausgabe »registerDriver:[...]« stammt von der Anmeldung des Treibers. Wir sehen genau die in der Klasse DriverInfo gespeicherten Informationen. Die Ausgabe wird auch von toString() von DriverInfo generiert.
Nicht nur Treiber und SQL-Klassen nutzen den Log-Stream, auch wir können Zeichenketten ausgeben. Dazu dient die statische Methode println(), die als Parameter nur einen String annimmt. println() ist so implementiert, dass bei einem nicht gesetzten Log-Stream die Ausgabe unterbleibt.
22.5.5 Verbindung zur Datenbank auf- und abbauen 

Nach dem Laden des Treibers können wir eine Verbindung zur Datenbank mit Hilfe des Connection-Objekts aufbauen, das DriverManager.getConnection() zurückgibt. Der Methode wird eine Datenbank-URL mitgegeben und optional Benutzername und Passwort.
Die Datenquelle angeben
Alle Datenquellen sind durch eine besondere URL qualifiziert, die folgendes Format besitzt:
jdbc:Subprotokoll:Datenquellenname
Die Datenbank definieren jeweils unterschiedliche Subprotokolle, und die Angabe des Servernamens ist auch immer individuell:
Datenbank | Subprotokoll | Beispiel |
Derby |
derby:net |
jdbc:derby:net://host:1527/ |
IBM DB2 |
db2 |
jdbc:db2://database |
HSQL DB |
hsqldb |
jdbc:hsqldb:file:database |
Interbase |
interbase |
jdbc:interbase://host/dabase.gdb |
MySQL |
mysql |
jdbc:mysql://host/database |
ODBC-Datenquellen |
odbc |
jdbc:odbc:database |
Oracle Thin |
oracle:thin |
jdbc:oracle:thin:@host:1243:database |
Sybase |
sybase:Tds |
jdbc:sybase:Tds:host:1234/database |
Verbindung aufnehmen
Die getConnection()-Methode liefert ein Connection-Objekt, das die Verbindung mit der Datenbank repräsentiert.
con = DriverManager.getConnection( "jdbc:hsqldb:file:TutegoDB;shutdown=true", "sa", "" ); |
Die Methode getConnection() erwartet bis zu drei Parameter: Die URL der Datenbank, zu der die Verbindung aufgenommen werden soll, ist der Pflichtparameter. Der Anmeldename und das Passwort sind optional und können auch leere Strings ("") sein, wenn eine Authentifizierung keine Rolle spielt.
Meldet getConnection() keinen Fehler, so liefert sie uns eine geöffnete Datenbankverbindung.
class java.sql.DriverManager |
- static Connection getConnection( String url ) throws SQLException Versucht, eine Verbindung zur Datenbank aufzubauen. Die Klasse DriverManager sucht dabei einen passenden Treiber aus der Liste der registrierten JDBC-Treiber für die Datenbank.
- static Connection getConnection( String url, String user, String password ) throws SQLException Versucht, eine Verbindung zur Datenbank aufzubauen. user und password werden für die Verbindung zur Datenbank verwendet.
- static Connection getConnection( String url, Properties info ) throws SQLException Versucht, eine Verbindung zur Datenbank aufzubauen. Im Properties-Objekt können die Felder »user« und »password« sowie weitere Informationen vorhanden sein.
Wie der Treiber gefunden wird
Es lohnt sich, einmal hinter die Kulissen der Methode getConnection() zu blicken. Das DriverManager-Objekt wird veranlasst, die Verbindung zu öffnen. Dabei versucht es, einen passenden Treiber aus der Liste der JDBC-Treiber auszuwählen. Sein Treiber verwaltet die Klasse DriverManager in einem privaten Objekt DriverInfo. Dieses enthält ein Treiber-Objekt (Driver), ein Objekt (securityContext) und den Klassennamen (className).
Bei getConnection() geht der DriverManager die Liste der DriverInfo-Objekte ab und versucht, sich über die connect()-Methode anzumelden. Bemerkt der Treiber, dass er mit der URL nicht viel anfangen kann, gibt er null zurück, und getConnection() versucht es mit dem nächsten Treiber. Ging alles daneben und konnte keiner der angemeldeten Treiber etwas mit dem Subprotokoll anfangen, bekommen wir eine SQLException("No suitable driver", "08001").
Verbindung beenden
Da eine Verbindung zu schließen ist (und nicht der DriverManager), finden wir eine Methode close() beim Connection-Objekt. Verbindungen zu schließen, ist immens wichtig, sodass dieser Teil im Allgemeinen im finally-Block steht:
Listing 22.5 com/tutego/insel/jdbc/FirstSqlAccess.java, Ausschnitt
Connection con = null; try { con = DriverManager.getConnection( ... ); ... } catch ( SQLException e ) { e.printStackTrace(); } finally { if ( con != null ) try { con.close(); } catch ( SQLException e ) { e.printStackTrace(); } }
interface java.sql.Connection
extends Wrapper |
- void close() throws SQLException Schließt die Verbindung zur Datenbank. Auch hier kann eine SQLException auftauchen.
Wartezeit einstellen
Wenn wir uns mit der Datenbank verbinden, lässt sich noch eine Wartezeit in Sekunden einstellen, die angibt, wie lange der Treiber für die Verbindung mit der Datenbank warten darf. Gesetzt wird dieser Wert mit setLoginTimeout() und entsprechend mit getLoginTimeout() ausgelesen. Standardmäßig ist dieser Wert 0.
class java.sql.DriverManager |
- static void setLoginTimeout( int seconds ) Setzt die Zeit, die maximal gewartet wird, wenn der Treiber sich mit einer Datenbank verbindet.
- static int getLoginTimeout() Liefert die Wartezeit in Sekunden.
22.5.6 DataSource 

Die Arbeit mit dem DriverManager sah bisher so aus, dass wir ihn mit der genauen Datenquelle parametrisiert haben. Wir mussten also immer den Namen der Datenbank sowie (optional) den Benutzernamen und das Passwort angeben. Diese feste Verdrahtung im Quellcode ist allerdings nicht so toll, weil Änderungen zu einer zwangsläufigen neuen Übersetzung führen (was sich allerdings mit Konfigurationsdateien verändern ließe), doch die Daten stehen auf der Clientseite, wo sie nicht immer gut aufgehoben sind. Besser ist eine zentrale Stelle für die Konfigurationsdaten und auch für die Datenbank. Nehmen wir an, ein Unternehmen rüstet spontan von Oracle auf DB2 um, so müsste ein Clientprogramm an allen Stellen, an denen der Datenbanktreiber geladen und die URL für die Datenbank aufgebaut wird, im Quellcode geändert werden. Das ist unflexibel.
So gibt es in Java eine weitere Möglichkeit, nämlich die Konfigurationsdaten an einer zentralen Stelle zu hinterlegen – und das heißt in Java Zugriff über JNDI. Im zentralen Namensdienst werden Informationen über Treibername, Datenbankname und so weiter als DataSource abgelegt und dann zum nötigen Zeitpunkt erfragt. Wenn sich die Datenbank einmal ändern sollte, muss nur an dieser zentralen Stelle eine Änderung eingespielt werden, und alle, die anschließend den JNDI-Dienst erfragen, bekommen die neue Information.
Die DataSource-Triologie
Die Verbindung zu einem Datengeber (es muss nicht unbedingt eine Datenbank sein) realisieren Objekte vom Typ DataSource-Objekt. Von der Schnittstelle DataSource gibt es drei unterschiedliche Ausführungen:
1. | Ein Standard-DataSource-Objekt. Mindestens das muss ein JDBC-2.0-kompatibler Treiber anbieten. |
2. | Ein DataSource-Objekt, das gepoolte Datenbankverbindungen zulässt (ConnectionPoolDataSource), sodass eine beendete Verbindung nicht wirklich beendet wird, sondern nur in einen Pool zur Wiederverwendung gelegt wird. Damit die Verbindung zurückgelegt werden kann, muss die Verbindung einfach nur geschlossen werden – ein Vorgang, der in jedem Programm mit Verbindungen zu finden sein sollte. |
3. | Ein DataSource-Objekt für verteilte Transaktionen (XADataSource). |
Das Schöne daran ist, dass der konkrete Typ verborgen bleiben kann und der Server ohne Änderung des Clients statt einer einfachen DataSource etwa eine ConnectionPoolDataSource in den Namensdienst ablegen kann.
Verbindung über JNDI
Das DataSource-Objekt ist über JNDI zu erfragen. Mit getConnection() wird anschließend das Connection-Objekt besorgt, und wir sind an der gleichen Stelle, wo uns auch der DriverManager hinführte.
Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup( "jdbc/database" ); Connection con = ds.getConnection( "username", "password" );
Das javax.naming.Context-Objekt und dessen Methode lookup() erfragen das mit dem Namen assoziierte Objekt vom Verzeichnisdienst. Vorher muss natürlich irgendjemand dieses Objekt dort abgelegt – auf Neudeutsch »deployed« – haben, doch das sehen wir uns später an. Mit dem Verweis auf das DataSource-Objekt können wir getConnection() aufrufen und Benutzername und Passwort angeben.
interface javax.sql.DataSource |
- Connection getConnection( String username, String password ) Versucht, unter Angabe des Benutzernamens und Passworts eine Verbindung aufzubauen.
- Connection getConnection() Versucht, eine Verbindung ohne Angabe von Benutzername und Passwort aufzubauen.
Eine DataSource im JNDI-Kontext deployen
Der Administrator ist nun dafür verantwortlich, dass das DataSource-Objekt, also die Beschreibung der Datenbank-Parameter, im Namensdienst eingetragen ist. Im Allgemeinen macht das der Container über eine XML-Beschreibungsdatei oder über eine GUI, sodass kein Programmieraufwand von Hand nötig ist. Wie die JNDI-DataSource im Web-Container Tomcat integriert werden kann, zeigt die Webseite http://tomcat.apache.org/tomcat-5.5-doc/jndi-datasource-examples-howto.html.
Zum Testen wollen wir den einfachen Namensdienst Simple-JNDI (http://www.osjava.org/simple-jndi/) nutzen, der die Daten im Speicher hält und keinen Server startet. Das Java-Archiv simple-jndi.jar von der Webseite muss dafür zusätzlich zum Datenbanktreiber in den Klassenpfad aufgenommen werden. In den Klassenpfad setzen wir auch die Datei jndi.properties, die Java bei Bildung eines Exemplars von InitialContext automatisch lädt.
Listing 22.6 src/jndi.properties
java.naming.factory.initial=org.osjava.sj.SimpleContextFactory org.osjava.sj.root=config/
In das Projektverzeichnis legen wir unter einem neuen Verzeichnis config die Datei UllisDS.properties. Damit kann Simple-JNDI die Datenquelle automatisch initialisieren.
Listing 22.7 config/UllisDS.properties
type=javax.sql.DataSource driver=org.hsqldb.jdbcDriver url=jdbc:hsqldb:file:TutegoDB;shutdown=true user=sa password=
Wir könnten den Namensdienst zwar dank der Implementierung der JNDI-API auch selbst konfigurieren, aber eine passende Datei macht das automatisch. Auf Grund des Dateinamens legt Simple-JNDI automatisch für uns eine Datenquelle »UllisDS« im Namenskontext an. Der Zugriff gestaltet sich einfach:
Listing 22.8 com/tutego/insel/jdbc/JdbcWithDS.java
package com.tutego.insel.jdbc; import java.sql.*; import javax.naming.InitialContext; import javax.sql.DataSource; public class JdbcWithDS { public static void main( String[] args ) throws Exception { Connection con = null; try { DataSource ds = (DataSource) new InitialContext().lookup( "UllisDS" ); con = ds.getConnection(); ResultSet rs = con.createStatement().executeQuery( "SELECT * FROM Customer" ); while ( rs.next() ) System.out.printf( "%s, %s %s%n", rs.getString( 1 ), rs.getString( 2 ), rs.getString( 3 ) ); } finally { if ( con != null ) try { con.close(); } catch ( SQLException e ) { e.printStackTrace(); } } } }
An diesem Beispiel ist gut abzulesen, dass die Konfiguration nun extern ist. Der Datenbanktreiber muss nun nicht mehr von uns geladen werden, und die Verbindungsdaten sind auch nicht mehr sichtbar.
|
22.5.7 Gepoolte Verbindungen 

Da der Auf- und Abbau von Datenbankverbindungen relativ teuer ist, soll eine Java-Applikation die Verbindung nur vordergründig schließen, ein spezieller Pooling-fähiger Datenbank-treiber die Verbindung allerdings für die nächste Operation offen halten. Für gepoolte Datenbankverbindungen gibt es eine Reihe quelloffener Implementierungen; zu ihnen zählt Apache Commons DBCP (http://jakarta.apache.org/commons/dbcp/) oder Proxool (http://proxool.sourceforge.net/). DBCP wird direkt von Simple-JNDI unterstützt, so dass wir es an dieser Stelle einsetzen wollen. Dazu sind in den Klassenpfad die – auch auf der Webseite von Simple-JNDI aufgeführten – zusätzlichen Java-Archive commons-collections-3.1.jar, commons-pool-1.3.jar und commons-dbcp-1.2.1.jar in den Klassenpfad mit aufzunehmen. Die Zeile pool=true veranlasst Simple-JNDI, automatisch auf DBCP zurückzugreifen.
Listing 22.9 UllisDS.properties
type=javax.sql.DataSource
driver=org.hsqldb.jdbcDriver
url=jdbc:hsqldb:file:TutegoDB;shutdown=true
user=sa
password=
pool=true