Programmieren mit JDBC

Transcription

Programmieren mit JDBC
JDBC mit Oracle und PostgreSQL
Holger Jakobs – [email protected], [email protected]
2012-03-13
Inhaltsverzeichnis
1 Warum Java in Verbindung mit Datenbanken?
2 Vorstellung von JDBC
2.1 ODBC – zum Vergleich .
2.2 JDBC – was ist anders?
2.3 Portabilität von JDBC .
2.4 Die JDBC-Treibertypen
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
.
.
.
.
.
.
.
.
.
.
.
.
3 Programmierung mit JDBC
3.1 Bestandteile eines JDBC-Programms . . . . . . .
3.1.1 Exceptions . . . . . . . . . . . . . . . . . .
3.1.2 Datenbanktreiber laden . . . . . . . . . .
3.1.3 Datenbankverbindung herstellen . . . . . .
3.1.4 Statement herstellen . . . . . . . . . . . .
3.1.5 Anfrage ausführen . . . . . . . . . . . . .
3.1.6 Ergebnistupel lesen . . . . . . . . . . . . .
3.1.7 Ergebnismenge und Statement freigeben .
3.2 Ein komplettes JDBC-Programm . . . . . . . . .
3.3 Automatische Datenbank-Verbindung . . . . . . .
3.4 Metadaten einer Ergebnismenge . . . . . . . . .
3.4.1 Ermitteln der Metadaten . . . . . . . . . .
3.4.2 Verarbeitung einer Ergebnismenge zu einer
3.4.3 Erweiterungen der dynamischen Abfrage .
3.5 Metadaten einer Datenbankverbindung . . . . . .
3.6 SQL-Escapes . . . . . . . . . . . . . . . . . . . .
3.7 Transaktionen . . . . . . . . . . . . . . . . . . . .
3.7.1 Beginnen und Beenden von Transaktionen
3.7.2 Isolationslevel . . . . . . . . . . . . . . . .
3.8 Fehler und Warnungen . . . . . . . . . . . . . . .
3.8.1 Fehler . . . . . . . . . . . . . . . . . . . .
3.8.2 Warnungen . . . . . . . . . . . . . . . . .
3.9 Prepared Statements . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
2
3
4
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
HTML-Tabelle
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
5
5
6
7
7
7
8
8
9
10
10
11
11
12
12
12
12
13
13
13
14
14
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
2 VORSTELLUNG VON JDBC
1 Warum Java in Verbindung mit Datenbanken?
Eigentlich erscheint es auf den ersten Blick nicht so sinnvoll zu sein, die objektorientierte
Sprache Java mit den tabellenorientierten relationalen Datenbanken zu verbinden. Trotzdem gibt es mehrere Anbindungsmöglichkeiten, eben weil sich der objektorientierte Ansatz
bei der Programmierung vorwiegend durchgesetzt hat, im Bereich der Datenbanken aber
nach wie vor die relationalen Datenbanken die führende Rolle spielen. Es gibt zwar auch
ernst zu nehmende Ansätze im Bereich objektorientierter Datenbanken, aber da meistens
auch auf bereits bestehende und nicht nur neu zu erstellende Datenbestände zugegriffen
werden muss, ist da der Übergang nicht so schnell zu schaffen. Die objektorienteren Datenbanken führen noch immer eher ein Nischendasein (Stand Mitte 2008).
Die relationalen Datenbanken haben aber bereits begonnen, objektorientierte Dinge hinzuzufügen, so dass man von objektrelationalen Datenbanken spricht. Oracle enthält seit
Version 8 objektorientierte Ansätze, PostgreSQL seit Version 7.
Zu diesen Ansätzen gehört z. B. die Abkehr vom Grundsatz, dass ein Objekt durch
seine Attribute identifiziert wird. Während im rein relationalen Ansatz niemals zwei Tupel
von ihren Werten her identisch sein können – das wäre ein Widerspruch zur Relation
als Tupelmenge –, ist das bei Objekt-Datenbanken durchaus möglich, denn die Identität
eines Objekts ergibt sich aus dem Object-Identifier (bei Oracle wäre das die rowid, bei
PostgreSQL die oid) und ist von den Attributwerten völlig losgelöst.
Darüber hinaus sind komplexe Datenelemente in Tupeln erlaubt, z. B. Mengen (set of),
Tupel (tupel of), Listen (list of) – so in Oracle 8. Es kann auch Vererbungen geben,
so dass eine Tabelle Attribute und ggf. auch Tupel einer anderen Tabelle (oder sogar von
mehreren) erbt – so in PostgreSQL 7.
Eine objektorientierte Datenbank kann auch Methoden enthalten, d. h. neben den reinen
Daten auch Ausführbares. Die Methoden beschreiben dann, wie die Datenobjekte manipuliert werden dürfen.
Es scheint sich also eher ein evolutionärer Weg von den relationalen Datenbanksystemen über die objektrelationalen abzuzeichnen als ein revolutionärer Bruch und Übergang
zu rein objektorientierten Systemen. Schließlich haben nicht einmal die objektorientierten Programmiersprachen die prozeduralen vollständig verdrängt – und das ist nicht ohne
Grund so.
2 Vorstellung von JDBC
Wenn ODBC für Open Data Base Connectivity steht, bedeutet dann JDBC nicht Java
Data Base Connectivity? Laut Sun, dem Erfinder von JDBC ist das nicht so – eine andere
Deutung der Buchstabenkombination liefert Sun allerdings auch nicht.
2
2 VORSTELLUNG VON JDBC
2.1 ODBC – zum Vergleich
2.1 ODBC – zum Vergleich
ODBC wurde von Microsoft entwickelt, um insbesondere den Office-Produkten einen direkten Zugriff auf Datenbanken beliebigen Typs zu geben. Für jede Datenbank auf Serverseite
und jedes Betriebssystem auf Clientseite benötigt man hierfür einen Treiber. Die Datenbank muss also ODBC-fähig sein, und es muss auf Clientseite für genau diese Datenbank
einen passenden ODBC-Treiber geben. Meistens handelt es sich hierbei um compilierten
C-Code, d. h. er ist in der übersetzten Form sowohl von der Hardware als auch dem Betriebssystem abhängig. Da es aber nur wenige Windows-Versionen und diese jeweils nur
auf einer einzigen Hardware-Plattform gibt, bleibt die Anzahl der Treiber in Grenzen. Bei
5 Plattformen und 10 Datenbanken wären das ansonsten auch schon 50 bereitzustellende
Treiber.
ODBC definiert übrigens keine völlig abstrakte Schnittstelle, sondern es werden einfach
SQL-Anfragen als Zeichenketten an die Datenbank geschickt, die diese dann interpretiert
– so wie das bei den gängigen Datenbank-Frontends auch der Fall ist. Das bedeutet aber,
dass eine SQL-Anfrage nach SQL92, z. B. SELECT * FROM A NATURAL JOIN B von vielen
Datenbanken nicht verstanden wird. Der jeweilige SQL-Dialekt muss also nach wie vor berücksichtigt werden, was beim Wechsel des Datenbanksystems zu Problemen führen kann,
wenn man viele Anfragen umschreiben muss.
Für die MS-eigenen Datenspeicher (von einfachen Textdateien über Excel-Tabellen, den
Jet-Engine-Dateien mit der Endung .mdb) sowie für einige klassische Datenbank-Dateien
wie dBase und Foxpro (.dbf) werden die ODBC-Treiber bei Windows bereits mitgeliefert,
für andere Datenbanken gibt es sie von den jeweiligen Herstellern. Alle werden in der
Systemsteuerung unter ODBC-Datenquellen verwaltet.
2.2 JDBC – was ist anders?
Java verfolgt konsequent einen plattformübergreifenden Ansatz und möchte auf möglichst
vielen Geräten lauffähig sein. Die Java VM (virtuelle Maschine) mit dem Bytecode-Interpreter wird daher fleißig auf immer neue Hardware portiert. Daher kann der compilierte
Bytecode quasi überall ausgeführt werden. Allerdings muss auch der JDBC-Treiber für die
jeweilige Datenbank passen, weil die Datenbanksysteme alle ein eigenes Protokoll für die
Kommunikation zwischen Client und Server verwenden.
Als JDBC vorgestellt wurde, gab es nur wenige Treiber, die von Sun entwickelt worden
waren. Um es trotzdem möglichst schnell einsetzbar zu machen, fügte Sun die JDBCODBC-Bridge hinzu, so dass auf diese Weise jede ODBC-fähige Datenbank auch gleichzeitig JDBC-fähig wurde. Allerdings muss betont werden, dass dies nur dem Anstoßen der
Entwicklung dienen sollte und heute keineswegs mehr der richtige Ansatz für den Einsatz
von JDBC ist. Es gibt heute ausreichend „native“ Treiber, die eine wesentlich bessere Performance und auch einen größeren Funktionsumfang bieten. Außerdem bindet man das
Java-Programm an eine Schnittstelle, die plattformabhängig ist, konterkariert also den
Java-Ansatz!
In Java-Applets ist die Verwendung von ODBC übrigens völlig unmöglich, weil Applets
3
2.3 Portabilität von JDBC
2 VORSTELLUNG VON JDBC
ausschließlich aus Java-Bytecode bestehen müssen und nur Java-Klassen nachladen können,
aber keinen Maschinencode.
Ein JDBC-Treiber besteht aus einer Java-Klasse – meist einer .jar-Datei. Diese ist, da
in Java geschrieben, unmittelbar auf allen Plattformen mit Java-Interpreter lauffähig. Man
benötigt also für jedes Datenbanksystem nur einen einzigen JDBC-Treiber. Dieser JDBCTreiber verhält sich gegenüber dem Datenbank-Server genau wie ein in C geschriebener
Client, d. h. es wird das datenbankspezifische Protokoll verwendet.
Gegenüber dem Java-Programm (Application oder Applet) verhalten sich alle JDBCTreiber gleich, d. h. für den Java-Programmierer ist es völlig gleichgültig, welcher JDBCTreiber später einmal mit dem Programm verwendet werden soll. Beim Umstieg von einem
Datenbanksystem auf ein anderes ist also keine Änderung am Programmcode notwendig –
außer den Namen des JDBC-Treibers und der Datenbank natürlich. Ein Java-Programm
kann sogar mehrere JDBC-Treiber laden und gleichzeitig auf mehrere Datenbanken zugreifen.
2.3 Portabilität von JDBC
Man fragt sich, warum das überhaupt ein Thema ist. Schließlich wurde oben erläutert, dass
die Java-Programme plattformunabhängig sind, weshalb die Portierung auf eine andere
Plattform eigentlich nur aus einem Kopiervorgang des Java-Programms bestehen sollte.
Das ist soweit auch richtig.
Allerdings geht es nicht nur um die Clientseite, sondern auch um die Datenbank. Leider
kommuniziert auch JDBC mit der Datenbank über die Sprache SQL mit all ihren Dialekten. Das bedeutet, dass die SQL-Queries auf die jeweilige Zieldatenbank abgestimmt sein
müssen. Das bereits in Abschnitt 2.1 auf der vorherigen Seite erläuterte Problem der Unterschiedlichkeit bei den Datenbanksystemen besteht weiterhin. JDBC ist eine „Low-Level“
Schnittstelle zum SQL-Interface diverser Datenbanken, aber ebnet diese Unterschiede keineswegs ein (genau das beabsichtigt beispielsweise das ADO-Konzept von PHP).
Immerhin verlangt Sun von JDBC-Treibern die Einhaltung des „Entry Levels“ von SQL92. Nur dann darf sich der Treiber „JDBC compliant“ nennen.
Für einige typische Probleme, beispielsweise das Datumsformat, gibt es die Lösung von
SQL-Escapes (siehe Abschnitt 3.6 auf Seite 12). Man darf in SQL-Kommandos ein Datum
als „{d ’2003-12-31’}“ schreiben, auch wenn das Datenbanksystem das Datum evtl. in
einem nicht ISO-8601-gemäßen Format verlangt.
Über diverse Metadaten-Abfragen kann zur Laufzeit abgefragt werden, welche Eigenschaften das gerade verwendete Datenbanksystem hat. Beispiele sind in Abschnitt 3.4 auf
Seite 10 zu finden. Falls man zum Entwicklungszeitpunkt nicht absolut sicher ist, welche
Datenbank-Backends später einmal verwendet werden, sollte man sich bemühen, nur sehr
kompatible Abfragen zu verwenden. Sogar so einfache Dinge wie NATURAL JOIN werden
schließlich nicht von allen Systemen verstanden.
4
2 VORSTELLUNG VON JDBC
2.4 Die JDBC-Treibertypen
Abbildung 1: Die 4 Treibertypen von JDBC
Java
JDBC−ODBC−
Bridge
Treiber für
DBMS B
C
ODBC−
Treiber (C)
Treiber für
DBMS B
Middleware−
JDBC−Treiber
Treiber für
DBMS D
Client
Java−Programm mit JDBC−Treibermanager
Middleware
A
Treibertyp: Typ 1
B
C
D
Typ 2
Typ 3
Typ 4
2.4 Die JDBC-Treibertypen
Die JDBC-Treiber lassen sich in vier Typen untergliedern, siehe auch Abbildung 1:
1. Typ 1
Das ist die oben erwähnte JDBC-ODBC-Bridge, eine reine Übergangslösung und
heute nicht mehr zeitgemäß. Bitte nicht verwenden!
2. Typ 2
Das ist ähnlich wie Typ 1, aber es wird nicht auf einen ODBC-Treiber, sondern auf
einen für die Zieldatenbank speziellen, z. B. in C geschriebenen Treiber zurückgegriffen. Auch dies ist eine plattformabhängige Übergangslösung und heute nicht mehr
zeitgemäß. Bitte nicht verwenden!
3. Typ 3
Hier wird ein einheitlicher Java-Treiber für alle Datenbanken verwendet. Er greift
nicht direkt auf die Zieldatenbank zu, sondern auf eine Middleware, die nicht unbedingt in Java geschrieben sein muss. Diese befindet sich nicht im plattformunabhängigen Client, sondern entweder auf dem Datenbankserver oder einem dritten Rechner.
Die Middleware setzt die Anfragen vom JDBC-Treiber für die Zieldatenbank passend
um. Dies ist clientseitig eine 100 %-Java-Lösung und daher plattformunabhängig. Ein
5
3 PROGRAMMIERUNG MIT JDBC
Beispiel für die Middleware ist OpenLink1 . Man spricht hier von einem DreischichtModell (3-Tier-Model).
4. Typ 4
Diese Treiber sind wie die von Typ 3 zu 100 % in Java geschrieben und daher plattformunabhängig. Sie benötigen allerdings keine Middleware, sondern greifen direkt
auf die Zieldatenbank zu. Das bedeutet, dass die gesamte Funktionalität der Middleware hier im Treiber auf Clientseite integriert ist. Das macht den Treiber zwar
etwas größer, aber man benötigt keine dritte Schicht, weshalb man hier von einem
Zweischicht-Modell spricht (2-Tier-Model). Die Treiber stammen meist direkt von
den Datenbank-Herstellern.
3 Programmierung mit JDBC
Hinweis: Komplette Beispielquelltexte stehen im Datenbank-Portal unter „Quelltexte“2 zur
Verfügung.
3.1 Bestandteile eines JDBC-Programms
Ein JDBC-Programm ist natürlich in erster Linie ein gewöhnliches Java-Programm – sei es
ein Applet oder eine Application. Hier sollen nur die Besonderheiten erläutert werden, die
JDBC betreffen. Als Beispiele dienen hier kleine, kommandozeilenorientierte Applications,
so dass der Code übersichtlich bleibt und den Blick auf das für JDBC Wesentliche nicht
versperrt wird.
3.1.1 Exceptions
Die Klasse, die JDBC verwendet, kann folgende Exceptions werfen: ClassNotFoundException, IOException, SQLException. Diese sind daher bei der Deklaration zusätzlich
zu den übrigen evtl. Notwendigen anzugeben.
3.1.2 Datenbanktreiber laden
Der Datenbanktreiber muss geladen werden. Der Name des Treibers hängt von der Datenbank (Typ 4) bzw. von der Middleware (Typ 3) ab. Bei PostgreSQL heißt er org.postgresql.Driver und muss daher bei Oracle mit der Anweisung Class.forName("oracle.
jdbc.driver.OracleDriver") und bei PostgreSQL mit Class.forName("org.postgresql.Driver") geladen werden. Damit das klappt, muss sichergestellt sein, dass die
Java-Archive mit dem Treiber (nicht das Verzeichnis, in dem es liegt!), bei Oracle classes12.jar
und nls_charset12.jar, bei PostgreSQL postgresql.jar, im CLASSPATH enthalten sind.
1) http://www.openlinksw.com
2) http://www.bg.bib.de/portale/dab/Quelltexte
6
3 PROGRAMMIERUNG MIT JDBC
3.1 Bestandteile eines JDBC-Programms
Eigentlich liefert die Methode nur die Klasse zu einem Namen, aber bei bislang nicht geladenen Klassen versucht der Classloader, diese Klasse im CLASSPATH zu finden und sie
zu laden. Dieser Nebeneffekt wird hier ausgenutzt, während das eigentliche Ergebnis der
(statischen) Methode gar nicht verwendet wird.
Der dynamische Ladevorgang wird erst zur Laufzeit ausgeführt, d. h. man kann den
zu ladenden Namen auch vom Anwender erfragen oder aus einer Datei oder anderswo
her laden, so dass man zum Zeitpunkt der Programmerstellung noch gar nicht wissen
muss, wie der Name lautet. Auf diese Weise können auch nachträglich bislang unbekannte
Datenbanken verwendet werden.
3.1.3 Datenbankverbindung herstellen
Wie bei den interaktiven Tools (psql, sqlplus, TOra, pgAdminIII) und den Programmen mit Embedded SQL in C auch muss (mindestens) eine Verbindung zur Datenbank
hergestellt werden. Die Angabe der Datenbank geschieht bei JDBC mit einem URL (Uniform Resource Locator), der außer Benutzername und Passwort alle notwendigen Angaben
enthält. Letztere werden in weiteren Parametern übergeben. Der URL besteht aus
• jdbc:
• subprotocol, d. h. Name des Datenbanktyps, z. B. oracle:thin oder postgresql,
gefolgt von einem weiteren Doppelpunkt
• weiteres ist der sogenannte subname, bei Oracle bestehend aus einem @-Zeichen,
dem Namen des Datenbankservers, der Portnummer und dem Namen der Datenbank, bei uns also @dbserver2:1521:ora10; bei PostgreSQL sind es zwei Slashes, der Name des Datenbankservers und der Name der Datenbank bei uns also
//dbserver2/datenbankname. Achtung: Auch die Trennzeichen unterscheiden sich.
Für die Verbindung wird ein Objekt vom Typ Connection benötigt.
Bei Oracle sieht die Anweisung zur Herstellung der Datenbankverbindung also so aus:
Connection conn = DriverManager.getConnection
("jdbc:oracle:thin:@dbserver2:1521:ora10", "username", "password");
Wenn das Passwort hier angegeben wird, dann ist es in der .class-Datei vorhanden, zwar
vielleicht nicht direkt im Klartext, aber auch nicht ausreichend geschützt.
Bei PostgreSQL sieht die Anweisung zur Herstellung der Datenbankverbindung also so
aus:
Connection conn = DriverManager.getConnection
("jdbc:postgresql://dbserver2/dbname", "username", "password");
Das Passwort kann leer bleiben, falls die Datenbank den Benutzer aufgrund seines bereits
erfolgen Logins beim Betriebssystem erkennt (sogenanntes Single-Sign-On).
7
3.1 Bestandteile eines JDBC-Programms
3 PROGRAMMIERUNG MIT JDBC
3.1.4 Statement herstellen
Die Datenbankverbindung kann während der gesamten Laufzeit aufrecht erhalten bleiben.
Lediglich wenn ein Programm nur gelegentlich auf die Datenbank zugreift und nur eine
begrenzte Anzahl von Verbindungen zur Verfügung steht, könnte man sie zwischendurch
beenden und wieder aufbauen.
Für jede einzelne Abfrage benötigt man aber noch ein Objekt vom Typ Statement. Das
Erzeugen eines Statements braucht keine Parameter. Anschließend ist alles bereit, um eine
Anfrage auszuführen.
Statement stmt = conn.createStatement();
3.1.5 Anfrage ausführen
Eine Anfrage liegt in Form eines String-Objekts vor und kann komplett vom Benutzer
erfragt oder auch aus festen und variablen Anteilen zusammengebaut werden. Das Ergebnis der Methode executeQuery ist ein Objekt vom Typ ResultSet. Diese ist bei Selects
anzuwenden – bei allen anderen Anfragen (Insert, Update, Delete oder auch Datendefinitionskommandos) ist executeUpdate zu benutzen, das statt einer Ergebnismenge die
Anzahl der vom Kommando betroffenen Tupel liefert.
String query = "select name, ort from kunden where knr=7";
ResultSet result = stmt.executeQuery (query);
Mit dieser Ergebnismenge kann weiter gearbeitet werden, z. B. Größe abgefragen, Tupel
des Ergebnisses lesen usw.
3.1.6 Ergebnistupel lesen
Die häufigste Anwendung ist wohl das Lesen der Ergebnistupel, weshalb genau das hier
gezeigt wird. Hierzu bietet das Ergebnismengenobjekt entsprechende Methoden an.
Die Methode next() setzt den Lesezeiger auf das nächste Tupel und liefert true, wenn
es dieses nächste Tupel gibt – d. h. am Ende der Daten liefert sie false. Dies eignet sich
zur Konstruktion einer while-Schleife.
Die Methode getString(int) verlangt einen int-Parameter, der die Nummer des Attributs angibt (gezählt wird ab 1). Es wird ein String-Objekt geliefert mit dem Inhalt des
entsprechenden Attributs aus dem aktuellen Tupel. Entsprechende Methoden gibt es für
die anderen Datentypen (getInt(), getFloat(), . . .).
Mit diesen Informationen können wir die Ausgabe der gefundenen Daten programmieren:
while (result.next()) {
String name = result.getString(1);
String ort = result.getString(2);
System.out.println (name + ", " + ort);
8
3 PROGRAMMIERUNG MIT JDBC
3.2 Ein komplettes JDBC-Programm
} // while
System.out.println(">> Ende der Daten <<");
Alternativ zum numerischen Parameter kann man auch den Namen des Attributs übergeben. Für diese Form ist die Methode overloadet. Da die Verwendung des numerischen Index
performanter ist, kann man den numerischen Index eines Attributs herausfinden über die
Methode findColumn(attributname).
Bei Attributen, die auch den Nullwert beinhalten können, kann man mittels der parameterlosen Methode wasNull() herausfinden, ob das letzte get. . .() einen Wert Null geliefert
hat. Die get. . .()-Methoden liefern zwar sowieso auch in Java ein null, aber wenn man
das Ergebnis in einfachen Variablen speichert, kann man es von einer numerischen 0 nicht
unterscheiden.
3.1.7 Ergebnismenge und Statement freigeben
Die Ergebnismenge und das Statement bleiben auch nach dem Auslesen erhalten. Zur
Ressourcenschonung und auch zur evtl. Aufhebung von Sperren auf die Datenbank ist es
notwendig, sie freizugeben. Ansonsten geschieht das erst am Ende des Programms.
result.close();
stmt.close();
Verbindung beenden
Am Ende des Programms – ggf. früher – kann man die Verbindung zur Datenbank schließen.
conn.close();
3.2 Ein komplettes JDBC-Programm
In diesem kleinen Beispielprogramm ExampleJDBC_pg.java bzw. in der Oracle-Variante
ExampleJDBC_ora.java sind die sonst notwendigen bzw. sinnvollen Fehlerprüfungen nicht
enthalten, so dass Exceptions zum Programmabbruch führen.
import java.io.*;
import java.sql.*;
public class ExampleJDBC_pg {
public ExampleJDBC_pg()
throws ClassNotFoundException, FileNotFoundException,
IOException, SQLException {
Class.forName("org.postgresql.Driver");
9
3.3 Automatische Datenbank-Verbindung
3 PROGRAMMIERUNG MIT JDBC
Connection conn =
DriverManager.getConnection ("jdbc:postgresql://dbserver2/kunden",
"hugo", "geheim");
Statement stmt = conn.createStatement();
System.out.print ("Ort: ");
System.out.flush();
BufferedReader r =
new BufferedReader (new InputStreamReader (System.in));
String ort = r.readLine();
String query = "select name, ort from kunden where ort = '"
+ ort + "'";
System.out.println (query);
ResultSet res = stmt.executeQuery (query);
while (res.next()) {
String name = res.getString(1);
ort = res.getString(2);
System.out.println (name + ", " + ort);
} // while
System.out.println(">> Ende der Daten <<");
res.close();
stmt.close();
conn.close();
}
public static void main (String args[]) {
try {
new ExampleJDBC_pg();
} catch (Exception exc) {
System.err.println ("Exception caught.\n" + exc);
exc.printStackTrace();
}
} // main
} // class
3.3 Automatische Datenbank-Verbindung
Es gibt bei uns eine vordefinierte Klasse namens DabVerbindung – die Quelltextdatei
finden Sie ebenfalls im Portal – mit den notwendigen Mechanismen, um eine Verbindung
wahlweise mit Oracle oder mit PostgreSQL herzustellen. Hierzu werden die Zugangsdaten
10
3 PROGRAMMIERUNG MIT JDBC
3.4
Metadaten einer Ergebnismenge
aus den Dateien $HOME/dabpw_oracle.sql bzw. $HOME/dabpw_postgresql.sql gelesen.
Das bedeutet, dass Sie ein manuell geändertes Datenbank-Kennwort dort eintragen müssen,
damit es funktioniert. Vorteil ist, dass Sie in Ihre Quelltexte kein Kennwort eintragen
müssen, denn auch aus dem compilierten Programm könnte man es ganz leicht extrahieren.
Außerdem können Sie compilierte Programme weitergeben, so dass andere Anwender sie
verwenden können, dabei aber auf ihrer jeweils eigene Datenbank zugreifen. Beachten Sie
hierzu auch die Dokumentation in Form von JavaDoc.
Das obige Programm funktioniert unabhängig von dieser Klasse. Bauen Sie aber in alle
Ihre eigenen Programm die Klasse DabVerbindung ein und vollziehen Sie die dort verwendeten Mechanismen nach.
3.4 Metadaten einer Ergebnismenge
3.4.1 Ermitteln der Metadaten
Natürlich ist es schöner, wenn man flexiblere Abfragen zulassen kann als im ersten Beispiel
gezeigt, wo Tabellen- und Attributnamen fest im Programm verankert sind. Bei Abfragen
mit SELECT * weiß man zum Zeitpunkt der Programmierung noch nicht, wie viele Attribute die Ergebnistabelle haben wird und wie diese heißen. Die Anzahl und auch die Namen
der Attribute können erst zur Laufzeit ermittelt werden. HTML-Tabellen kann man daraus besonders leicht erstellen, weshalb die Verwendung in CGI-Programmen und Servlets
besonders beliebt ist.
Wenn die Abfrage der Ergebnisse nun allgemein formuliert werden soll, d. h. unabhängig
vom Datentyp der Ergebnisspalte, dann fragt man sich, ob man mit der im ersten Beispiel
verwendeten Methode getString() weit kommt. Hier kommt dem Programmierer die
Eigenschaft von Objekten entgegen, eine toString()-Methode zu haben. Man kann also
getrost getObject() verwenden und sich darauf verlassen, dass bei der Stringverkettung
oder beim print() das Objekt sinnvoll in eine Zeichenkette umgewandelt wird.
Tabelle 1: Ergebnis-Metadaten-Methoden
Methode
getMetaData()
Beschreibung
liefert ein Objekt vom Typ ResultSetMetaData, das
bei allen folgenden Methodenaufrufen verwendet werden
muss
getColumnCount() liefert die Anzahl Attribute (Spalten) der Abfrage
getColumnName(i) liefert den Namen des Attributs Nr. i, wobei die Attribute (Spalten) ab 1 gezählt werden.
11
3.4
Metadaten einer Ergebnismenge
3 PROGRAMMIERUNG MIT JDBC
3.4.2 Verarbeitung einer Ergebnismenge zu einer HTML-Tabelle
Um ein in seiner Struktur zum Programmierzeitpunkt unbekanntes Abfrageergebnis in
einer HTML-Tabelle aufzubereiten, geht man wie folgt vor:
• Metadaten holen (getMetaData())
• Anzahl der Spalten holen (getColumnCount())
• Schleife über die Spalten, um die Spaltennamen zu holen (getColumnName(i))
• Erzeugung der Kopfzeile für die HTML-Tabelle
• Abrufen der Ergebnistupel und Schreiben der Datenzeilen für die HTML-Tabelle
ResultSet res = stmt.executeQuery (query);
ResultSetMetaData rsmd = res.getMetaData();
int anzSpalten = rsmd.getColumnCount();
System.out.print ("<table border><tr>");
for (int i=1; i <= anzSpalten; i++) {
System.out.print ("<th>" + rsmd.getColumnName (i) + "</th>");
} // for
System.out.println ("</tr>");
while (res.next()) {
System.out.print ("<tr>");
for (int i=1; i <= anzSpalten; i++) {
System.out.print ("<td>" + res.getObject(i) + "</td>");
} // for
System.out.println ("</tr>");
} // while
System.out.println ("</table>");
res.close();
stmt.close();
Dies kann man gut in ein CGI-Programm oder in ein Servlet einbauen. Es ist allerdings
auch noch ausbaufähig, daher im Folgenden ein paar Anregungen dazu.
3.4.3 Erweiterungen der dynamischen Abfrage
1. Es könnte z. B. es sinnvoll sein, numerische Spalten rechtsbündig auszurichten. Dazu
müsste man herausfinden, von welcher Klasse das gerade geholte Objekt ist (Methode
getClass()) und darauf basierend eine Fallunterscheidung durchführen. Alternativ
12
3 PROGRAMMIERUNG MIT JDBC
3.5
Metadaten einer Datenbankverbindung
kann man auch mit einer der Methoden getColumnType() oder getColumnTypeName() den SQL-Datentyp herausbekommen. Allerdings ist der Typname datenbankabhängig. Verwenden Sie die Java-API-Doku, um mehr herauszubekommen.
2. Obiger Programmausschnitt ist nicht in der Lage, mit NULL-Werten umzugehen, sondern bricht mit einer NullPointerException ab. Verhindern Sie dies, indem Sie das
gelieferte Objekt in Java mit null vergleichen oder indem Sie die Methode wasNull()
des ResultSets verwenden. Letztere können Sie ebenfalls in der Java-API-Doku finden.
3. Um Datumswerte hervorzuheben, stellen Sie diese in der HTML-Tabelle kursiv dar.
Wie finden Sie heraus, ob eine Spalte Datumswerte enthält?
3.5 Metadaten einer Datenbankverbindung
Auch über eine Datenbankverbindung und das verwendete Datenbanksystem gibt es Metadaten. Hierzu holt man sich zunächst ein Objekt der Klasse DatabaseMetaData zu einer
bestehenden Datenbankverbindung mit conn.getMetaData() und verwendet dies bei den
in Tabelle 2 auf Seite 16 aufgeführten Methoden.
3.6 SQL-Escapes
SQL-Escapes dienen zur Egalisierung von Unterschieden zwischen Datenbanksystemen, um
Programme portabler zu machen, auch wenn die SQL-Dialekte etwas voneinander abweichen. Sie dienen u. a. zur Darstellung von Werten, die nicht bei allen Datenbanksystemen
gleich dargestellt werden, nämlich Zeit- und Datumsstrings, siehe Tabelle 3 auf Seite 16.
Außerdem kann man mit ihnen Funktionen und Prozeduren aufrufen (wird hier nicht erläutert) und Outer Joins datenbankneutral formulieren.
Mit der Methode nativeSQL(query) kann man eine Datenbankanfrage, die mit SQLEscapes formuliert ist, in den datenbankspezifischen SQL-Dialekt übersetzen lassen, beispielsweise in die Oracle-spezifische Darstellung von Outer Joins. Die einzugebende Schreibweise orientiert sich am ANSI-Standard.
3.7 Transaktionen
Ohne weitere Festlegung wird bei JDBC immer Autocommit verwendet, so dass jede einzelne Anweisung in ihrer eigenen Transaktion ausgeführt wird – sofern die verwendete
Datenbank überhaupt Transaktionen unterstützt (was man auch herausfinden kann, siehe
Abschnitt 3.5).
3.7.1 Beginnen und Beenden von Transaktionen
Die für Transaktions-Management notwendigen Methoden werden mit dem VerbindungsObjekt (hier: conn) aufgerufen. Um Transaktionen zu benutzen, muss man zunächst Auto-
13
3.8 Fehler und Warnungen
3 PROGRAMMIERUNG MIT JDBC
commit ausschalten: conn.setAutoCommit(false). Den aktuellen Status kann man mit
conn.getAutoCommit() abfragen.
Bei jeder Anweisung wird eine neue Transaktion begonnen, wenn nicht bereits eine solche
begonnen wurde. Sie kann dann mit conn.commit() abgeschlossen oder mit conn.rollback() zurückgefahren werden.
3.7.2 Isolationslevel
Den aktuell vewendeten Isolationslevel kann man abfragen mit conn.getTransactionIsolation();, dessen Ergebnis man mit den Konstanten TRANSACTION_NONE, TRANSACTION_READ_UNCOMMITTED, TRANSACTION_READ_COMMITTED, TRANSACTION_REPEATABLE_
READ und TRANSACTION_SERIALIZABLE vergleichen muss, um den Level festzustellen. Der
Wert TRANSACTION_NONE wird bei „Datenbanken“ geliefert, die keine Transaktionen unterstützen (und daher eigentlich gar keine Datenbanken sind).
Entsprechend kann man den Isolationslevel auch setzten mit conn.getTransactionIsolation(level);.
Beim Absetzen von Datendefinitionskommandos innerhalb von Transaktionen reagieren
die einzelnen Datenbanksysteme völlig unterschiedlich. Daher kann man das genaue Verhalten mit diversen Methoden abfragen, die alle nicht mit dem Verbindungsobjekt, sondern
mit dem Metadaten-Objekt (siehe Abschnitt 3.5 auf der vorherigen Seite) aufgerufen werden und einen boolean-Wert liefern:
dataDefinitionIgnoredInTransactions(),
dataDefinitionCausesTransactionCommit(),
supportsDataDefinitionAndDataManipulationTransactions(),
supportsDataManipulationTransactionsOnly().
3.8 Fehler und Warnungen
3.8.1 Fehler
Bei Fehlern werden Java-Exceptions ausgelöst, so dass keine ständige manuelle Prüfung
notwendig ist. Der Mechanismus ist vergleichbar mit den WHENEVER-Konstruktionen bei
Embedded SQL in C.
Die ausgelöste Exception enthält nähere Information über den Fehler. Abfragen kann
man eine Fehlermeldung mittels toString(). Den SQLSTATE gemäß ANSI kann man mit
der Methode getSQLState() des Exception-Objekts ermitteln. Der SQLSTATE ist eine fünf
Zeichen lange Zeichenkette. Die ersten beiden Zeichen geben die Fehlerklasse an, die letzten
drei Zeichen den genauen Fehler. In nebenstehender Tabelle sind die wichtigsten Fehlerklassen von PostgreSQL aufgeführt (nähere Information ist im Kapitel „Error Handling and
Diagnostics“ des „Programmer’s Guide to the Oracle Precompilers“ bzw. im Appendix A
der PostgreSQL-Dokumention zu finden).
Damit sind die Fehlerinformationen aus SQLSTATE wesentlich umfangreicher und ausführlicher als die aus dem SQLCODE. Den hier so genannten Error Code bekommt man mit
14
3 PROGRAMMIERUNG MIT JDBC
3.9 Prepared Statements
der Methode getErrorCode(). Der Error Code ist –
jeweiligen Datenbanksystem abhängig. SQLSTATE ist
siert.
Zu einem Zeitpunkt können – je nach Datenbanksystem – nicht nur eine, sondern auch mehrere Fehlermeldungen erzeugt worden sein. Zunächst wird
immer der schwerwiegendste Fehler gemeldet, aber
zur Bestimmung der Ursache kann es hilfreich sein,
sich auch die weiteren Meldungen anzuschauen, wozu man die Methode getNextException() verwendet.
3.8.2 Warnungen
im Gegensatz zum SQLSTATE – vom
dagegen in weiten Teilen standardiCode
00
01
02
03
08
09
0B
22
23
24
25
26
40
53
SQLSTATE-Klasse
kein Fehler
Warnung
keine Daten
Kommando unvollständig
Verbindungsproblem
Fehler bei Triggeraktion
ungültiger Transaktionsbeginn
Datenfehler
Integritätsverletzung
ungültiger Cursorzustand
ungültiger Transaktionszustand
ungültiges SQL-Kommando
Transaktions-Rollback
unzureichende Ressourcen
Über Warnungen informiert Java nicht automatisch,
sondern sie müssen abgefragt werden. Mit Hilfe der
Methode getWarnings(), die auf das Result Set angewendet werden, ermittelt man die erste Warnung.
Gibt es keine, so wird null geliefert. Die jeweils
nächste Warnung bekommt man, wenn man die Methode getNextWarning() auf das Warnungsobjekt
anwendet. Die Klasse SQLWarning ist übrigens eine Unterklasse von SQLException, so
dass man die im vorigen Abschnitt erläuterten Methoden zur Abfrage von Error Code
und SQLSTATE verwenden kann, toString() gibt es natürlich auch.
3.9 Prepared Statements
Prepared Statements (vorbereitete Kommandos) sollen Vorteile bei der Ausführungsgeschwindigkeit bieten, weil sie beim Vorbereiten einmal besonders gründlich optimiert werden und bei den nachfolgenden Ausführungen auf die bereits erfolgte Optimierung zurückgegriffen werden kann. Bei voll dynamischen Kommandos müssen Analyse und Optimierung bei jeder einzelnen Ausführung erneut durchgeführt werden.
Man benötigt ein Statement-Objekt, d. h. ein Objekt der Klasse PreparedStatement.
Diesem weist man den Rückgabewert der Methode prepareStatement (Abfrage) zu, die
auf das Verbindungsobjekt angewendet wird. Die genannte Abfrage kann Platzhalter in
Form von Fragezeichen enthalten. Diese Platzhalter sind von 1 an durchnumeriert und
müssen vor der Ausführung mit einer der Methoden setInt (nr, intWert, setDouble
(nr, doubleWert, setString (nr, StringWert usw. gefüllt werden. nr gibt dabei die
laufende Nummer des Platzhalters an. Mittels setNull (nr kann für einen Platzhalter
auch ein Nullwert eingetragen werden.
Die vorbereitete Abfrage kann anschließend mit der Methode executeQuery() ausführen. Hier ein kleines Codebeispiel, das auf die bereits bestehende Verbindung conn zugreift.
PreparedStatement pst;
15
3.9 Prepared Statements
3 PROGRAMMIERUNG MIT JDBC
pst = conn.prepareStatement ("SELECT * FROM kunden where ort = ?");
pst.setString (1, "Bonn");
ResultSet result = pst.executeQuery();
Die Ergebnismenge result kann genauso wie in Abschnitt 3.1.6 auf Seite 7 beschrieben
durchgeführt werden. Die Ergebnismenge muss natürlich auch freigegeben werden mittels result.close(), ebenso das vorbereitete Kommando genauso wie ein gewöhnliches
Kommando mittels pst.close().
Es ist nicht garantiert, dass eine Datenbank von den möglichen Vorteilen der vorbereiteten Kommandos Gebrauch macht, d. h. es kann sein, dass es zwar die beschriebenen
Methoden gibt, aber vielleicht doch keine Speicherung des Optimierungsergebnisses erfolgt.
$Id: jdbc.tex,v efcb5b401798 2009/03/23 14:15:07 bibjah $
16
3 PROGRAMMIERUNG MIT JDBC
3.9 Prepared Statements
Tabelle 2: kleine Auswahl aus den Datenbank-Metadaten-Methoden
getDatabaseProductName();
liefert einen String mit dem Namen des Datenbanksystems
getTables(catalog, schemamuster, tabellenmuster, tabellentyp);
liefert eine Ergebnismenge (genau wie executeQuery()) mit 5 Attributen: Katalogname,
Schemaname, Tabellenname, Tabellentyp, Bemerkungen. Als Parameter werden übergeben: Katalogname (sofern vom Datenbanksystem unterstützt, sonst null für alle); Muster
(mit % und _ als Platzhalter wie bei SQL allgemein üblich) für den Schemanamen, ggf. nur
%; Muster für den Tabellennamen, ggf. nur %; Tabellentyp (mögliche Werte abfragbar mit
getTableTypes() oder null für alle.
supportsANSIEntryLevelSQL();
supportsANSIIntermediateLevelSQL();
supportsANSIFullLevelSQL();
liefert einen boolean-Wert, der angibt, ob der jeweilige ANSI-Level unterstützt wird oder
nicht
getTypeInfo();
liefert eine Ergebnismenge (genau wie executeQuery()) mit 18 Attributen, von denen
hier nur die ersten 2 erläutert werden: SQL-Datentyp (diese stehen bei CREATE TABLE
zur Verfügung), zugehöriger Java-Datentyp. Dieser wird in Form einer numerischen
Konstanten angezeigt, wobei es folgende Entsprechungen gibt (Auswahl):
bigint
boolean
double
other
time
−5
16
8
1111
92
binary
−2
char
1
float
6
real
7
timestamp 93
bit
date
integer
smallint
varbinary
−7
91
4
5
−3
blob
2004
decimal
3
numeric
2
struct
2002
varchar
12
Tabelle 3: SQL-Escapes
Escape-Notation
{d ’yyyy-mm-dd’}
{t ’hh:mm:ss’}
{ts ’yyyy-mm-dd hh:mm:ss.ffffff ’}
{oj tabelle1 NATURAL LEFT JOIN tabelle2}
17
Darstellung von:
Datum
Zeit
Zeitstempel (timestamp)
Natural Left Outer Join