THEMA: JDBC/SQL Vortrag: Pascal Gbodogbe am 14.6.2001 TEIL_1: SQL & TEIL_2: JDBC Einführung: Durch die Einführung der JDBC-Vorlesung können wir merken: *JDBC stellt eine standarisierte Methode zum Zugriff auf Datenbanken aus Java-Programmen zur Verfügung. *JDBC enthält die Möglichkeiten: – Um Verbindungen zu Datenbanken zu öffnen und zu schließen, – Um SQL-Anweisungen an Datenbanken zu schicken – und um die Antworten entgegenzunehmen und deren Auswertung im Java-Programm zu ermöglichen. JDBC baut vollständig auf SQL auf. SQL "Standard Query Language". Die Kommunikation zwischen dem Java-Programm und der Datenbank erfolgt über SQL-Anweisungen. Diese werden als Text an die Datenbank geschickt, die daraufhin als Antwort die Ergebnisse zurückschickt. Die SQL-Anweisungen werden nicht vom Java-Compiler auf Korrektheit geprüft. Fehler können also erst durch die Datenbank festgestellt werden. Bevor wir in Detail bei JDBC gehen , wollen wir SQL in Überblick sehen. TEIL_1: SQL SQL - ein Überblick Unser Überblick ist unterteilt in : - Tabellendeklaration in SQL, - Anfragen in SQL, - Änderungsoperationen in SQL, und - Definition von Sichten in SQL Tabellendeklaration in SQL: Bei der Deklaration einer Tabelle müssen folgenden Punkte festgelegt werden: * Attribute als Namen der Spalten, * Wertebereiche als Angaben von Datentypen für die Einträge in Spalten, * Relationenschemata als Typdefinitionen von Tabellen, * Schlüssel zur Festlegung eindeutiger Identifikatoren für Tupel in Tabellen, und * die Deklaration von Fremdschlüssel zur Erzwingung der referentiellen Integrität bei Verweisen auf Einträge in anderen Tabellen. Die Sprache zur Festlegung von Datenbankbeschreibungen nennt man database definition language, kurz DDL. Die Tabellendeklaration in SQL sieht der Definition von Record-Strukturen in Programmiersprachen nicht unähnlich, erweitert diese aber um die RDBMSspezifischen Aspecte. Folgende Beispieldeklaration zeigt den prinzipiellen Aufbau: CREATE TABLE book ( isbn VARCHAR ( 30 ) NOT NULL, title VARCHAR ( 50 ) , price FLOAT ); Wertebereiche für Attribute: Die Möglichen Wertebereiche für Attribute sind die Datentypen: * integer (oder auch integer4, int ), * smallint (oder auch integer2), * float (p) (oder auch kurz float), * decimal ( p , q ) und numeric ( p , q ) mit jeweils q Nachkommastellen, * character (n) (oder kurz char(n), bei n = 1 auch char ) für Strings fester Länge n, * character varying (n)(oder kurz varchar(n) ) für Strings variabler Länge bis zur Maximallänge n, * bit (n) oder bit varying (n) analog für Bitfolgen und * date, time bzw. timestamp für Datums-, Zeit- und kombinierte Datums-Zeit-Angaben. BLOB und CLOB: Eine weitere Neuerung im Standard sind die sogenannten large object-Datentypen BLOB und CLOB. BLOB steht für binary large object; CLOB für charater large object. Die Werte entsprechender Daten- typen sind ( in der Regel sehr lange ) Folgen von Binärwerten ( bei einem BLOB ) oder von Character-Zeichen (CLOB), wie sie zum Beispiel bei der Abspeicherung eines digitalisierten Photos oder eines unstrukturierten Textes entstehen. Nullwerte: Mit der Klausel NOT NULL können in bestimmten Spalten Nullwerte als Attributwerte ausgeschlossen werden. Primärschlüssel: Das folgende Beispiel zeigt die Definition von identifizierenden Schlüsseln ( in SQL als Primärschlüssel bezeichnet: derjenige von mehreren möglichen Schlüsseln, der vom System genutzt und kontrolliert wird) und Fremdschlüsselbeziehung: CREATE TABLE book ( isbn VARCHAR ( 30 ) NOT NULL, title VARCHAR ( 50 ) , price FLOAT publ_id INTEGER , PRIMARY KEY ( isbn ) , FOREIGN KEY publ_id REFERENCES (publisher) ); Anfragen in SQL: Eine erste SQL-Anfrage an eine einzelne Tabelle könnte wie folgt lauten: SELECT title , price FROM book WHERE isbn = ‘ 3 - 929821 - 31 - 1 ‘ ; Diese Anfrage gibt die Relation an, aus der Daten gewonnen werden sollen ( hinter FROM ), sowie eine Selektionsbedingung und die Attribute einer Projektion. Die Angaben der Selektionsbedingung können wie gewohnt durch boolesche Operatoren verbunden werden, die als englische Worte notiert werden (AND, OR und NOT). Als Basisprädikate sind insbesondere die bekannten Vergleichsprädikate erlaubt. Duplikatunterdrückung: SQL hat eine Multimengensemantik; die Duplikatunterdrückung muß also explizit erzwungen werden, wenn sie erwünscht ist: SELECT DISTINCT lastname FROM author; Kreuzprodukt: Als wesentliche Möglichkeit, Daten aus zwei Relationen miteinander zu verknüpfen, nutzt SQL das Kreuzprodukt, und nicht den natürlichen Verbund. Die folgende Anfrage berechnet also das Kreuzprodukt zweier Relationen, wobei die Angabe * die Ausgabe aller Attribute erwirkt: SELECT * FROM book, publisher; Verbundbedingungen in SQL: Der ( eigentlich erwünschte) Verbund muß durch die Angabe einer Verbundbedingung erzwungen werden. Hierzu müssen gleichlautende Attribute des Kreuzproduktes unterschieden werden - einen expliziten Umbenennungsoperator für Zwischenergebnisse wie in der Relationenalgebra kennt SQL nicht. Dies erfolgt über sogenannte Tupelvariablen, die im Standardfall aus den Namen der Relationen abgeleitet werden: SELECT * FROM book , publisher WHERE book . Publ_id = publisher . Publ_id; Selbstverbund: Natürlich können auch neue Variablennamen generiert werden - bei Selbstverbunden ist dies sogar notwendig, wie folgende Anfrage zeigt, die die ISBN von Büchern berechnet, die von zwei Autoren gemeinsam geschrieben wurde: SELECT ba1 . isbn FROM book_author ba1 , book_author ba2 WHERE ba1 . isbn = ba2 . isbn AND ba1 . a_id = 3 AND ba2 . a_id = 4; In der letzten Standardisierungsversion SQL-92 wurden auch explizite Verbundoperatoren eingeführt. So berechnet die folgende Anfrage tatsächlich den natürlichen Verbund: SELECT * FROM book NATURAL JOIN publisher; Üblich ist aber in SQL-Anwendungen noch die vorher beschriebene Variante, die dem ersten SQL-Standard entspricht. Die erste Variante wird auch von allen RDBMS unterstützt, was beim SQL-92-Standard nicht automatisch im vollem Umfang der Fall ist. Vereinigung und Differenz in SQL: Der Algebraoperator Vereinigung wird in SQL explizit mit UNION notiert. Der Differenzbildung hingegen wird in der Regel durch geschachtelte Anfragen realisiert, wie folgendes Beispiel zeigt: SELECT * FROM customer WHERE customer . cust_id NOT IN ( SELECT cust_id FROM book_order ); Da geschachtelte Unteranfragen etwas komplexer in der Semantik und den Syntaktischen Varianten sind, belassen wir es an dieser Stelle mit diesem einfachen Beispiel. Aggregierung: * Die Aggregatfunktionen COUNT, SUM, AVG, MAX und MIN erlauben es, aggregierte Werte für Spalten zu bilden. Gruppierung: * Die Gruppierung mittels GROUP BY und HAVING gruppiert Tupel in einer Relation und erlaubt die Bildung eines aggregierenden Resultats für jede Gruppe. Sortierung: Die Angabe von ORDER BY sortiert ein Anfrageergebnis. Änderungsoperationen in SQL In SQL werden die folgenden Änderungsoperationen unterstützt: * Die INSERT-Anweisung ermöglicht das Einfügen eines oder mehrerer Tupel in eine Basisrelation oder Sicht. * Mittels UPDATE können ein oder mehrere Tupel in eine Basisrelation oder Sicht geändert werden. * Die DELETE-Anweisung realisiert das Löschen eines oder mehrerer Tupel aus einer Basisrelation oder Sicht. Die INSERT-Anweisung: Für das Einfügen mittels INSERT kann man zwei Varianten unterscheiden. In der ersten Variante werden konkrete Werte in eine Tabelle eingetragen: INSERT INTO publisher (publ_id , name) VALUES (1 , ‘ dpunkt - Verlag ‘ ) ; Die Angabe der Attribute hinter dem Tabellenname ist notwendig, wenn nicht alle Attribute gesetzt werden sollen und die restlichen mit Nullwerten gefüllt werden sollen. In der zweiten Variante werden die einzufügenden Tupel mittels einer Anfrage berechnet: INSERT INTO publisher (publ_id , name) ( SELECT id, name FROM company ) ; Die UPDATE-Anweisung: Die Attributwertänderung mittels UPDATE entspricht dem Zuweisungsoperator aus Programmiersprachen ( so kann der alte Wert zur Berechnung des neuen genutzt werden). UPDATE book SET price = price * 0.9 WHERE stock < 10; Mehrere Attribute einer Tabelle können gleichzeitig gesetzt werden. Die DELETE-Anweisung: Bei der Löschoperation kann neben der Angabe der Tabelle eine qualifizierende Bedingung angegeben werden, die die zu löschenden Tupeln festlegt: DELETE FROM book_order WHERE status = 1; Definition von Sichten in SQL: Sichtdefinition in SQL: In SQL wird die Sichtdefinition durch die Schlüsselwörter CREATE VIEW, gefolgt vom Namen der Sicht, eingeleitet. Nach dem Sichtnamen steht im einfachsten Fall das Schlüsselwort AS und eine SQLAnfrage, die die Sicht definiert (optionale Teile sind in eckige Klammern gesetzt): CREATE VIEW SichtName [ SchemaDeklaration ] AS SQLAnfrage [ WITH CHECK OPTION ] Das Relationenschema der Sicht, also Name und Datentyp der einzelnen Spalten, wird aus der Anfrage abgeleitet und muß nicht explizit angegeben werden. Allerdings kann optional eine SchemaDeklaration hinter dem Sichtnamen eingefügt werden, in der die Namen der Spalten festgelegt werden können. Dieses Sprachkonstrukt erlaubt auch die Umbenennung von Spalten. WITH CHECK OPTION: Ein weiterer optionaler Teil einer Sichtdeklaration ist die Angabe der Schlüsselwörter WITH CHECK OPTION am Ende der Sichtdefinition. Diese Angabe legt fest, ob Änderungen der Sicht, die den in ihr nicht sichtbaren Teil der Datenbank beeinflussen, in einem Test erkannt und abgewiesen werden sollen. Das folgende Beispiel zeigt eine Sichtdeklaration in SQL: CREATE VIEW book_info AS SELECT b . isbn, b . title, b . price , a . lastname , a . firstname , p . name FROM book b , book_author ba , author a , publisher p WHERE b . isbn = ba . isbn AND ba .a_id = a . a_id AND b . publ_id = p . publ_id; Änderungsoperationen auf Sichten: Soll eine Änderung auf einer Sicht in Änderungen auf der Basisdatenbank umgesetzt werden, müssen eine Reihe von notwendigen oder wünschenswerten Kriterien eingehalten werden: * Effektkonformität: Der Benutzer, der die Änderung auf der Sicht formuliert, soll nach der ausgeführten Änderung auf der Basisdatenbank im nächsten Zustand eine Ausprägung der Sicht erhalten, die dem Effekt entspricht, als wäre die Änderung auf der Sichtrelation direkt ausgeführt worden. Dieses Kriterium fordert in gewissem Sinne die Korrektheit der Transformation. * Minimalität bei Sichtäderungen: Die Basisdatenbank sollte nur minimal geändert werden, um den erwähnten Effekt zu erhalten. * Konsistenzerhaltung bei Sichtänderungen: Die Änderung einer Sicht darf zu keinen Integritätsverletzungen der Basisdatenbank führen. * Zugriffskontrolle bei Sichtänderungen: Wird die Sicht aus Gründen der Zugriffskontrolle eingeführt, darf der bewußt ausgeblendete Teil der Basisdatenbank von Änderungen der Sicht nicht betroffen werden. * Zugriffsrechte: In SQL-Datenbanken wird eine einfache Modellierung von Zugriffsrechten auf Datenbestände unterstützt. Dabei haben Zugriffsrechte den folgenden Aufbau (Subjekt, Objekt, Aktion): (AutorisierungsID, DB-Ausschnitt, Operation) Die einzelnen Punkte haben die folgende Bedeutung: * Autorisierungsidentifikator: (kurz AutorisierungsID) ist eine interne Kennung eines>>Datenbankbenutzers<<. Eine Typische AutorisierungsID kann die Benutzerkennung des Betriebssystems sein. Andere Beispiele wären Kennungen von Zugreifenden Softwaresystemen oder auch Kennungen von Benutzergruppen. * Datenbank-Ausschnitte sind im Relationenmodell gespeicherte Relationen und Sichten, aber auch ganze Datenbanken oder Schemainformationen. * Unter den Operationen sind insbesondere die klassischen Operationen des Lesens, Einfügens, Änderns und Löschens von Datensätzen zu verstehen. Je nach konkret realisiertem System kommen weitere Operationen beispielsweise zur Verwaltung von Systeminformationen (etwa der Datenverteilung) hinzu. * Rechtevergabe mittelsGRANT: Im SQL-Standard werden Rechte mittels der GRANT-Anweisung vergeben, die nach folgendem Muster notiert wird: GRANT Rechte ON Tabelle TO BenutzerListe [WITH GRANT OPTION] Ein Spezialfall der Angabe eines AutorisierungsIDs ist die Angabe PUBLIC. * Trigger: Ein Trigger besteht im wesentlichen aus der Angabe eines Auslösers und der Angabe von auszuführenden Folgeactionen: CREATE TRIGGER . . . ON Opreation : ( Anweisungen ); Als Beispiel für den Einsatz von Triggern betrachten wir die Realisierung eines berechneten Attributs durch Trigger. Das Beispiel basiert auf zwei Relationen, der Relation customer mit einem zusätzlichen Attribut num_orders für die Anzahl der Aufträge und einer zweiten Relation book_order. Der Wert des Attributs num_orders soll vom System verwaltet werden. Hierzu definieren wir einen Trigger wie folgt: CREATE TRIGGER OrderCounter ON INSERTION OF book_order o: UPDATE customer SET num_orders = num_order + 1 WHERE cust_id = NEW o . Cust_id; Neben dem vorgestellten syntaktischen Grundgerüst der Definition von Triggern sind weitere Angaben möglich, die insbesondere den Zeitpunkt der Triggeraktivierung betreffen: * Zeitpunkt der Aktivierung von Trigger: Mittels IMMEDIATE bzw. DEFERRED kann der Zeitpunkt der Aktivierung desTriggers festgelegt werden: sofort nach der aktivierenden Operation oder am Ende der Transaktion. Aktuelle Implementierungen unterstützen oft nur den IMMEDIATE-Modus. Die Angabe FOR EACH ROW aktiviert den Trigger für alle Einzeländerungen einer mengenwertigen Änderung separat. Die Angaben BEFORE und AFTER steurn etwa in der aktuellen Version des kommerziellen Systems Oracle, ob der Trigger direkt vor oder nach einer Änderung aktiviert wird. Mit REFERENCING NEW AS bzw. REFERENCING OLD AS kann eine Tupelvariable an die neu eingefügten bzw. gerade gelöschten (>>alten<<) Tupel einer Relation gebunden werden. Diese >>neuen<< bzw. >>alten<< Tupel werden auch als Elemente der sogenannten Differenzrelation bezeichnet. TEIL_2: JDBC JDBC: Java DataBase Connectivity Das JDBC-Package stellt eine vollständige Schnittstelle zu SQL-Datenbanken zur Verfügung und erlaubt damit den Einsatz von Java-Programmen und Java-Applets (Applet ist eine Bezeichnung für ein Java-Progamm, das in eine HTML-Seite integriert ist.) als Front-Ends für kommerzielle Applikationen. JDBC erfordert die direkte Nutzung von SQL-Anweisungen (Low-Level-API (Application Programmer Interface) ) JDBC besteht einer Menge von Klassen und Schnittstellen, die im Java-Package java . sql zusammengefaßt sind. Die wichtigsten Klassen und Schnittstellen sind hierbei: java . sql . DriverManager: bildet den Einstiegspunkt, indem über diese Klasse Treiber registriert und Verbindungen zur Datenbank aufgebaut werden können. Java . sql . Connection: repräsentiert eine Datenbankverbindung. Java . sql . Statement: ermöglicht die Ausführung von SQL-Anwendungen über eine gegebene Verbindung. Java . sql .ResultSet: verwaltet die Ergebnisse einer Anfrage in Form einer Relation und unterstützt den Zugriff auf einzelne Spalten. JDBC am Beispiel Ablauf einer JDBC-Anwendung Um die verschiedenen Aspekte der JDBC-Programmierung einzugehen, wollen wir uns mit einigen kurze Beispiele befassen. Diese Programmausschnitte sollen die Anwendung der wichtigsten Klassen demonstrieren und gleichzeitig illustrieren, wie die Benutzung von JDBC ist. Der prinzipielle Ablauf einer JDBC-Datenbankanwendung umfaßt die folgenden Schritte: 1. Aufbau einer Verbindung zur Datenbank 2. Senden einer SQL-Anweisung 3. Verarbeiten der Anfrageergebnisse Explizites Laden Voraussetzung für den Aufbau einer Datenbankverbindung ist das Laden eines geeigneten Treibers. Hierzu werden in JDBC zwei Varianten unterstützt. Zum einen kann der Treiber (d.h. die Java-Klasse des Treibers) explizit im Programm geladen werden: Class . forName( “ oracle . jdbc . driver .OracleDriver “); Automatisches Laden Bei der zweiten Variante wird eine Liste von Treibern, jeweils getrennt durch doppelpunkt, in der Systemeigenschaft ( Property ) sql . drivers festgelegt, die vom Treibermanager beim Start automatisch geladen werden. In beiden Fällen müssen sich die Treiber nach dem Laden selbständig beim Treibermanager registrieren. Verbindungsaufbau Der nächste Schritt ist der Verbindungsaufbau. Hierfür stellt der Treibermanager, d. h. die Klasse Java . sql . DriverManager, eine eigene Methode getConnection bereit. Als Argument dieser Methode muß eine URL (Uniform Resource Locator)angegeben werden, die den Verbindungs mechanismus und damit den zu verwendenden Treiber bezeichnet. Zusätzlich sind noch Benutzername und Passwort zu übergeben. Der Aufruf der Methode liefert im Erfolgsfall ein Connection-Objekt. Connection con; String url = “ jdbc:oracle:thin:@antarctica:1521:mydb “ ; con = DriverManager . getConnection (url , “tux“ , “ pingus “ ); Mit dem Connection-Objekt kann nun eine SQL-Anweisung erzeugt werden. Wir wollen zunächst eine einfache Anfrage betrachten, die mit executeQuery ausgeführt wird. String query = “ SELECT title, price, “ + “ stock FROM book “; Statement stmt = con.createStatement ( ) ; ResultSet rs = stmt .executeQuery ( query); Ausführung von Anfragen Die executeQuery-Methode liefert ein ResultSet-Objekt, das die Ergebnisse der Anfrage verwaltet. Die Navigation über die Ergebnismenge erfolgt nach dem Cursor-Prinzip. Die Ergebnismenge kann als eine Tabelle angesehen werden, auf die zunächst zeilenweise und dann spaltenweise zuge- griffen werden kann. In JDBC existiert jedoch kein expliziter Cursor, vielmehr wird die aktuelle Position in der Ergebnismenge vom ResultSet intern verwaltet. Zum Weitersetzen des Cursors wird die Methode next verwendet. Diese Methode liefert so lange den Wert true, bis das Tabellenende erreicht ist. Außerdem ist zu beachten, daß der ResultSet -Cursor zu Beginn vor dem ersten Tupel positioniert ist, d.h., bevor ein Tupel gelesen werden kann, muß die Methode next aufgerufen werden. Zugriff auf Spalten Nachdem der Cursor positioniert ist, können die Spaltenwerte des aktuellen Tupels ausgelesen werden. Hierfür stehen eine Reihe von getXXX-Methoden für die verschiede- nen Datentypen und die korrespondierenden Java-Typen zur Verfügung. So heißt die Methode zum Lesen einer Zeichenkette beispielsweise getString, wobei der Attributtyp der Relation hier VARCHAR und der Java-Typ des Ergebnisses java .Lang . String ist. Der Zugriff auf eine konkrete Spalte der Ergebnisrelation mit Hilfe der getXXX-Methoden erfolgt entweder über den Index der Spalte oder den Spaltennamen. Wichtig ist dabei, daß der Spaltenindex mit 1 beginnt, d. h., die erste Spalte wird durch 1 bezeichnet, die zweite mit 2 usw. Insgesamt kann das Ergebnis der Anfrage wie folgt ausgewertet werden: while (rs . next ( ) ) { String s = rs . getString (1); double d = rs .getDouble (2); int i = rs .getInt (3); System . out .println(s + “ “ + d + “ “ + i ); } Ressourcenfreigabe Zum Abschluß werden die benutzten Ressourcen durch Aufruf der close-Methoden von ResultSet und Statement freigegeben: rs .close ( ); stmt .close ( ); Fehlerbehandlung Fehler werden in JDBC grundsätzlich als Ausnahmen (Exception ) der Klasse SQLException signalisiert und sind daher in geeigneter Weise mit einem try…catch-Block ab- zufangen und zu behandeln.Details zu einem aufgetretenen Fehler können über die Methode getMessage ermittelt werden, die eine Zeichenkette mit der Beschreibung des Fehlers liefert. Der entsprechende Programmausschnitt zur Fehlerbehandlung ist danach wie folgt zu formulieren: try { / / Aufruf von JDBC-Anweisungen, / / die Exception generieren } catch ( SQLException exc) { System . out . println ( “ SQLException: “ + exc . getMessage ( ) ); } Mit Hilfe der bisher vorgestellten Klassen und Methoden können wir nun die erste vollständige JDBC-Anwendung erstellen. Beispiel 1. JDBC-Programm import java . sql .*; public class JdbcDemo1 { public static void main (String [ ] args) { String driverClass = “ oracle . jdbc . driver . OracleDriver “; try { Class . forName ( driverClass ); } catch ( ClassNotFoundException exc ) { System . out . Println ( exc . getMessage ( ) ); System .exit ( 1 ); } try { String url = “jdbc : oracle : thin:@antarctica:1521:mydb“; String query = “ SELECT title, price, “ + “ stock FROM book “; Connection con = DriverManager . getConnection ( url , “ tux“ , “ pingus “ ); Statement stmt = con .createStatement ( ); ResultSet rs = stmt . executeQuery (query); while ( rs . next ( ) ) { String s = rs . getString (1); double d = rs . getDouble (2); int i = rs . getInt (3); System . out . println ( s + “ , “ + d + “ DM, “ + i ); } } catch ( SQLException exc ) { System . out . println (“ SQLException: “ + exc . getMessage ( ) ); } } } Nachdem wir einen ersten Überblick zur Anwendungsentwicklung mit JDBC gewonnen haben, werden wir nun auf wichtige Aspekte im Detail eingehen. JDBC im Detail Verbindungsinformationen Datenbank: mydb; Server: antarctica ; Benutzer: tux ; Paßwort: pingus. Treiber und Datenbankverbindung – Treibermanager Der Treibermanager implementiert in der Klasse java .sql .DriverManager und bildet die Vermittlungsschicht zwischen Anwendung und Datenbanktreiber. Da es in jeder Java-Anwendung nur einen Treibermanager geben kann, sind alle Methoden statisch. Die Methoden lassen sich grob in drei Gruppen einteilen: – – – Registrieren und Laden von Treibern, Herstellen einer Verbindung zur Datenbank, Konfigurieren der Verbindung (Protokollierung, Login- Timeout). Laden von Treibern -Explizit durch Laden der Treiber Hierbei wird der JDBC-Treiber über den Aufruf von Class . forName direkt geladen. Einzige Voraussetzung für diese Methode ist ein Java-Klassenpfad, der die Klassen des benötigten JDBC-Treibers enthält. Da keine weiteren Konfigurationen notwendig sind, ist dies wohl die Methode, die am häufigsten eingesetzt wird. -Über die Systemeigenschaft sql . Driver Die Systemeigenschaft ( Property) umfaßt eine Liste von Treibern, die durch Doppelpunkt getrennt sind. Bei der Initialisierung des Treibermanagers, d. h. beim ersten Aufruf einer Klassenmethode, werden alle dort angegebenen Treiber geladen. Die Eigenschaft kann z. B. beim Aufruf des Java-Interpeters angegeben werden: java -Djsql . drivers = foo . bar . Driver JdbcDemo1 Eine weitere Möglichkeit sind Property-Files, wie z. B. durch den Browser HOTJAVA oder den Applet-Viewer ausgewertet werden. In beiden Fällen ist der Name der Klasse der Dokumentation des Herstellers zu entnehmen. Diese Tabelle zeigt einige wichtige Treiberklassen. Klassennamen einiger JDBC - Treiber DBMS Oracle 8 DB2 MiniSQL MySQL ODBC Treiberklasse oracle . jdbc . driver . OracleDriver COM . ibm . db2 . jdbc . app .DB2Driver com . Imaginary . sql . msql . MsqlDriver org .gjt . mm . mysql . Driver sun . jdbc . odbc . JdbcOdbcDriver Treiber Registrierung von Tribern Wird ein JDBC-Treiber geladen, so muß dieser sich beim Treibermanager registrieren. Hierzu existieren die Methode / / java . sql . DriverManager static void registerDriver (Driver driver) throws SQLException; Die Methode wird normalerweise in einem Initialisierungsblock ( ein static {…. }-Block) der Treiberklasse aufgerufen, der beim Laden der Klasse über Class . forName ausgeführt wird und die Treiberregistrierung vornimmt. Für die Deregistrierung gibt es eine entsprechende Methode deregisterDriver. Beide Methoden sind aber nur für Entwickler für Datenbanktreibern interessant. Weiterhin kann man explizit den Treiber für eine gegebenen JDBC-URL anfordern oder mittels eines Iterators über alle geladenen Treiber navigieren: / / java . sql . DriverManager static Driver getDriver (String url) throws SQLException; static java . Util . Enumeration getDrivers ( ) * Iteraor (Enumeration) Ein für Container-Klassen ( wie Listen, Bäume, Felder, usw. ) sehr nützliches Interface, das eine Möglichkeit bereitstellt, alle Elemente des Containers nacheinander zu referenzieren. Das folgende Beispiel zeigt die Typische Anwendung einerEnumeration e: for (Enumeration e = v . elements ( ) ; e . hasMoreElements ( ) ; ) { System . out .println (e . nextElement ( ) ) ; } Aufbau einer Datenbankverbindung Verbindungsaufbau Nach dem Laden eines Treibers kann die Verbindung zur Datenbank aufgebaut werden. Der Treibermanager stellt hierfür drei verschiedene Methoden bereit, die sich nur in der Form der Parameterlisten unterscheiden: / / java . sql . DriverManager static Connection getConnection ( String url ) throws SQLException; static Connection getConnection ( String url ,java . util . Properties info ) throws SQLException; static Connection getConnection (String url ,String user , String password) throws SQLException; JDBC-URL Alle Methoden erwarten einen Uniform Ressource Locator (URL) als Parameter und liefern ein ConnectionObjekt. Die JDBC-URLs sind im Aufbau an die aus dem Web bekannten URLs angelehnt. Sie haben grundsätzlich folgende Form: jdbc : < subprotocol > : < subname > Hierbei bezeichnen < subprotocol > den Mechanismus zur Datenbankverbindung (z. B. ein konkretes Protokoll) und < subname > ein Identifikator für die Datenbank. Der <subname >-Teil ist außerdem abhängig vom Protokoll und kann auch eine Netzwerkadresse beinhalten. Dagegen müssen für eine Verbindung zu einer OracleDatenbank die Netzwerkadresse (Hostname und Port) des Datenbankservers bzw. Des Listener-Prozesses sowie der Name der Datenbank angegeben werden : jdbc : oracle : thin : @antarctica : 1521 : mydb Beim Aufruf der Methode getConnection versucht der Treibermanager einen Treiber zu finden, der die angegebene URL akzeptiert. Dazu werden alle geladenen Treiber nach einander getestet, d. h ., es wird die Methode acceptsURL- von java . sql .Driver aufgerufen. Mit dem ersten gefundenen Treiber wird die Verbindung zur Datenbank hergestellt. Als weitere Parameter für den Verbindungsaufbau sind insbesondere Benutzername und Paßwort anzugeben, entweder direkt oder als Schlüssel-Wert-Paare in Form eines Property-Objektes. Timeouts und Protokollierung Timeout : Mit den weiteren Methoden der Klasse DriverManager kann die maximale Wartezeit für das Anmelden bei der Datenbank (Timeout) abgefragt bzw. geändert werden. Die Zeitangabe erfolgt dabei jeweils in Sekunden: / / java . sql . DriverManager static int getLoginTimeout ( ) static void setLoginTimeout (int seconds) Tracing : Schließlich kann noch ein Ausgabestrom für die Protokollierung (Tracing) vom Treibermanager sowie den Treibern angegeben werden: / / java . sql . DriverManager static java . io . PrintStream getLogStream ( ) static void setLogStream ( java . io . PrintStream out ) Als Parameter muß hier ein Stream-Objekt eingesetzt werden, wie z. B. System . out oder ein Strom auf einer geöffneten Datei. Auf diese Weise lassen sich Fehlermeldungen protokollieren. Durch Übergabe von null als Parameter kann die Protokollierung wieder abgeschaltet werden. Diese Methoden sind in JDBC2 durch setLogWriter bzw. getLogWriter ersetzt, die einen PrintWriter-Strom als Parameter bzw. Ergebnis verwenden. Connection Ein Connection-Objekt repräsentiert eine Verbindung zur Datenbank. Eine Applikation kann dabei mehrere Verbindungen zu einer oder zu verschiedenen Datenbanken öffnen. Wie bereits beschrieben, wird eine Verbindung durch Aufruf der getConnection-Methode des Treibermanagers erzeugt. Über eine geöffnete Verbindung können SQL-Anweisungen zur Datenbank gesendet und Transaktionsabläufe gesteuert werden.Weiterhin lassen sich Informationen über die Datenbank abfragen. Katalogzugriff: Der Bereich der Datenbank, auf den über die Verbindung zugegriffen werden soll, kann mit den Methoden getCatalog und setCatalog festgelegt bzw. abgefragt werden: / / java . Sql . Connection void setCatalog (String Catalog) throws SQLException; String getCatalog ( ) throws SQLException; Jedes Datenbankobjekt (Relationen, Sichten, Prozeduren, usw.) ist eindeutig über das Tripel (Katalogname, Schemaname, Objektname) identifizierbar, wobei im Normalfall der Katalogname dem Datenbanknamen und der Schemaname dem Benutzernamen entspricht. Für eine Verbindung ist der Katalog auf den Default-Katalog des angemeldeten Benutzers initialisiert, so daß diese Methoden nur selten benötigt werden. Die exakte Bedeutung des Katalognamens ist jedoch vom DBMS abhängig. Mit der Methode setReadOnly kann angegeben werden, daß nachfolgend nur Leseoperationen ausgeführt werden sollen. / / java . sql . Connection void setReadOnly ( boolean readOnly ) throws SQLException; boolean isReadOnly ( ) throws SQLException; Allerdings bedeutet das nicht, daß Schreiboperationen damit verboten sind. Vielmehr ist es ein Hinweis für den Treiber und das DBMS und z. B. Optimierungen vorzuneh men. Der aktuelle Modus der Verbindung kann über isReadOnly abgefragt werden. Für einen Treiber, der keine Schreibinformationen unterstützt, wird hier entsprechend auch true zurückgegeben. Verbindungsabbau: Eine aktive Verbindung zur Datenbank wird mittels der close-Methode beendet. / / java . sql . Connection void close ( ) throws SQLException; boolean isClosed ( ) throws SQLException; Obwohl das Schließen auch automatisch bei der Freigabe des Objektes durch den Garbage Collector erfolgt, ist das explizite Schließen die empfohlene Variante. So können Ressourcen des DBMS sofort freigegeben werden, wenn sie nicht mehr benötigt werden. Das automatische Schließen wird dagegen unter Umständen (z. B. Hauptspeicher) erst viel später erfolgen. Der Zustand einer Verbindung ist wiederrum über die Methode isClosed abgefragbar. Transaktionssteuerung Der Begriff der Transaktion ist als elementare Ausführungseinheit zur Überführung der Datenbank von einem konsistenten Zustand in einen veränderten, konsistenten Zustand unter Einhaltung des ACID-Prinzips vorgestellt. Unter ACID versteht man: Atomarität = Ununterbrechbarkeit. Konsistenz = Integritätserhaltung. Isolation = Isolation. Dauerhaftigkeit = Persistenz der Ergebnisse. Die Kommandos zur Steuerung des Transaktionsablaufes sind als Methoden der Schnittstelle Connection definiert: / / java . Sql . Connection void commit ( ) throws SQLException; void rollback ( ) throws SQLException; Mit der Methode commit wird angezeigt, daß die Transaktion erfolgreich abgeschlossen werden soll und alle Än derungen permanen t in die Datenbank zu schreiben sind. Mit rollback wird die aktive Transaktion abgebrochen alle Änderungen, die im Rahmen dieser Transaktion durchgeführt wurden, werden rückgängig gemacht.Ein explizites >> Begin of Transaction << existiert dagegen nicht. Transaktionen werden - wie in relationalen Datenbanksystemen üblich - automatisch mit dem ersten Datendefinitions- bzw. Datenmanipulationsbefehl bzw. mit dem Ende oder Abbruch der vorangegangenen Transaktion gestartet. Auto-Commit: Wird eine neue Verbindung zur Datenbank hergestellt, so befindet sich diese im Auto-Commit-Modus, d. h., nach jeder Anweisung wird automatisch ein Commit ausgeführt und eine Transaktion umfaßt jeweils nur eine Anweisung. Dieser Modus kann mit der Methode setAutoCommit ein- (mit true als Parameter) bzw. ausgeschaltet (false) werden: / / java . Sql . Connection void setAutoCommit (boolean enable) throws SQLException; boolean getAutoCommit ( ) throws SQLException; Ohne Auto-Commit wird die Transaktion erst mit einem expliziten Commit bzw. Rollback abgeschlossen. Durch das Aabschalten des Auto-Commit-Modus lassen sich meh -rere Anweisungen zu einer Transaktion zusammenfassen. Außerdem können so Änderungen bis zum Ende der Transaktion rückgängig gemacht werden. Das folgende Beispiel zeigt die Nutzung von Transaktionen beim Einfügen mehrerer Datensätze, wobei beim Auftreten eines Fehlers alle Änderungen zurückgenommen werden. Die Fehlermeldung wird außerdem durch Weiterleiten der Ausnahme an den Aufrufer propagiert: try { stmt = con . createStatement ( ); con .setAutoCommit ( false) ; stmt . executeUpdate ( “ INSERT INTO book “ + “ VALUES ( ‘ 3-608--93421-9 ‘ , ‘ Otherland ‘ , “ + “ 2, 1, 49.90, 20 ) “ ); stmt . executeUpdate ( “ INSERT INTO author “ + “ VALUES (1, ‘Tad‘ , ‘Williams‘ )“ ); stmt . executeUpdate ( “ INSERT INTO book_author “ + “ VALUES (‘3- 608-93421-9‘ , 1)“); con . Commit ( ); } catch (SQLException exc) { con . Rollback ( ); throws exc; } Für eine Reihe von Anwendungen ist die strenge Einhaltung der ACID-Eigenschaften jedoch zu restriktiv und zu Laufzeiteinbußen. Ausführung von SQL-Anfrage In JDBC wird jede SQL-Anweisung durch ein StatementObjekt gekapselt. Dieses Objekt sendet die Anweisung zur Datenbank, liefert das Ergebnis zurück und verarbeitet Parameter, die der Anweisung übergeben werden. Es werden drei Formen von Statements unterstützt: Arten von Statements: java . sql .Statement als Basisschnittstelle für alle anderen Formen erlaubt die Verarbeitung einfacher Anweisungen ohne Parameter. Java . sql .PreparedStatement kapselt eine vorkompilierte Anweisung und wird insbesondere dann eingesetzt, wenn eine Anweisung mehrfach und mit verschiedenen IN-Parametern ausgeführt werden soll. Java . sql .CallableStatement ermöglicht den Aufruf von gespeicherten Prozeduren mit IN- und OUT-Parametern. Erzeugen von Anweisungen - Erzeugen von Statements Ein Statement-Objekt wird grundsätzlich über die Connection-Schnittstelle erzeugt. Hierzu ist für jede Anweisungsform eine eigene Methode definiert: / / java . sql . Connection Statement createStatement ( ) throws SQLException; PreparedStatement preparedStatement (String sql) throws SQLException; CallableStatement prepareCall ( String sql) throws SQLException; Ausführung von Anweisungen Die Statement- Schnittstelle definiert als Basisschnittstelle die Methoden für alle Anweisungsformen. Zur Ausführung der Anweisungen stehen drei execute-Methoden zur Verfügung. / / java . sql . Statement ResultSet executeQuery ( String sql) throws SQLException; int executeUpdate (String sql) throws SQLException; boolean execute (String sql) throws SQLException; -Ausführung von SELECT-Anweisungen Mit der Methode executeQuery wird eine SELECT-Anweisung ausgeführt. Diese Methode liefert ein ResultSet-Objekt mit dem Anfrageergebnis: Statement stmt = con . createStatement ( ); ResultSet rs = stmt . executeQuery (“ SELECT * FROM book“); Ausführung von DDL- und DML-Anweisungen DDL-(Database Definition Language) und DML-(Database Manager Language) Anweisungen, d. h. CREATE TABLE oder INSERT, UPDATE und DELETE, werden über die Methode executeUpdate zur Datenbank gesendet. DMLAnweisungen manipulieren Tupel einer Relation, das Ergebnis ist demnach die Anzahl der betroffenen Tupel. Für DDL-Anweisungen ist dagegen der Rückgabewert immer 0. Int nrows = stmt . executeUpdate (“ DELETE FROM customer WHERE cust_id = 256 “); Anweisungen mit mehreren Ergebnisse Eine besondere Behandlung erfordert Anweisungen, die mehrere Ergebnisse liefern. Dies kann z.B. bei gespeicherten Prozeduren auftreten, die mehrere UPDATE-Operationen oder mehrere Anfragen ausführen. Für diese (eher selte nen) Fälle wird die Methode execute verwendet. Diese Methode liefert den Wert true, wenn das Ergebnis der Anfrage ein ResultSet-Objekt ist ( z. B. nach einem SELECT ) und false, wenn es ein Integer-Wert ist (z. B. die Anzahl der betroffenen Tupel nach einem UPDATE). Das entsprechende Ergebnis wird mit den folgenden Methoden ermittelt: / / java . sql .Statement ResultSet getResultSet ( ) throws SQLException; int getUpdateCount ( ) throws SQLException; boolean getMoreResults ( ) throws SQLException; Ein ResultSet-Objekt wird danach durch getResultSet geliefert, die Anzahl der betroffenen Tupel mit getUpdateCount Ob weitere vorliegen und von welcher Art diese sind, kann mit getMoreResults erfragt werden. Diese Methode liefert true, wenn das nächste Ergebnis ein ResultSet-Objekt ist, und false für einen Integer-Wert. Das jeweils nächste Ergebnis wird durch getResultSet bzw. geUpdateCount ermittelt, wobei getResultSet den Wert null zurückgibt, wenn das Ergebnis ein Integer-Wert ist und getUpdateCount einen der folgenden Werte liefert: >0 für eine UPDATE-, INSERT- oder DELETE-Operation 0 für eine DDL-Operation bzw. eine DML-Operation, die keine Tupel verändert hat, und -1, wenn das Ergebnis ein ResultSet-Objekt ist oder keine weiteren Ergebnisse vorliegen. Damit sind alle Ergebnisse ermittelt, wenn die Bedingung gilt: (getMoreResults ( ) = = false) & & (getUpdateCount ( ) = = -1) Das folgende Beispiel demonstriet die verwendung dieser Methoden beim Aufruf einer gespeicherten Prozedur, die mehrere ResultSets zurüchgeben kann.: CallableStatement stmt = con. prepareCall (“ { call MultipleResultsProc } “); stmt. execute ( ) ; while (stmt. getMoreResults ( ) ) {ResultSet rs = stmt. getResultSet ( ); / / ResultSet auswerten } Parameter für Ausführung von Anweisungen Weiterhin lassen sich die Ausführung von Anweisungen sowie die Ergebnisgröße beeinflussen. Mit setQueryTimeout wird die maximale Zeit des Wartens auf die Ausführung in Sekunden festgelegt, wobei der Default-Wert von 0 für unbegrenztes Warten steht. Ein Überschreiten des gesetzten Limits wird durch eine Exception signalisiert. Die Maximale Anzahl von Tupeln im Anfrageergebnis wird mit der Methode setMaxRows vordefiniert. Auch hier ist der Default-Wert 0 und bedeutet keine Einschränkung der Anzahl. Wird dagegen ein anderer Wert angegeben, so werden nur die entsprechende Anzahl von Tupeln ausgelesen, der Rest wird ignoriert. Die Maximale Größe einer Spalte in Bytes kann mit setMaxFieldSize festgelegt werden, wobei dies nur für die SQL-Typen VARBINARY, LONGVARBINARY, CHAR, VARCHAR und LONGVARCHAR relevant ist. Überschreiten die Daten das festgelegte Limit, so wird der Rest abgeschnitten. Der Default-Wert von 0 bedeutet auch hier keine Beschränkung. / / java . sql . Statement int getMaxFieldSize ( ) throws SQLException; void setMaxFieldSize (int max) throws SQLException; int getMaxRows ( ) throws SQLException; void setMaxRows (int max) throws SQLException; int getQueryTimeout ( ) throws SQLException; void getQueryTimeout (int secs ) throws SQLException; Abbruch von Transaktionen Schließlich läßt sich die Ausführung einer Anweisung über die Methode cancel durch einen anderen Thread (Ausfüh rungsfaden)abbrechen, wenn Treiber und DBMS dies unterstützen. / / java . sql . Statement void cancel ( ) throws SQLException; void close ( ) throws SQLException; Nachdem eine Anweisung ausgeführt und die Ergebnisse ermittelt wurden, sollten damit verbundene Ressourcen durch Aufruf der close-Methode freigegeben werden. Auch hier gilt die bereits beim Connection-Objekt gegebene Emp -fehlung der expliziten Freigabe gegenüber dem automatischen Schließen durch den Garbage Collector („ Speicherfreigeber “). Abbildung von SQL-Typen in java Da SQL und Java jeweils eigene Typsysteme besitzen, muß eine Abbildung zwischen beiden definiert werden. Das Pro -blem der Typabbildung tritt in JDBC an drei Stellen auf: beim Zugriff auf Spaltenwerte eines Tupels mit den getXXX-Methoden der Schnittstelle ResultSet, bei der Übergabe von Parametern an ein PreparedState -ment mit den setXXX-Methoden sowie beim Zugriff auf die OUT-Parameter eines CallableStatements. SQL - Typbezeichner: Darüber hinaus existiert noch eine weitere Schwierigkeiten: Einige SQL-Typen werden von den DBMS-Herstellern teilweise unterschiedlich bezeichnet, insbesondere die Datentypen zum Speichern großer Binärdaten. Zur Überwindung dieser Inkompatibilitäten sind in JDBC zunächst einige generische Typbezeichner definiert. Diese sind in der Klasse java . sql . Types als Konstanten zusammengefaßt: CHAR, VARCHAR, LONGVARCHAR für Zeichenketten BIT für Bitwerte und BINARY,VARBINARY, LONGVAR -BINARY für Binärfelder, TINYINT, SMALLINT, BIGINT und INTEGER für Integer-Werte, REAL, FLOAT und DOUBLE für Gleitkommawerte, DATE, TIME, TIMESTAMP für Datums- und Zeitwerte. Diese Konstanten werden in JDBC überall dort verwendet, wo SQL-Typen anzugeben sind, also z. B. beim Registrieren von OUT-Parametern für gespeicherte Prozeduren. Die Umsetzung in die DBMS-spezifischen Typen erfolgt wenn notwendig - durch den Treiber. Beim Auslesen von Werten aus einem ResultSet-Objekt bzw. bei der Übergabe von IN-Parametern an ein Prepared -Statement sind dagegen die getXXX- bzw. setXXX-Methoden zu verwenden. Hierbei findet ebenfalls eine Konvertierung zwischen SQL- und Java-Typ durch den Treiber statt. Die verwendete Abbildungsvorschrift ist in nachfolgenden Tabelle dargestellt. Die getXXX- und setXXX-Methoden sind entsprechend dem Java-Typ bezeichnet, wobei für die Byte-Felder getBytes bzw. setBytes verwendet wird. Der in der Tabelle angegebene Java-Typ ist auch der Typ, der beim Aufruf von getObject zurückgegeben wird. Datums- und Zeittypen: Zur Repräsentation der Datums- und Zeittypen sind in JDBC drei zusätzliche Klassen definiert: Date, Time und Timestamp. Diese Klassen kapseln im wesentlichen die Funktionalität der Klassen aus java . util und fügen nur noch die Behandlund der Escape-Syntax hinzu. So kann ein Date-Objekt für den 18. Februar 1999 über den normalen Konstruktoraufruf erzeugt werden: Date d = new Date ( 99 , 2 , 18 ); Das erste Argument bezeichnet dabei die Jahreszahl minus 1900, d. h., für das Jahr 2000 ist dementsprechend der Wert 100 anzugeben. Das zweite und dritte Argument sind Monat und Tag. Alternativ dazu kann das Date-Objekt auch durch Aufruf der statischen Methode valueOf erzeugt werden: Date d = Date . valueOf ( “ 1999 - 02 - 18 “ ); Diese Methode ist eine Zeichenkette mit dem gewünschten Datum in Escape-Syntax als Argument zu übergeben. In Ähnlicher Weise wird die Klasse java . sql .Time zur Repräsentation der Uhrzeit (SQL-Typ Time) verwendet. Hier ist das Format der Zeitangabe hh : mm : ss. Tabelle : Abbildung zwischen Java- und JDBC-Typen . JDBC -Typ CHAR VARCHAR LONGVARCHAR NUMERIC DECIMAL BIT TINYINT SMALLINT Java-Typ String String String java . math . BigDecimal java . math . BigDecimal boolean byte short INTEGER BIGINT REAL FLOAT DOUBLE BINARY VARBINARY LONGVARBINARY DATE TIME TIMESTAMP int long float double double byte [ ] byte [ ] byte [ ] java . sql . Date java . sql . Time java . sql . Timestamp BEISPIELANWENDUNG MIT JDBC Beispiel 1: Eintragen einer Bestellung Die Methode createOrder zum Anlegen einer Bestellung ist etwas aufwendiger. Hier ist zunächst die Bestellung als ein Tupel in die Relation book_order einzutragen und für jeden Artikel dieser Bestellung zusätzlich noch ein Tupel in die Relation order_item einzufügen. Die einzelnen Artikel werden dabei durch Einlesen der ISBN sowie der Anzahl vom Benutzer erfragt. Da die Ausführung der einzelnen Anweisungen aufgrund von Verletzungen der Integritätsbedingungen (z. B. wenn ISBN oder Kundennummer nicht existieren) fehlschlagen kann, muß der gesamte Ablauf als eine Transaktion realisiert werden. Hierzu wird das Auto-Commit zu Beginn ausgeschaltet und im Fehlerfall ein Rollback ausgeführt. Erst wenn die Bestellung voll- ständig erstellt werden konnte, wird das abschließende Commit gesendet. Programm: void createOrder (int custId) throws SQLException { boolean ready = false; preparedStatement stmt1, stmt2; try { con . setAutoCommit (false); / / Bestellung Eintragen stmt1 = con . prepareStatement ( “ INSERT INTO book_order “ + “ VALUES ( order_seq. NEXTVAL, ?, “ + “ SYSDATE, 0 ) “); stmt1 . setInt (1, custId); stmt1 . executeUpdate ( ); stmt2 = con . prepareStatement ( “ INSERT INTO order_item “ + “ VALUES (order_seq . CURRVAL, ?, ?)“ ); do{ / / ISBN erfragen String isbn = Utils . readString ( “ isbn: “); if ( isbn = = null | | isbn . Length ( ) = = 0 ) ready = true; else { / / Anzahl erfragen int num = Utils . readInt ( “ number: “ ); if (num > 0) { / / Bestellposition eintragen stmt2 . setString ( 1, isbn ); stmt2 . setInt ( 2, num); stmt2 . ExecuteUpdate ( ); } } } while (! ready); con . commit ( ); } catch ( SQLException exc ) { con . Rollback ( ); throws exc;} }