Java für Fortgeschrittene Proseminar im Sommersemester 2009 JDBC und Java DB Dhyan Blum Technische Universität München 13. Juli 2009 Zusammenfassung Das Thema dieser Seminararbeit ist die Datenbankschnittstelle JDBC, sowie die freie Datenbank Java DB. Schwerpunkte sind somit verschiedene Aspekte des Zugriffs auf relationale Datenbanken aus Java-Anwendungen heraus, sowie eine Einführung in die vollständig in Java geschriebene Datenbank Java DB. Ein weiterer Aspekt ist die nähere Betrachtung von Java DB als eingebettete Datenbank und ihre Anwendungsmöglichkeiten als solche. 1 Inhaltsverzeichnis 1 Einleitung 2 2 Exkurs: Relationale Datenbanken 3 3 JDBC 3.1 Der Vorläufer: ODBC . . . . . . . . 3.2 Geschichte: Java und CGI . . . . . 3.3 Einführung in JDBC . . . . . . . . 3.4 Konzeptioneller Aufbau von JDBC 3.5 JDBC Treiber . . . . . . . . . . . . 3.6 Klassenstruktur von JDBC . . . . . 3.7 Einführung in die Praxis . . . . . . 3.8 Erweiterte Funktionen von JDBC . 3.9 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 5 5 5 6 6 8 9 14 16 4 Java DB 16 4.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 4.2 Architektur von Java DB . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 4.3 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 5 Literatur 20 1 Einleitung Motivation Wann immer Daten gesammelt und archiviert werden, sind wir an ihrer einfachen Verwaltung interessiert. Wir wünschen uns eine Ordnung, die auch nach längerer Zeit sicherstellt, dass wir aus einer unüberschaubaren Menge von Informationen gezielt einzelne Daten heraussuchen, modifizieren oder auch entfernen können. Bei kritischen Daten stellen wir zudem die Forderung nach ihrer Sicherheit und Unversehrtheit. In größeren Systemen möchten wir häufig mehreren Nutzern den gleichzeitigen, den wechselseitig ausgeschlossenen oder schlicht den eingeschränkten Zugriff auf bestimmte Daten ermöglichen. Diese und weitere Aspekte der Datenspeicherung (Schlagwörter wie Redundanz, Inkonsistenz, Integrität, usw. spielen hier eine Rolle), haben die Entwicklung und schließlich den flächendeckenden Einsatz von Datenbankmanagementsystemen (DBMS) initiiert. Diese Systeme versprechen die meisten Probleme in der Domäne der Datenverwaltung auf praktikable Weise zu lösen. Problemstellung Zeitlich betrachtet hat Java erst spät den Zugang zur Datenbank gefunden: Die erste Version von Suns Datenbankschnittstelle JDBC erschien im Januar 1997 und erst die 2 zweite API-Version wurde ab Mai 1998 in Form des Pakets java.sql.* mit J2SE (Java 2, Versionsnummer 1.2) ausgeliefert. Auch beim Erlernen der Sprache steht der Zugriff auf Informationen einer Datenbank nicht unbedingt ganz oben auf der Prioritätenliste. Im Bedarfsfall ist das naheliegendste Problem für den Java-Anwendungsentwickler also, wie er mit seinem Programm eine Datenbank ansprechen und ihre Daten für sich nutzbar machen kann. Verbindungen herstellen, Daten anfragen und Ergebnisse verarbeiten sind die zentralen Probleme hierbei. Daneben stellt sich die Frage nach dem einzusetzenden Datenbanksystem und dessen Eignung für unterschiedliche Einsatzzwecke. Überblick Die folgende Seminararbeit soll eine Einführung in die angesprochene Thematik bieten. Zu Beginn folgt ein kurzer Ausflug in die Welt der (relationalen) Datenbanken, anschließend ein wenig Geschichte zur Entwicklung und zu den Vorläufern der Datenbankschnittstelle JDBC und eine praktische Einführung in selbige. Im zweiten Teil der Arbeit wird die vollständig in Java geschriebene Datenbank Java DB eingeführt und an ihrem Beispiel das Konzept eingebetteter Datenbanken diskutiert. 2 Exkurs: Relationale Datenbanken Aufbau und Konzept Relationale Datenbanken dienen zur Datenverwaltung in Computersystemen. Grundkonzept ist die Relation, die eine mathematische Beschreibung einer Tabelle darstellt. Für das Abfragen und Manipulieren von Daten wird die Structured Query Language (SQL) verwendet. Abseits der mathematischen Hintergründe kann man sich eine solche Datenbank schlicht als Sammlung von benannten Tabellen (Relationen) vorstellen. Jede Zeile einer solchen Tabelle stellt einen Datensatz (ein Tupel) dar, jeder Datensatz besteht aus einer Reihe von Werten (Attributen), die in den Spalten der Tabelle organisiert sind. Anzahl, Name und Typ aller Spalten bestimmen die Struktur der Tabelle, das sogenannte Relationenschema. Eine reale, mit Daten gefüllte Tabelle wird als Ausprägung bezeichnet. Schema und Ausprägung verhalten sich im übertragenen Sinne wie Klasse und Objekt. Spaltentypen können z.B. Integer, String oder Boolean sein. Um einzelne Datensätze identifizieren zu können, ist es möglich, eine Spalte als Schlüssel zu definieren bzw. mehrere Spalten zu einem Schlüssel zusammenzufassen, der das Tupel dann eindeutig identifiziert. Ist kein eindeutiger Schlüssel möglich, wird häufig ein künstlicher Schlüssel in Form einer fortlaufende Identifikationsnummer erzeugt. Tabelle 1 zeigt ein Beispiel, in dem die Spalte mit Namen id“ der Schlüssel ist. ” Anfragen mit SQL Für Anfragen an relationale Datenbanken hat sich die Datenbanksprache SQL als absoluter Standard etabliert, sie wird von allen gängigen Datenbanksystemen unterstützt. 3 Tabelle 1: personen“ ” id 1 2 3 4 ... vorname Hans Gabriele Erwin Hans ... nachname Meier Huber Schuster Meier ... wohnort Augsburg München München Augsburg ... Da eine Einführung in SQL den Rahmen dieser Arbeit sprengen würde, sei lediglich folgendes Beispiel gegeben: Angenommen der Name Gabriele Huber“ wäre innerhalb der ” Tabelle eindeutig und wir wollten die id und den wohnort dieser Person herausfinden, so müssten wir folgende SQL-Anfrage formulieren: SELECT id , wohnort FROM personen WHERE vorname = ’ Gabriele ’ AND nachname = ’ Huber ’; In der Konsole würde z.B. eine MySQL-Datenbank diese Anfrage wie folgt beantworten: mysql > SELECT id , wohnort FROM personen WHERE ... + - - - -+ - - - - - - - - -+ | id | wohnort | + - - - -+ - - - - - - - - -+ | 2 | München | + - - - -+ - - - - - - - - -+ 1 row in set (0.00 sec ) Hätten wir stattdessen nach Personen mit wohnort = ’München’ gefragt, so würde das Ergebnis mindestens zwei Datensätze umfassen. Einfache SQL-Anfragen sind also recht intuitiv. SQL kann jedoch auch für sehr viel mächtigere Fragestellungen verwendet werden und spätestens dann haben Textdateien als Speicherstruktur klar das Nachsehen. Dieses Beispiel zeigt uns die grundsätzliche Problemstellung auf: Wie setzen wir SQLAnfragen aus Java heraus an die Datenbank ab, und wie kommen wir an die Ergebnisse? 3 JDBC In diesem Kapitel werden die Entstehungsgeschichte, der konzeptionelle Aufbau und der praktische Einsatz der Datenbankschnittstelle JDBC erläutert. Abschließend wird 4 ein Blick auf die erweiterten Funktionalitäten von JDBC geworfen und dann ein Fazit gezogen. 3.1 Der Vorläufer: ODBC Die von Microsoft entwickelte Open Database Connectivity (ODBC [7]) ist eine Programmierschnittstelle, die den einheitlichen Zugriff auf relationale und andere Datenbanken mit SQL ermöglicht. ODBC 1.0 wurde 1992 veröffentlicht, ist seit Windows 2000 in alle Microsoft-Betriebssysteme integriert und inzwischen auch für Unix verfügbar. Vergleichbare Abstraktionsschichten für den Datenzugriff bieten auch DBI (Database Independent) für die Programmiersprache Perl oder die Erweiterung PDO (PHP Data Objects) für PHP. Solange man sich auf standardisierte SQL-Konstrukte beschränkt und auf proprietäre SQL-Erweiterungen einzelner Datenbankhersteller verzichtet, erlaubt ODBC die Anwendungsentwicklung unabhängig von der letztlich verwendeten Datenbank. Somit wird auch das transparente Austauschen der Datenbank zu einem späteren Zeitpunkt möglich, ohne das eigentliche Programm verändern zu müssen. Es sind sowohl Zugriffe auf lokale wie auch auf entfernte Datenbanken möglich. Damit ODBC verwendet werden kann, muss die eingesetzte Datenbank einen passenden ODBC-Treiber zur Verfügung stellen. Da ODBC vielerorts als Standard etabliert ist, bieten alle größeren Datenbankhersteller entsprechende Treiber für diverse Programmiersprachen an, darunter für C++, Perl und PHP. Ein Nachteil von ODBC ist naturgemäß die geringere Performanz im Vergleich zu nativen Datenbankanfragen. 3.2 Geschichte: Java und CGI Bevor eine ähnliche Technik wie ODBC für Java existierte, mussten Datenbankanfragen zunächst an ein serverseitiges CGI-Programm weitergereicht werden, welches dann auf die Datenbank zugriff und schließlich die Anfrageergebnisse zurück an die JavaAnwendung übergab. Dieser Ansatz war nicht nur langsam, er erforderte auch Kenntnisse zweier verschiedener Programmiersprachen und begünstigte so fehleranfällige und schwer zu wartende Anwendungen[5]. 3.3 Einführung in JDBC 1996 wurde in Anlehnung an ODBC mit einem vergleichbaren Projekt für Java begonnen, der Java Database Connectivity (JDBC [10]). Im Januar 1997 wurde die erste Version der JDBC API spezifiziert und über die Jahre folgten weitere Versionen, bis schließlich 2006 die heute noch aktuelle Version 4.0 veröffentlicht wurde[11]. Analog zu ODBC ist JDBC eine Javaschnittstelle für den einheitlichen Zugriff auf relationale Datenbanken verschiedener Hersteller. Als solche erlaubt sie das Einbetten der Datenbanksprache SQL in den Quelltext der Anwendung, obwohl SQL nicht zum eigentlichen Sprachumfang von Java gehört und somit als Fremdkörper anzusehen ist. Die Einbettung von SQL ist deshalb 5 notwendig, weil die Sprache allein keine Entwicklung komplexer Datenbankanwendungen gestattet. JDBC erfüllt im wesentlichen drei Aufgaben: ∙ Verbindungsaufbau zur Datenbank ∙ Absetzen von SQL-Anfragen ∙ Verarbeitung der Ergebnisse Die Stärke der Schnittstelle liegt darin, dass eine JDBC-basierte Java-Anwendung Clienten auf einer beliebigen Plattform mit einer beliebigen Datenbank verbinden kann. 3.4 Konzeptioneller Aufbau von JDBC Abbildung 1 illustriert den konzeptionellen Aufbau von JDBC: Die Java-Anwendung ruft Funktionen der JDBC API auf (Aufrufschnittstelle, Call Library Interface“), die einen ” Datenbanktreiber lädt und über diesen mit der zugehörigen Datenbank kommuniziert. Der DriverManager ist die Basis der JDBC-Architektur und verwaltet die auf dem System installierten JDBC-Treiber (vgl. Kap. 3.5). Über die getConnection()-Methode des DriverManagers wird die Verbindung zu einer spezifischen Datenbank hergestellt. Die Klasse Connection repräsentiert eine solche Verbindung. In ihrem Kontext werden SQL-Anfragen ausgeführt und Ergebnisse zurückgeliefert, wobei SQL-Anweisungen in Form von Zeichenketten an entsprechende API-Methoden übergeben werden. Falls die Datenbank eine Ergebnismenge zurückliefert, kann über andere API-Methoden auf diese zugegriffen werden. 3.5 JDBC Treiber Um eine Datenbank mit JDBC ansprechen zu können, muss diese einen Treiber bereitstellen, der die JDBC-Spezifikation implementiert. Solche Treiber werden in der Regel vom Hersteller des Datenbanksystems zur Verfügung gestellt. Eine Liste aller 221 derzeit bekannten Treiber wird online von Sun angeboten[12]. JDBC kennt vier verschiedene Typen von Treibern[13] (vgl. Abb. 1), die im Folgenden beschrieben werden: Typ 1: JDBC-ODBC bridge Datenverbindungen werden über eine ODBC-Schnittstelle aufgebaut. JDBC-Anweisungen werden an die entsprechenden Funktionen des ODBC-Treibers übergeben, der im nativen Code des Betriebssystems auf dem Client vorliegt und die eigentliche Verbindung zur Datenbank herstellt. Der ODBC-Treiber nutzt die Aufrufschnittstelle (CLI) des Herstellers, d.h. die nativen ODBC-Bibliotheken des Datenbanksystems müssen auf dem Client installiert sein. Dieser Treibertyp ist für die meisten Datenbanksysteme verfügbar, kann aber z.B. nicht von Applets genutzt werden, die standardmäßig keinen Zugriff auf lokale Ressourcen des Clients haben. Der Treiber ist daher für den realen Einsatz nicht zu empfehlen, die Abhängigkeit von nativen Bibliotheken beeinträchtigt die Portabilität der Anwendung. 6 Abbildung 1: Konzeptioneller Aufbau von JDBC Typ 2: Native-API partly Java technology-enabled Der Treiber ist teilweise in Java geschrieben und bildet JDBC-Aufrufe eigenständig auf native Anweisungen des Datenbank-Clients ab, der die Kommunikation zur Datenbank herstellt. Nach wie vor müssen die nativen Bibliotheken des Datenbanksystems auf dem Client installiert sein, die Portabilität wird eingeschränkt. Der Zugriff über diesen Treibertyp ist allerdings sehr schnell. Typ 3: Net-protocol fully Java technology-enabled Plattformunabhängiger Treiber (reine Java-Bibliothek), der JDBC-Anfragen in ein datenbankunabhängiges Protokoll übersetzt und dieses an einen Middleware-Server überträgt. Dort folgt die Übersetzung in spezifische Datenbankanweisungen. Ein solcher Server ist in der Lage, alle angeschlossenen Java-Clients mit jeweils unterschiedlichen, auf dem Middleware-Server laufenden, Datenbanken zu verbinden. Typ 4: Native-protocol fully Java technology-enabled Der Treiber ist vollständig in Java geschrieben und implementiert ein Netzwerkprotokoll für spezifische Datenbanksysteme. JDBC-Anfragen werden direkt in entsprechende Befehle des jeweiligen Datenbankservers übersetzt und über das Protokoll an diesen übertragen. Der Client ist direkt mit der Datenbank verbunden, der Treiber somit schneller, aber weniger flexibel als ein Typ-3-Treiber. 7 Die meisten Datenbankhersteller bieten Treiber vom Typ 3 oder 4 an. Für diese Ausarbeitung ist ausschließlich der Typ-4-Treiber von Interesse. Da standardmäßig lediglich JDBC-ODBC Brücken vorhanden sind, müssen andere Treiber in der Regel selbst bezogen und installiert werden. Am Beispiel von MySQL bedeutet dies, den JDBC Typ4-Treiber Connector/J“ von der Herstellerwebsite herunterzuladen und die Bibliothek ” mysql-connector-java-[version]-bin.jar dem CLASSPATH hinzuzufügen. 3.6 Klassenstruktur von JDBC Der Kern der JDBC API ist das Paket java.sql.*, welches mit dem JDK ausgeliefert wird. Wichtige Teile des Pakets java.sql.* sind die Interfaces CallableStatement, Connection, Driver, PreparedStatement, ResultSet und ResultSetMetaData. Unter den Klassen ist der bereits erwähnte DriverManager von Bedeutung. Abbildung 2 Abbildung 2: Die wichtigsten JDBC-Klassen und -Interfaces zeigt ein reduziertes Klassendiagramm von JDBC: Das Interface Driver ist hier nicht eingezeichnet, da sich ein Treiber nach dem Laden selbstständig beim DriverManager registriert und der Entwickler selbst keinen direkten Kontakt zu Driver hat. Die übrigen Klassen werden in 3.7 näher beschrieben. Doch an Hand des Diagramms können wir schon jetzt die typischen Schritte bei der Entwicklung einer einfachen Datenbankanwendung mit JDBC nachvollziehen: 1. Importieren des Pakets java.sql.* und Laden eines Treibers mit Class.forName("Treibername") 2. Herstellen einer Verbindung mit getConnection() 3. Erstellen eines Statement-Objekts mit createStatement() 4. Ausführen einer SQL-Anfrage mit executeQuery() 8 5. Einholen der Ergebnisse mit getResultSet() und den entsprechenden get<Type>()Methoden für einzelne Spaltwerte 6. Schließen von ResultSet, Statement und Connection 3.7 Einführung in die Praxis Die wichtigsten JDBC-Klassen und ihre praktische Verwendung werden im Folgenden näher beschrieben. Schritt 1: Laden des Datenbanktreibers Der erste Schritt besteht darin, das JDBC-Paket java.sql.* zu importieren und einen Treiber zu laden. Für gewöhnlich werden Treiber mittels Class.forName geladen, was automatisch eine Instanz des Treibers erzeugt und beim DriverManager registriert. In folgendem Beispiel wird der Typ-4-Treiber von MySQL geladen: import j a v a . s q l . ∗ ; ... C l a s s . forName ( ”com . mysql . j d b c . D r i v e r ” ) ; ... Schritt 2: Herstellen einer Verbindung Die Methode getConnection(String url, String user, String pass) des DriverManagers versucht eine Datenbankverbindung zur angegebenen URL mit dem Benutzer user“ und dem Passwort pass“ herzustellen. Bei Erfolg gibt sie ein Objekt vom Typ ” ” Connection zurück. Das Argument url hat die Form jdbc:subprotocol:subname, wobei subprotocol für den Datenbanktyp (z.B. derby“ für Java DB oder mysql“ für die ” ” Datenbank MySQL) und subname für die Adresse der Datenbank steht, an die zusätzlich der eigentliche Datenbankname angehängt wird. Beispiel: Der Parameter url = "jdbc:mysql://localhost/seminardatenbank" würde eine JDBC-Verbindung zu einer MySQL-Datenbank mit Namen seminardatenbank“ auf dem lokalen System ” herstellen. In folgendem Ausschnitt wird eine Verbindung zu obiger Beispiel-URL hergestellt: ... S t r i n g u r l = ” j d b c : mysql : / / l o c a l h o s t / seminardatenbank ” ; Connection con = DriverManager . g e t C o n n e c t i o n ( u r l , ” u s r ” , ”pwd” ) ; ... 9 Schritt 3: Erzeugen eines Statement-Objekts Das Interface Connection repräsentiert die Verbindung zu einer spezifischen Datenbank. In ihrem Kontext werden Anfragen durchgeführt und Ergebnisse verarbeitet. Ihre wichtigsten Methoden sind createStatement(), um ein Statement-Objekt für das Absetzen von SQL-Anfragen an die Datenbank zu erzeugen, sowie close(), um eine Verbindung nach Verarbeiten der Ergebnisse wieder zu schließen und ihre belegten Ressourcen freizugeben. Beim schließen einer Verbindung werden automatisch alle von der Verbindung erzeugten Datenbankobjekte, wie z.B. Statements und ResultSets, ebenfalls geschlossen. Die Methode getMetaData() erzeugt ein DatabaseMetaData-Objekt, mit dem es möglich ist, eine ganze Reihe von Informationen über die angeschlossene Datenbank zu erfragen. Dazu gehören z.B. eine Beschreibung der Datenbanktabellen, die von der Datenbank unterstützten Funktionen und SQL-Grammatiken oder auch Eigenschaften der Verbindung. Man könnte beispielsweise mit DatabaseMetaData.supportsOuterJoins herausfinden, ob die Datenbank den Outer Join (äußerer Verbund von zwei oder mehr Tabellen) unterstützt. Schritt 4: Anfragen durchführen Die Klasse Statement dient dem Ausführen von SQL-Anweisungen und der Rückgabe eventuell entstandener Ergebnisse. Ihre wichtigsten Methoden lauten wie folgt: void close() Sobald die Arbeit mit einem Statement-Objekt abgeschlossen ist, sollte die Methode close() aufgerufen werden. Sie gibt dem Garbage Collector explizit die Gelegenheit, den vom Statement belegten Speicher sofort freizugeben, was in der Regel die Performanz verbessert. ResultSet executeQuery(String sql) Die Methode erhält als Argument eine SQL-Anweisung in Form eines Strings, führt die gewünschte Anfrage an die Datenbank aus und gibt ein ResultSet-Objekt zurück. Das ResultSet repräsentiert die Ergebnisse einer SQL-Anfrage in Form einer Tabelle und wird später noch näher beschrieben. executeQuery() ist die Standardmethode, um Informationen aus der Datenbank abzufragen. int executeUpdate(String sql) Führt entweder SQL-Operationen durch, die schreibend auf die Datenbank zugreifen, oder solche, die keinen Rückgabewert haben. Zu den schreibenden Operationen gehören z.B. INSERT- (zum Einfügen von Datensätzen), UPDATE- (zum Verändern bestehender Datensätze) und DELETE-Statements (zum Löschen von Datensätzen). Die Methode gibt entweder die Zahl der betroffenen Datensätze zurück, oder 0, falls das SQL-Statement keinen Rückgabewert liefert. Operationen ohne Ergebnismenge Wir verwenden nun die Methode executeUpdate(String sql), um eine neue Person in unsere Tabelle personen aus Abschnitt 2 einzufügen: 10 ... Statement s t a t e m e n t = con . c r e a t e S t a t e m e n t ( ) ; S t r i n g s q l = ”INSERT INTO p e r s o n e n ( vorname , nachname , wohnort ) VALUES ( ’ Maria ’ , ’ K l e i n ’ , ’ München ’ ) ” ; int r e s u l t = s t a t e m e n t . executeUpdate ( query ) ; ... Im SQL-Statement geben wir an, in welche Tabelle eingefügt werden soll, in welche Spalten und was für Werte. Da die Spalte id in der Struktur der Tabelle als fortlaufende Identifikationsnummer definiert wurde, müssen wir sie nicht selbst inkrementieren und bei Einfügeoperationen auch nicht angeben. Der Rückgabewert von executeUpdate ist bei INSERT-Anweisungen die Anzahl eingefügter und bei UPDATE-Anweisungen die Anzahl betroffener Datensätze. Schritt 5: Anfragen mit Ergebnismenge - Arbeiten mit dem ResultSet Im Allgemeinen wird sehr viel häufiger lesend auf eine Datenbank zugegriffen, was dann eine entsprechende Verarbeitung der Ergebnisse notwendig macht. Wir wollen im Folgenden die Menge aller in München wohnenden Personen erfragen und das Ergebnis auf der Konsole ausgeben. Zunächst führen wir die Anfrage durch und legen das Ergebnis in einem Objekt vom Typ ResultSet ab. ... query = ”SELECT ∗ FROM p e r s o n e n WHERE wohnort = ’ München ’ ” ; R e s u l t S e t r e s u l t = s t a t e m e n t . executeQuery ( query ) ; ... Anstatt jede einzelne Spalte explizit anzugeben, kann man wie oben ein Sternchen verwenden, um alle vorhandenen Spalten ins Ergebnis aufzunehmen. Ein ResultSet-Objekt repräsentiert eine Datentabelle mit den Ergebnissen einer SQLAnfrage. Es verfügt über einen Zeiger, der auf die aktuelle Zeile bzw. den aktuellen Datensatz verweist. Zu Beginn wird der Zeiger vor dem ersten Datensatz positioniert und kann dann mittels der next()-Methode in die jeweils nächste Zeile bewegt werden. Da die Methode false zurückgibt, sobald keine Datensätze mehr im ResultSet vorhanden sind, kann sie dazu verwendet werden, in einer Schleife durch die Ergebnisse zu iterieren. Die Anzahl der im Ergebnis vorhandenen Spalten, ihre Namen und Typen können über die Methode getMetaData() erfragt werden. In unserem Beispiel sind uns all diese Daten durch die Struktur der Tabelle und der SQL-Anfrage bereits bekannt und wir können uns ohne Umwege zwei grundlegende Methoden zum Auslesen der Spaltenwerte ansehen: int getInt(String columnName) Greift auf den Integer-Wert der Spalte columnName des aktuellen Ergebnistupels zu. Statt über den Spaltennamen hätte man auch über die Position der Spalte zugreifen können, was im Allgemeinen etwas effizienter 11 ist: int getInt(int colIndex). Die Spalten werden von 1 an durchnummeriert, nicht von 0. String getString(String columnName) Greift analog auf den String-Wert des aktuellen Datensatzes zu. Auch hier kann stattdessen über die Position der Spalte zugegriffen werden: String getString(int colIndex). Laut [9] gibt es leider keine Methode um die Daten einer gesamten Zeile auf einmal zu erhalten. Wir iterieren nun also durch unsere Ergebnismenge und geben jeden Datensatz als einzelne Zeile in der Konsole aus: ... while ( r e s u l t . next ( ) ) { System . out . p r i n t ( r e s u l t . g e t I n t ( ” i d ” ) + ” ” ) ; System . out . p r i n t ( r e s u l t . g e t S t r i n g ( ” vorname ” ) + ” ” ) ; System . out . p r i n t ( r e s u l t . g e t S t r i n g ( ”nachname” ) + ” ” ) ; System . out . p r i n t l n ( r e s u l t . g e t S t r i n g ( ” wohnort ” ) ) ; } ... Die zugehörige Ausgabe: 2 Gabriele Huber München 3 Erwin Schuster München 5 Maria Klein München Anmerkung: Seit JDBC 2.0 ist es unter bestimmten Umständen möglich, das ResultSet mittels seiner set()-Methoden zu bearbeiten, wobei diese Änderungen auf die Datenbasis zurückpropagiert werden. Anfrageinformationen: ResultSetMetaData Ein Objekt vom Typ ResultSetMetaData kann dazu verwendet werden, um Informationen über die Typen und Eigenschaften von Spalten in einem ResultSet zu erhalten. Das ist insbesondere dann wichtig, wenn eine SQL-Anfrage dynamisch von der Anwendung generiert wurde und die Struktur der Ergebnistupel somit nicht von Beginn an bekannt ist. Einige nützliche Methoden der Klasse ResultSetMetaData lauten: int getColumnCount() Gibt die Anzahl der Spalten im ResultSet zurück. String getColumnName(int i) Gibt den Namen der i-ten Spalte im ResultSet zurück. Die erste Spalte des ResultSets besitzt den Index 1. int getColumnType(int i) Gibt den Typ der i-ten Spalte im ResultSet zurück. int getColumnDisplaySize(int i) Gibt die Anzahl der maximal möglichen Zeichen der i-ten Spalte im ResultSet zurück. 12 String getTableName(int i) Gibt den Namen der Tabelle zurück, zu der die i-te Spalte im ResultSet gehört. boolean isAutoIncrement(int i) Zeigt an ob die i-te Spalte im ResultSet eine automatisch generierte, fortlaufende Nummer (und somit nur lesbar) ist. Wir wenden nun einige dieser Methoden auf unser erhaltenes ResultSet an: ... ResultSetMetaData meta = r e s u l t . getMetaData ( ) ; System . out . p r i n t l n ( ” Tabellenname : ” + meta . getTableName ( 1 ) ) ; System . out . p r i n t l n ( ” S p a l t e n a n z a h l : ” + meta . getColumnCount ( ) ) ; System . out . p r i n t l n ( ” i d Auto−I n k r . : ” + meta . i s A u t o I n c r e m e n t ( 1 ) ) ; ... Die zugehörige Ausgabe: Tabellenname: PERSONEN Spaltenanzahl: 4 id Auto-Inkr.: true Schritt 6: Schließen der Datenbankverbindung Abschließend muss noch die Verbindung mittels connection.close() geschlossen werden. Falls nicht schon manuell geschehen, werden durch diesen Aufruf auch alle im Rahmen der Verbindung erzeugten Datenbankobjekte geschlossen. Nach Möglichkeit sollte man nicht mehr benötigte Statements und ResultSets jedoch schon früher schließen, um dem Garbage Collector die Möglichkeit zu geben, den von ihnen belegten Speicher sofort freizugeben. Überblick: JDBC-Spaltentypen Beim Auslesen des ResultSets versucht JDBC die zu Grunde liegenden Ergebnisdaten aus der Datenbank in ihre jeweiligen Rückgabetypen der get()-Methoden umzuwandeln. Seit Version 2.0 kennt JDBC 24 Datentypen, die zusammen mit ihren entsprechenden JavaDatentypen im folgenden aufgelistet werden. Die aufgeführten Typen stellen dabei aber lediglich die Standard-Zuordnung dar. Bei der Abfrage eines Werts aus einem ResultSetObjekt können automatisch Konvertierungen über die get<type>()-Methoden vorgenommen werden. Ebenso können beim Setzen eines Werts in einer SQL-Anweisung über die set<type>()-Methoden des Interfaces PreparedStatement Konvertierungen durchgeführt werden. 13 JDBC-Datentyp Java-Datentyp JDBC-Datentyp Java-Datentyp CHAR VARCHAR LONGVARCHAR NUMERIC DECIMAL BIT TINYINT SMALLINT INTEGER BIGINT REAL FLOAT java.lang.String java.lang.String java.lang.String java.math.BigDecimal java.math.BigDecimal boolean byte short int long float double DOUBLE BINARY VARBINARY LONGVARBINARY DATE TIME TIMESTAMP BLOB CLOB STRUCT ARRAY REF double byte[] byte[] byte[] java.sql.Date java.sql.Time java.sql.Timestamp java.sql.Blob java.sql.Clob java.sql.Struct java.sql.Array java.sql.Ref 3.8 Erweiterte Funktionen von JDBC Im Folgenden werden einige erweiterte Möglichkeiten von JDBC vorgestellt. Prepared Statements Das Interface PreparedStatement leitet sich vom Statement ab und repräsentiert ein präkompiliertes SQL-Statement. Im Gegensatz zum Statement erhält ein konkretes PreparedStatementObjekt bei seiner Erzeugung einmalig einen SQL-Befehl, der nur bei seiner ersten Ausführung von der Datenbank kompiliert wird. In der Folge kann die Anfrage sehr effizient mehrfach wiederholt werden, da der Kompiliervorgang wegfällt. Es ist außerdem möglich, das Statement bei jeder Wiederholung mit unterschiedlichen Parametern auszuführen. Analog zu Statements werden auch PreparedStatements von einer Connection-Methode erzeugt. Variable Werte innerhalb des SQL-Statements werden durch Fragezeichen-Platzhalter ersetzt, an deren Stelle später bei der Ausführung die übergebenen Parameter treten. Für jeden Platzhalter muss je nach dessen Datentyp eine passende set<type>(int i, <type> x)Methode aus dem Interface PreparedStatement aufgerufen werden, wobei i für den i-ten Platzhalter steht und x der einzusetzende Wert ist. Sobald man allen Platzhaltern einen Wert zugewiesen hat, kann das Statement ausgeführt werden. Anschließend genügt es einzelne Parameter zu ändern und das Statement erneut auszuführen. Beispiel: ... PreparedStatement u p d a t e S a l e s = c o n n e c t i o n . p r e p a r e S t a t e m e n t ( ”UPDATE c o f f e e s SET s a l e s = ? WHERE c o f f e e s o r t LIKE ? ” ) ; // Beide Parameter s e t z e n updateSales . setInt (1 , 75); updateSales . setString (2 , ” Excelsa ” ) ; u p d a t e S a l e s . executeUpdate ( ) ; // Ausf ühren 14 // Zwei neue Parameter s e t z e n updateSales . setInt (1 , 100); u p d a t e S a l e s . s e t S t r i n g ( 2 , ” Arabica ” ) ; u p d a t e S a l e s . executeUpdate ( ) ; // Ausf ühren // Nur e i n e n Parameter s e t z e n u p d a t e S a l e s . s e t S t r i n g ( 2 , ” Robusta ” ) ; u p d a t e S a l e s . executeUpdate ( ) ; // Ausf ühren ... Stored Procedures und Callable Statements Stored Procedures fassen Abläufe von Anweisungen unter einem Namen zu einer Prozedur zusammen und speichern diese auf dem Datenbankserver ab. Sie stehen dort als eigenständiger Befehl zur Verfügung und können bei Bedarf aufgerufen werden. Diese abgespeicherten Prozeduren bieten sich für häufig verwendete Abläufe an, die andernfalls durch viele einzelne Clientbefehle ausgeführt werden müssten. Da nun alle Befehle bereits auf dem Server liegen, müssen weniger Daten ausgetauscht werden, wodurch eine Steigerung der Leistung möglich ist. JDBC unterstützt Stored Procedures in Form von CallableStatements, die wiederum von Statement abgeleitet sind und in der Verwendung den PreparedStatements ähneln. Ein Beispiel: ... C a l l a b l e S t a t e m e n t c s = con . p r e p a r e C a l l ( ”{ c a l l doBigJob ( ? , ? ) } ” ) ; c s . s e t S t r i n g ( 1 , ” S t r i n g parameter ” ) ; // Parameter 1 c s . s e t F l o a t ( 2 , 7 . 3 8 f ) ; // Parameter 2 c s . r e g i s t e r O u t P a r a m e t e r ( 1 , Types .INTEGER ) ; cs . execute ( ) ; int r e s u l t = c s . g e t I n t ( 1 ) ; // E r g e b n i s h o l e n ... Zunächst wird der Prozeduraufruf vorbereitet, anschließend werden zwei Parameter übergeben. Falls der Aufruf ein oder mehrere Ergebnis(se) liefert, müssen diese vor der Ausführung des Aufrufs mit registerOutParameter() je nach Rückgabetyp registriert werden. Nach der Ausführung können eventuelle Ergebnisse mit den bekannten get<type>()-Methoden eingeholt werden. Transaktionen Unter Transaktionen versteht man Folgen von Operationen, die als logische Einheit betrachtet und entweder vollständig ( Commit“) oder überhaupt nicht ( Abort“) aus” ” geführt werden. In JDBC ist standardmäßig jedes Statement für sich genommen eine Transaktion, deren Auswirkungen sich erst nach erfolgreicher Ausführung manifestie- 15 ren. Hierzu wird implizit eine commit()-Methode aufgerufen, die den vorläufigen in einen permanenten Zustand überführt. Der Entwickler merkt hiervon nichts. Ruft man jedoch die Connection-Methode con.setAutoCommit(false) auf, werden alle darauf folgenden Statements nicht mehr automatisch manifestiert, sondern zunächst zu einer Transaktion zusammengefasst. Die Methode con.commit() führt schließlich alle in die Transaktion eingeschlossenen Statements aus, oder eben keines, falls die geschlossene Ausführung nicht möglich war. Mit setAutoCommit(true) kehrt man wieder in den normalen Modus zurück. Ein Beispiel: ... con . setAutoCommit ( f a l s e ) ; PreparedStatement ps1 = con . p r e p a r e S t a t e m e n t ( s q l 1 ) ; // S e t p a r a m e t e r s o f ps1 h e r e ps . executeUpdate ( ) ; PreparedStatement ps2 = con . p r e p a r e S t a t e m e n t ( s q l 2 ) ; // S e t p a r a m e t e r s o f ps2 h e r e ps2 . executeUpdate ( ) ; con . commit ( ) ; con . setAutoCommit ( true ) ; ... 3.9 Fazit JDBC bringt alle nötigen und etablierten Funktionen mit, um produktiv Datenbankanwendungen zu entwickeln. Mit entsprechenden Treibern können JDBC-Anwendungen bei minimalem Aufwand auf jede Plattform portiert und die darunterliegende Datenbank nach Belieben ausgetauscht werden. Nachteilig ist die geringere Performanz im Vergleich zur Alternative SQLJ und auch die Tatsache, dass Fehler in Datenbankanfragen erst zur Laufzeit erkannt werden. Zudem ist z.B. der Zugriff auf Ergebnisspalten nur über einzelne get()-Methoden möglich, was dem ein oder anderen Entwickler aus anderen Bereichen umständlich erscheinen wird. In jedem Fall aber wurde mit JDBC eine wichtige Alternative zu ODBC-Anwendungen oder gängigen Kombinationen aus Skriptsprache und Datenbank geschaffen. 4 Java DB 4.1 Einführung Java DB ist Suns Variante der vollständig in Java geschriebenen, relationalen Datenbank Apache Derby[2]. Sie bietet sich insbesondere als eingebettete Datenbank an und unterstützt die Standards SQL und JDBC. 16 Entwicklung Apache Derby wurde ursprünglich von Cloudscape entwickelt und 1997 veröffentlicht. 1999 wurde Cloudscape von Informix Software aufgekauft, deren Datenbanksparte 2001 von IBM übernommen wurde. IBM brachte die Datenbanksoftware 2004 unter dem Namen Derby“ in ein neues Open Source Projekt unter dem Dach der Apache Software ” Foundation ein1 . 2006 wurde Derby schließlich unter dem Namen Java DB in Java 6 integriert und mit dem JDK ausgeliefert. Sun verfolgt dabei nicht das Ziel einer eigenständigen Entwicklungsschiene, sondern behält sich Erweiterungen und Aktualisierungen der Datenbank vor, die dann wieder in das ursprüngliche Open Source Projekt einfließen. Somit bleibt Java DB eine freie Datenbank, die lediglich unter anderem Namen vertrieben wird. Motivation Mit der Derby Project Charter“ hat sich Apache bei der Entwicklung von Derby einige ” für Entwickler interessante Ziele gesetzt: Reiner Javacode, Einfache Benutzung, Kleiner Fußabdruck“, Standardisierung und Sicherheit. Tatsächlich ist Derby vollständig in ” Java geschrieben (somit uneingeschränkt kompatibel mit allen Plattformen, auf denen eine JVM läuft), die bereits voll einsatzfähige Hauptbibliothek derby.jar ist lediglich 2,5 MByte groß und die Datenbank an sich erfordert praktisch keinen Administrationsaufwand. Zur Installation genügt es bereits, derby.jar zum Klassenpfad der Anwendung hinzuzufügen. Anders als andere Datenbanken ist Derbys JDBC-Treiber bereits voll in die Datenbank integriert und muss nicht zusätzlich installiert werden. Auch die Tatsache, dass die Datenbank in der gleichen JVM läuft, wie die eigentliche Anwendung, dürfte sie von anderen Datenbanken abheben. Zudem bleiben mit der Implementierung des JDBC- und des SQL-Standards eigentlich keine Wünsche offen. 4.2 Architektur von Java DB Abbildung 3 zeigt den grundlegenden Aufbau und die strukturellen Einsatzmöglichkeiten der Datenbank. Derby ist aus mehreren Modulen aufgebaut, die im rechten Teil der Abbildung dargestellt sind. Der Zugriff erfolgt über die Module JDBC und SQL. Das SQLModul ist für das Parsen und die Optimierung von SQL-Anweisungen zuständig. Das Zugriffsmodul bestimmt unter Einbeziehung von Indizes und Statistiken den günstigsten Zugriffspfad. Als Datenstrukturen kommen balancierte B+-Bäume, Heaps und Hashtabellen zum Einsatz. Das Speichermodul verarbeitet Daten, verwaltet den Buffer, die Daten und die Transaktionslogs. Als Speicher wird nur das Dateisystem verwendet, Derby kann somit nicht auf Systemen ohne Plattenspeicher eingesetzt werden. Konkrete Datenbanken werden in einem Verzeichnis gespeichert, das den gleichen Namen wie die jeweilige Datenbank trägt. Es enthält die drei Unterverzeichnisse seg0 (für die eigentliche Datenbank, deren Daten, Strukturen und Indizes), log (für das Transaktionslog) und tmp (für temporäre Arbeitsdaten). Außerdem existieren die Dateien service.properties 1 Der Hut auf Seite 1 wurde von der Community zum offiziellen Apache Derby Logo gewählt. 17 (enthält Konfigurationsparameter der Datenbank) und db.lck (eine Sperre für das einmalige Starten). Im Verzeichnis seg0 liegen die eigentlichen Daten, wobei für jede Tabelle eine eigene cXXX.dat existiert. Somit ist die Tabellengröße von der maximalen Dateigröße des Betriebssystems abhängig. Derby unterstützt die Verteilung der Log, Tmp- und Datenbankverzeichnisse auf mehrere Festplatten, um so die Schreib- und Leseraten zu erhöhen. Derby-Tools Möchte man die Datenbank für bestimmte Zwecke (z.B. Administration) nicht über JDBC ansprechen, kann dies über die mitgelieferten Derby-Tools geschehen: Das Programm ij dient dem Ausführen fertiger Skripts oder interaktiver SQL-Anfragen über die Konsole, dblook ist ein Schema-Extraktionstool und sysinfo dient lediglich der Anzeige von Versionsnummern und Klassenpfad. Prinzipielle Einsatzmöglichkeiten In Abbildung 3 stellt Szenario 1 den Einsatz als eingebettete Datenbank dar, für den lediglich die Hauptbibliothek derby.jar nötig ist. Für den Netzwerkbetrieb (Szenario 2 und 3), bei dem Anwendung und Datenbank in getrennten Umgebungen laufen, werden die Bibliotheken für Netzwerkserver und -client benötigt (derbynet.jar und derbyclient.jar). Die Kommunikation zum Netzkwerkserver erfolgt über eine TCP/IP-Verbindung und das Standardprotokoll DRDA. Über JDBC bzw. ODBC können Clients unterschiedlicher Programmiersprachen eine Verbindung zum Server aufbauen. Treiber Der zu Java DB gehörende JDBC-Treiber trägt den Namen org.apache.derby.jdbc.EmbeddedDriver“ und kann wie üblich mit Class.forName ge” laden werden. Die URL für getConnection() lautet "jdbc:derby:dbname". Erweiterte Funktionen von Java DB Einige weitere erwähnenswerte Funktionen von Java DB sind Mehrbenutzerfähigkeit, die Unterstützung von Parallelität, Transaktionsmanagement, Stored Procedures, Triggern und Datenbankdateiverschlüsselung. Zudem können neue Versionen der Datenbank die alten Daten einfach weiter verwenden. Nachteile von Java DB Zwei auffällige Nachteile bei der Verwendung der Datenbank sind Folgende: ∙ Zugriffsrechte können nur für Benutzer, nicht für Gruppen vergeben werden. ∙ Derby kennt keine LIMIT bzw. PAGING oder OFFSET Konstrukte[3]. Es ist somit nicht ohne weiteres möglich durch Teilmengen gespeicherter Daten zu blättern“, ” 18 Abbildung 3: Architektur und Einsatzmöglichkeiten von Java DB also z.B. die i-ten 10 Datensätze anzufragen. Dies kann unter Umständen die Leistung beeinträchtigen. 4.3 Fazit Die Vermutung liegt nahe, dass hauptsächlich kleine und mobile Geräte von Java DB profitieren könnten, doch der Einsatz eingebetteter Datenbanken ist gerade in traditionellen Anwendungen viel weiter verbreitet als gedacht: So verfügt z.B. Open Office über eine (ebenfalls in Java geschriebene) integrierte Datenbank namens HSQLD und Firefox verwaltet seine Bookmarks und die History mit SQLite. Auch das Verwalten von Logdaten, Benutzereinstellungen, Adressen, Nachrichten und anderem kann mit geringem Aufwand von einer Datenbank erledigt werden. Zwar bietet z.B. Java ME (Java Platform, Micro Edition) zumindest für mobile Endgeräte ein Record Management System (RMS), doch ähnelt dies einer satzorientierten Datenbank, die nur eingeschränkte Suchfunktionen und einen langsamen, insgesamt eher umständlichen Datenzugriff bietet[1]. Somit kann Java DB gerade im Bereich der eingebetteten Datenbanken als sehr gute Alternative angesehen werden. 19 5 Literatur [1] Al-Biladi, Yasser: Konzeption und Implementierung einer mobilen Anwendung für J2ME-fähige Endgeräte, HTWK Leipzig, Fachbereich Informatik, Mathematik und Naturwissenschaften, Diplomarbeit, Juni 2004 [2] Apache: Apache Derby. http://db.apache.org/derby/. – [Online; Stand 8. Juli 2009] [3] Bosanac, Dejan: Tuning Derby. http://www.onjava.com/pub/a/onjava/2007/01/31/ tuning-derby.html. Version: 2009. – [Online; Stand 3. Juli 2009] [4] Foundation, Apache: Apache Derby logo contest. http://issues.apache.org/jira/ browse/DERBY-297. Version: 2005. – [Online; Stand 5. Juli 2009] [5] Hobbs, Ashton: Teach Yourself Database Programming With Jdbc in 21 Days (Teach Yourself Series). Sams Publishing, 1997. – ISBN 1575211238 [6] Kemper, Alfons ; Eickler, Andre: Datenbanksysteme. Oldenbourg Wissensch.Vlg, 2006. – ISBN 3486576909 [7] Microsoft: Microsoft Open Database Connectivity (ODBC). http://msdn.microsoft. com/en-us/library/ms710252(VS.85).aspx. – [Online; Stand 8. Juli 2009] [8] Riccardi, Greg: Principles of Database Systems With Internet and Java Applications. Addison Wesley Longman, 2000. – ISBN 020161247X [9] Sun: Frequently Asked Questions about JDBC. http://java.sun.com/products/jdbc/ reference/faqs/index.html#11. – [Online; Stand 8. Juli 2009] [10] Sun: Java SE Technologies - Database. http://java.sun.com/javase/technologies/ database/. – [Online; Stand 8. Juli 2009] [11] Sun: JDBC downloads and specifications. http://java.sun.com/products/jdbc/ download.html. Version: 2006. – [Online; Stand 8. Juli 2009] [12] Sun: JDBC Drivers. http://developers.sun.com/product/jdbc/drivers. Version: 2009. – [Online; Stand 5. Juli 2009] [13] Sun: Types of JDBC technology drivers. http://java.sun.com/products/jdbc/ driverdesc.html. Version: 2009. – [Online; Stand 3. Juli 2009] 20