![]() |
|
|||||
Wir beschränken uns im Folgenden auf die Verbindung zu dem komerziellen Datenbanksystem Microsoft-Access und der quasi-freien Datenbank Interbase von Borland. Interbase wurde von den Lesen des Java-Magazins neben Oracle als bestes Produkt 2001 in der Sparte Datenbank normiert. 21.3 Die Rolle von SQL
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Sprache | Entwicklung |
| SQUARE | 1975 |
| SEQUEL | 1975, IBM Research Labs San Jose |
| SEQUEL2 | 1976, IBM Research Labs San Jose |
| SQL | 1982, IBM |
| ANSI-SQL | 1986 |
| ISO-SQL | 1989, drei Sprachen Level 1, Level 2, +IEF |
| Firmenstandards | IBM SQL, OS/2 SQL, X/Open UNIX SQL, ... |
| SQL-2 (bzw. SQL-92) | 1992 |
| SQL-3 | in Bearbeitung |
Damit sich ein Datenbanktreiber JDBC-kompatibel nennen kann, muss er mindestens SQL-92 unterstützen. Das heißt jedoch nicht, dass die existierenden Treiber alle Eigenschaften von SQL-92 unterstützen.
Da das Wort »Anfragesprache« irgendwie eine Art Programmiersprache suggeriert, sind wir interessiert an einem Beispiel. Um es vorweg zu sagen: Es gibt nur eine Hand voll wichtiger Befehle und SELECT, UPDATE, CREATE decken schon einen Großteil ab.
Beispiel Eine einfache Abfrage in SQL
SELECT Lfr_Name |
Tabellen nehmen die Benutzerdaten auf und mit dem Kommando FROM wählen wir die Tabelle LIEFERANTEN aus, die für die Berechnung nötig ist. Die Tabelle LIEFERANTEN enthält drei Attribute (die Spalten), die wir mit SELECT auswählen. In einer Datenbank werden normalerweise mehrere Tabellen verwendet, ein so genanntes Datenbank-Schema. Jede Tabelle gehört genau zu einem Schema.
SQL-Abfragen sind nahe an einer natürlichsprachlichen Formulierung. Im oberen Beispiel liest sich die Abfrage einfach als: »Wähle die Spalte Lfr_Name aus der Tabelle LIEFERANTEN«. Der Designer einer Datenbank muss sich natürlich vor der Umsetzung der Tabellen und somit der Relationen gründlich Gedanken machen. Denn eine spätere Änderung der Struktur ist teuer. So muss schon am Anfang einkalkuliert werden, welche Daten in welchen Ausprägungen auftreten können. Nach Statistiken des amerikanischen Library of Congress verdoppelt sich insgesamt alle fünf Jahre die Informationsmenge. Was wäre, wenn diese Informationen alles Einträge in einer Datenbank wären, und jemand würde feststellen, dass das Tabellenschema ungünstig ist? Eine Datenbank muss also schon enorm leistungsfähig sein, um sich einer solchen Menge stellen zu können. Datenbankdesigner nennen den Vorgang von einem ersten Modell zur fertigen Relation Normalisierung.
Bevor wir mit den einzelnen Sprachelementen von SQL fortfahren, an dieser Stelle ein paar Regeln für SQL-Ausdrücke:
| Die SQL-Anweisungen sind unabhängig von der Groß- und Kleinschreibung. Im Text sind die SQL-Kommandos großgeschrieben, damit die Ausdrücke besser lesbar sind. |
| Leerzeichen, Return, Tabulatoren sind in einer Anfrage bedeutungslos. Im Folgenden werden zur besseren Lesbarkeit Zeilenumbrüche verwendet. |
| SQL-Anweisungen werden mit einer Zeichenkette abgeschlossen. Diese unterscheidet sich aber von Datenbank zu Datenbank. Häufig ist dies ein Semikolon; es kann aber auch ein \go sein. Wir werden die Anweisungen in den Beispielen nicht abschließen, da JDBC diesen Abschluss automatisch vornimmt. |
Im Folgenden werden wir uns noch intensiver um SQL-Anfragen kümmern. Es wird sich zeigen, dass eine einzelne Anweisung sehr ausdrucksstark sein kann. JDBC hat aber mit dieser Ausdrucksstärke nichts zu schaffen, es weiß noch nicht einmal um ihre Korrektheit. JDBC leitet den SQL-Befehl einfach an den Treiber weiter und dieser leitet das Kommando dann wiederum an die Datenbank weiter.
Unsere ersten Beispiele, die wir benutzen werden, basieren auf einer existierenden Datenbank mit Pflanzen, Bestellungen und Lieferanten. Interessieren wir uns nun also für Abfragekommandos, um auf die Inhalte zuzugreifen. In SQL steckt auch schon das Wort »Query«, unsere Anfrage. Das wichtigste Element ist hierbei das schon oben genannte SELECT.
SELECT {Feldname, Feldname,..|*} ( * = alle Felder )
FROM Tabelle [, Tabelle, Tabelle....]
[WHERE {Bedingung}]
[ORDER BY Feldname [ASC|DESC]...]
[GROUP BY Feldname [HAVING {Bedingung}]]
Das angenehme bei SQL ist, dass wir uns nicht um das Wie kümmern müssen, sondern nur um das Was. Wir fragen also etwa »Welche Lieferanten wohnen in Aalen?« und formulieren:
SELECT Lfr_Name
FROM LIEFERANTEN
WHERE Wohnort='Aalen'
Dabei ist es uns egal, wie die Datenbankimplementierung mit dieser Anfrage umgeht. Und hier unterscheiden sich auch die Anbieter in ihrer Leistungsfähigkeit und in den Preisen.
Kümmern wir uns nun um die verschiedenen Schreibweisen von SELECT. Geben wie im SELECT eine Spalte an, so bekommen wir nur die Ergebnisse dieser Spalte zurück. Eine Anfrage, die alle Spalten zurückgibt, wird mit dem »*« geschrieben. Damit wir also nicht nur den Namen des Kunden bekommen, sondern auch noch die anderen Angaben – um ihm gleich einen Auftrag zu geben – schreiben wir Folgendes, um eine Liste aller Lieferanten in Aalen zu bekommen:
SELECT * FROM LIEFERANTEN WHERE Wohnort='Aalen'
Wir sehen, dass es keinen Unterschied macht, ob die Anfragen in mehrere Zeilen aufgespaltet sind oder in einer Zeile stehen. Wir werden jedoch zur besseren Lesbarkeit bei einzelnen Zeilen bleiben.
Die SELECT-Anweisung geht über die Spalten und die WHERE-Angabe filtert Zeilen nach einem Kriterium heraus. Wir haben zunächst mit einer Gleich-Abfrage gearbeitet. SQL kennt die üblichen Vergleichsoperatoren: = gleich, <> ungleich, > größer, < kleiner, >= größer gleich, <= kleiner gleich. Vergleiche werden mit einem einfachen Gleichheitszeichen und nicht durch == formuliert. Die Vergleichsoperatoren lassen sich durch die Operatoren AND, OR und NOT weiter verfeinern. Bei numerischen Daten können wir auch die Rechenoperatoren (+, –, *, /) anwenden. Anstelle vielfacher AND-Anfragen lässt sich mit zwei SQL-Anweisungen auch der Wertebereich noch weiter einschränken. Mit BETWEEN Wert1 AND Wert2 lässt sich testen, ob ein Vergleichswert zwischen Wert1 und Wert2 liegt. Mit IN (Werteliste) wird getestet, ob der Vergleichswert in der angegebenen Werteliste liegt. Für Zeichenketten spielt noch LIKE eine Rolle, da hier ein Mustervergleich vorgenommen werden kann. Mit IS NULL lässt sich mit einem NULL-Wert vergleichen.
Wenn wir diese SQL-Anweisung von der Datenbank ausführen lassen, so wollen wir die Daten gerne nach dem Preis sortiert haben. Dazu lässt sich die SELECT-Anweisung mit einem ORDER BY versehen. Dahinter folgt die Spalte, nach der sortiert wird. Jetzt wird die Tabelle aufsteigend sortiert, also der kleinste Wert unten. Wünschen wir die Sortierung absteigend, dann setzen wir noch DESC hintenan.
Wir wollen nun die Informationen mehrerer Tabellen miteinander verbinden. Dazu führen wir eine Tupelvariable ein. Diese kann dann eingesetzt werden, wenn sich Attribute nicht eindeutig den Relationen zuordnen lassen. Dies ist genau dann der Fall, wenn zwei Relationen verbunden werden sollen und beide den gleichen Attributnamen besitzen.
Beispiel Die SQL-Anweisung zeigt die Verwendung der Variable, die jedoch hier nicht nötig ist, da nur eine Tabelle verwendet wird.
SELECT L.Lfr_Name, L.Wohnort |
Der Buchstabe »L« ist hier nur eine Abkürzung, eine Art Variable. Abkürzungen für Spalten werden in SQL auch mit AS abgetrennt, etwa
FROM LIEFERANT AS L
Standardmäßig darf der Relationenname auch als Tupelvariable benutzt werden. Normalerweise können wir mit Tupelvariablen Schreibarbeit sparen.
SELECT LIEFERANTEN.Lfr_Name, LIEFERANTEN.Wohnort
FROM LIEFERANTEN
Damit wir auch zwei Tabellen gleichzeitig zeigen können, trennen wir die Anfragen mit Komma. Das Attribut der Tabelle wird wie bei Objekten mit einem Punkt vom Tabellennamen getrennt.
Mit Gruppenfunktionen (auch Aggregationsfunktionen) lassen sich etwa Durchschnittswerte oder Minima über Spalten beziehen. Sie liefern genau einen Wert, beziehen sich jedoch auf mehrere Tabellenzeilen. Die folgende Anfrage liefert alle Anbieter aus Aalen:
SELECT COUNT(*)
FROM LIEFERANTEN
WHERE Wohnort = 'Aalen'
Die Spalten, die die Gruppenfunktion bearbeitet, steht in Klammern hinter dem Namen. Die SQL-Standardfunktionen (es gibt datenbankabhängig noch viel mehr) sind in der folgenden Tabelle aufgeführt:
| AVG | Durchschnittswert |
| COUNT | Anzahl aller Einträge |
| MAX | Maximalwert |
| MIN | Minimalwert |
| SUM | Summe aller Einträge in einer Spalte |
Damit wir JDBC nutzen können, brauchen wir einen passenden Treiber für die Datenbank. JavaSoft unterscheidet vier Treiber-Kategorien:
| 1. | JDBC-ODBC Bridge Treiber Da es am Anfang der JDBC-Entwicklung keine Treiber gab, haben sich die Entwickler etwas ausgedacht: Eine JDBC-ODBC-Brücke, die die Aufrufe von JDBC in ODBC-Aufrufe der Client-Seite umwandelt. Die Methoden sind nativ. Im Folgenden werden wir die Brücke einsetzen, wenn wir Access über ODBC ansprechen. |
Versuchen wir daher einmal einige Unterscheidungsmerkmale herauszuarbeiten. Ein Kriterium ist, ob sie in Java implementiert sind oder plattformabhängigen Programmcode beinhalten. Die Treiber vom Typ 3 und Typ 4 sind vollständig in Java implementiert und daher portabel. Treiber vom Typ 0 oder Typ 1 sind das nicht, da sie zum einen für die JDBC-ODBC-Brücke auf die Plattform-Bibliothek für ODBC zurückgreifen müssen, und zum anderen auf plattformspezifische Zugriffsmöglichkeiten für die Datenbank. Damit ist der Nachteil verbunden, dass Applets mit diesen Treibern nichts anfangen können. Ein Applet erlaubt es nicht, nativen Code von anderen Quellen zu laden und auszuführen. Das ist auch schwierig, wenn etwa ein Macintosh mit Power PC Prozessor einen binären Treiber für eine MS-SQL Datenbank installieren möchte. Die Quintessenz ist: Applets können damit keine Verbindung zu einer externen Datenquelle aufbauen.
Besonders eine Definition der Typ3-Treiber fällt schwer, da die Definition von Sun nicht unbedingt auf den ersten Blick klar ist. Zum Vergleich beginnen wir mit Typ 4. Diese Treiber sprechen mit dem datenbankspezifischen Protokoll direkt mit der Datenbank über einen offenen IP-Port. Dies ist in einer Direktverbindung die performanteste Lösung. Jedoch ist sie nicht immer möglich. Ein Grund ist, dass manche Datenbanken wie MS-Access, dBase oder Paradox kein Netzwerkprotokoll definieren. Da erfüllen Typ 3-Treiber eine Vermittlerrolle, denn dieser Treibertyp spricht nicht mit der Datenbank, sondern mit einer Softwareschicht, die zwischen der Anwendung und der Datenbank sitzt: die so genannte Middleware. Diese kann etwa in der Mitte die Anweisungen entgegennehmen und an die Datenbank weiterleiten. Für Applets und Internetdienste hat ein Typ 3-Treiber zudem den Vorteil, dass ihre Klassendateien oft kleiner als Typ 4-Treiber sind, da ein komprimiertes Protokoll eingesetzt werden kann. Über das spezielle Protokoll zur Middleware ist auch eine Verschlüsselung der Verbindung möglich. Kaum eine Datenbank unterstützt verschlüsselte Datenbankverbindungen. Da zudem das Middleware-Protokoll unabhängig von der Datenbank ist, müssen auf der Client-Seite für einen Datenbankzugriff auf mehrere Datenbanken auch nicht mehr alle Treiber installiert werden, sondern im günstigsten Fall nur noch ein Typ 3-Treiber von einem Anbieter. Die Ladezeiten sind damit deutlich niedriger.
Wir wollen uns im Folgenden mit einigen Datenbanken beschäftigen, die für den Zugriff unter Java geeignet sind.
Die Datenbank Interbase 6 (http://www.interbase.com) von InterBase Software Corporation, eine Tochter der Borland International Inc, implementiert viele Anforderungen aus dem ANSI-SQL-92. Interbase, kurz IB, bietet gespeicherte Prozeduren oder Trigger und ist eine ausgewachsene performante Lösung, die zum Beispiel von Motorola, Nokia, MCI, Northern Telecom, First National Bank of Chicago, Bear Stearns, The Money Store, US-Army, NASA, Boeing, National Semiconductor und Boston Stock Exchange seit Jahren eingesetzt wird. Warum wir uns hier für Interbase und nicht für Oracle, DB 2 oder Microsoft SQL2 entscheiden, ist darin begründet, dass Borland den Quellcode sowie die ausführbaren Dateien für Windows, Novell Netware und verschiedenen UNIX-Versionen wie Linux und Solaris zur Verfügung stellt (http://www.inprise.com/interbase/downloads) und auf eine Variante der Mozilla Public License3 (MPL) V 1.1 setzt. IB kann auf dem Rechner leicht installiert werden und benötigt wenige Megabytes Hauptspeicher.
Nach dem Download der 5.11 MB (für die Version ib_server_6_0_1.zip) können wir mit setup.exe IB installieren. Nach der Installation wird der Server automatisch gestartet und auch bei jedem Windows-Start mit hochgefahren, da er als Windows-Service eingetragen ist. Ändern lässt sich dies unter Start/Programme/InterBase Service Manager.
Obwohl wir den Server auch auf der Konsole administrieren können, ziehen wir eine grafische Bedienoberfläche vor. Dazu rufen wir unter Start/Programme/InterBase das Programm IBConsole auf. Im linken Baum findet sich unter InterBase Servers eine Auflistung der Server. Bisher ist dort keiner aufgeführt, weshalb wir einen Server eintragen, der automatisch mit der Installation mit installiert wird. Entweder öffnen wir mit Doppelklick auf InterBase Servers den Dialog Register Server and Connect oder wir bringen das Fenster durch den Menüeintrag Servers nach vorne. In den Login-Informationen müssen wir zum Serverzugang Benutzername und Passwort eintragen. Folgende Einträge sind vorbelegt:
User Name: SYSDBA
Passwort: masterkey
Falten wir im linken Fenster den Baum bei Local Server auf, so treten die Einträge Databases, Backup, Server Log und Users hervor. Bisher ist noch keine Datenbank eingetragen, aber für Übungen wollen wir eine Datenbank verwenden. Um eine Datenbank mit dem Manager zu verbinden, gehen wir entweder über das Menü Database/Register oder aktivieren auf Databases in der Baumansicht mit der rechten Maustaste den ersten Menüpunkt Register.
Über den grafischen Datenbankadministrator lassen sich Tabellen verwalten, Trigger programmieren und einfache SQL-Anweisungen absetzen. Eine hübsche Erweiterung der Fähigkeiten bietet die InterBase Workbench (kurz IBWorkbench) von Upscene Productions unter http://www.interbaseworkbench.com. Mit diesem Programm lassen sich einfach Tabellen anlegen, Constraints und Rechte festlegen. Integriert ist ein leistungsfähiger SQL-Editor mit Debugger für gespeicherte Prozeduren.
Ein Problem ist immer die Konvertierung von vorhandenen Daten in ein anderes Datenbankformat. Liegen Tabellen zum Beispiel in Microsoft Access vor und diese sollen nach InterBase wandern, so bietet sich zum Beispiel der Weg über ODBC-Treiber an. Bei der Interbase 6 Version liegt leider kein ODBC-Treiber bei, sodass dieser aus der alten Version bezogen werden muss. Dafür müssen wir unter http://www.interbase.com/open/downloads/ 5x_eval_kits.html die ältere Version laden, das Installationstool aufrufen und lediglich den ODBC-Treiber installieren. Wird die Datenquelle anschließend im ODBC-Manager eingetragen, so lassen sich die Tabellen von einem beliebigen ODBC-Client lesen. Wer Access nutzt und existierende Daten in die IB importieren möchte, muss über einen kleinen Umweg gehen. Zuerst sollten in der IB-Datenbank die Tabellen mit gleichem Schema angelegt werden. Anschließend sollte die zu konvertierende Tabelle in Access geladen werden und mittels Tabellen verknüpfen auf die IB-Datenbank verwiesen werden. Per Copy&Paste lassen sich jetzt die Daten kopieren. Es gibt leider keinen Weg in Access, mit dem die Schemata der Tabellen aus einer Datenbank auf eine andere übertragen werden können.
Aus Java-Sicht interessiert uns bei der InterBase Datenbank kein ODBC-Treiber, sondern ein Java-Datenbanktreiber. Dieser trägt den Namen InterClient und ist eine 100 %-ige Java-Implementierung. Für die Java-Version gibt es ein plattformabhängiges Installationsprogramm. Daher pflegt Borland auf der Webseite http://www.interbase.com/open/ downloads/ib_download.html unterschiedliche Betriebssystemversionen. Die Version InterClient 1.6 für Windows besitzt lediglich eine Dateilänge von 2.6 MB.
Nach der Installation können wir den Treiber testen und die Test-HTML-Seite CommDiag.html im Installationsverzeichnis (etwa C:\Program Files\InterBase Corp\InterClient) im Browser aufrufen. Dieser erzeugt dann ein Fenster und gibt die Meldung
interbase.interclient.Driver registered
Mit Microsoft Access lässt sich mit wenigen Handgriffen eine Datenbank zusammenbauen. Der folgende Ablauf gilt für Access 2000. Wird Access geöffnet, erscheint ein Dialog mit dem Eintrag Leere Access-Datenbank. (Alternativ lässt sich eine neue Datenbank unter dem Menüpunkt Datei, Neu ... einrichten.) Wir wählen den ersten Eintrag Datenbank und speichern dann die Datenbank unter einem aussagekräftigen Namen. Access benötigen wir nicht mehr direkt, da die Kommunikation mit der Datenbank anschließend über den ODBC-Manager läuft. Dieser setzt dann auf den SQL-Kern von Access auf. Im ODBC-Manager muss die Datenquelle dazu angemeldet werden. Dies beschreiben wir etwas später. Mit SQL können nun die Relationen eingetragen und darauf die Anfragen gestellt werden.
Access wird als Beispiel für die JDBC-ODBC-Brücke beschrieben. Viele Anwender haben das Office-Paket von Microsoft zu Hause installiert und so schon eine Datenbank wie Access in der Nähe. Der Nachteil bei der JDBC-OCBC-Brücke ist jedoch, das diese erst seit der Version 1.4 den neueren JDBC 2-Standard implementiert.
ODBC (Open Database Connectivity Standard) ist ein Standard von Microsoft, der den Zugriff auf Datenbanken über eine genormte Schnittstelle möglich macht. ODBC ist weit verbreitet und auch für Macintosh-Systeme und einige UNIX-Plattformen verfügbar. Um die Anzahl der Produkte, die JDBC nutzen, zu beschleunigen, haben JavaSoft und Intersolv eine JDBC-ODBC-Brücke entwickelt, die die JDBC-Aufrufe nach ODBC umwandelt. Leider ist diese Brücke nur für Win32- und Solaris-Systeme verfügbar. Das JDK von Sun und deren Lizenznehmern liefern diese Brücke aus und damit können Java-Entwickler Datenbank-Applikationen mit der Unterstützung einer Vielzahl von existierenden ODBC-Treibern programmieren. In diesem Tutorial wird die JDBC-ODBC-Brücke genutzt, um unter Windows über ODBC auf die Datenbank Microsoft Access zuzugreifen. Obwohl es bisher von Microsoft noch keinen reinen Java-Treiber für Access gibt, bieten doch Drittanbieter JDBC-Treiber auch für die große Datenbank MS-SQL Server an. Unter ihnen WebLogic (http://www.weblogic.com) und InterSolv (http://www.intersolv.com).
Damit wir die JDBC-ODBC-Brücke nutzen können, brauchen wir einen ODBC-Treiber für die spezielle Datenbank. ODBC ist kein Teil eines Betriebssystems, sondern muss getrennt installiert werden, zum Beispiel vom Office-Paket oder vom MS SQL-Server. Auch bei Microsofts Web Server, das Windows NT beiliegt, kann ein ODBC-Treiber installiert werden. Da ODBC von Microsoft ist, gibt es ODBC-Treiber nicht für alle Plattformen. Das heißt, die JDBC-ODBC-Bridge fällt für manche Systeme sofort flach. Microsoft liefert Versionen von ODBC für Windows, Windows 95, Windows NT und Macintosh aus. Von verschiedenen Herstellern gibt es Portierungen für einige UNIX-Plattformen, unter anderem Solaris. Ein Projekt unter dem Namen FreeODBC4 hat sich zum Ziel gesetzt, ODBC auch für andere Plattformen zu verbreiten. Unter anderem gibt es eine freie JDBC-ODBC-Bridge unter den Systemen OS/2, UNIX (auch Linux) und Win32.
Die Installation von ODBC sieht bei jedem Datenbankanbieter anders aus. Benutzen wir Microsoft Access, so werden die ODBC-Treiber während der Installation automatisch mitinstalliert. Wenn wir eine Datenquelle unter ODBC hinzufügen wollen, müssen wir nur wenige Schritte unternehmen. In den Systemeinstellungen (Start, Einstellungen, Systemeinstellungen) suchen wir nach dem Symbol ODBC-Datenquellen (32 Bit), unter Windows 2000 im zusätzlichen Verzeichnis Verwaltung. Nach dem Aktivieren öffnet sich ein Dialog mit dem Titel ODBC-Datenquellen-Administrator.
Wir gehen auf Hinzufügen, um eine neue Benuter-Datenquelle hinzuzufügen. Im Dialog mit dem Titel Neue Datenquelle erstellen wählen wir den Microsoft Access-Treiber aus, und gehen auf Fertigstellen. Ein Dialog öffnet sich und wir tragen unter Datenquellenname einen Namen für die Datenquelle ein. Darunter können wir später in Java die Datenbank ansprechen. Der Name der Datei hat nichts mit dem Namen der Datenquelle gemeinsam. Optional können wir noch eine Beschreibung hinzufügen. Wichtig ist nun die Verbindung zur physikalischen Datenbank. Im umrandeten Bereich Datenbank aktivieren wir über die Schaltfläche Auswählen einen Datei-Selektor. Hier hangeln wir uns bis zur in Access erstellten Datei durch und tragen sie ein. Nun nur noch ein paar Mal OK drücken und wir sind fertig. Wenn der Administrator nicht meckert, können wir nun ein JDBC-Programm starten.
|
Die Geschwindigkeit des Zugriffs über die JDBC-ODBC-Brücke hängt von vielen Faktoren ab, sodass eine pauschale Antwort nicht zu geben ist. Denn zwischen der Abfrage unter JDBC bis zur Datenbank hängen viele Schichten, bei denen unter anderem viele Hersteller beteiligt sind:
| Der JDBC-Treiber-Manager von JavaSoft |
| Der Treiber der JDBC-ODBC-Bridge von JavaSoft und InterSolv |
| Der ODBC-Treiber-Manager von Microsoft |
| Der ODBC-Treiber zum Beispiel vom Datenbankhersteller |
| Die Datenbank selbst |
Jede der Schichten übersetzt nun die Anfragen an die Datenbank in möglicherweise völlig andere Anfragen. So muss zwischen JDBC und ODBC eine Übersetzung vorgenommen werden, dann muss das SQL-Kommando geparst werden usw. Dann geht der Weg auch wieder zurück, von der Datenbank über die Treiber bis hin zum Java-Code. Dies dauert natürlich seine Zeit. Zusätzlich kommen zum Zeitaufwand und dem benötigten Speicher, den die Konvertierung benötigt, noch Inkompatibilitäten und Fehler hinzu. Somit hängt das Gelingen der JDBC-ODBC-Brücke von vielen Schichten ab und ist in vielen Fällen nicht so performant wie eine native Implementierung.
Einer der bekanntesten Datenbanken stammt von Oracle. Um die Verbreitung weiter zu erhöhen, ist die Firma dazu übergegangen, eine vollwertige Version der Enterprise Edition Release 3 (8.1.7) zum Download oder als CD freizugeben. Wer das Download nicht scheut, der kann unter http://otn.oracle.com/software/products/oracle8i/htdocs/winsoft.html die Version für Windows 2000 bzw. NT von rund 600 MB herunterladen.
Mit einem abschließenden Beispiel wollen wir in der Einleitung die Programmkonzepte für JDBC deutlich machen. Das Programm in der Klasse Sql baut eine Verbindung zum Datenbank-Manager auf und möchte auf die Daten der Datenbank Pflanzen zugreifen. Die Datenbank ist als ODBC-Datenquelle eingetragen.
| Hinweis Bisher haben wir den Aufbau der Datenbank noch nicht erläutert, sie fällt also etwas vom Himmel. Das Beispiel verdeutlicht nur die Verwendung der Klassen und Methoden. Es soll zeigen, dass mit wenigen Programmzeilen Datenbankabfragen möglich sind. Zudem setzen wir hier eine ODBC-Datenquelle mit einer Beispiel-Datenbank vorraus. |
import java.sql.*;
public class Sql
{
public static void main( String args[] )
{
try {
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
}
catch ( Exception e ) {
System.out.println( "Fehler bei ODBC-JDBC-Bridge" + e );
return;
}
Connection conn;
Statement stmt;
ResultSet rSet;
try
{
String url = "jdbc:odbc:Pflanzen";
conn = DriverManager.getConnection( url, "User", "User" );
stmt = conn.createStatement();
String sqlQuery = "SELECT Lfr_Name, Adresse FROM LIEFERANTEN";
rSet = stmt.executeQuery( sqlQuery );
}
catch ( Exception e )
{
System.out.println( "Fehler bei Datenbankzugriff" + e );
return;
}
try
{
while ( rSet.next() )
System.out.println ( rSet.getString(1) + "\t" + rSet.getString(2) );
stmt.close();
conn.close();
}
catch ( Exception e )
{
System.out.println( "Fehler bei Tabellenabfrage" + e );
return;
}
}
}
Die Verbindung zu einer Datenbank wird über die Klasse DriverManager und der Schnittstelle Connection aufgebaut. Alle verwendeten Pakete liegen unter java.sql.*. Vor der Ausführung der JDBC-Befehle muss ein passender Datenbanktreiber geladen werden. In unserem Beispiel verwenden wir die von JavaSoft mitgelieferte JDBC-ODBC-Bridge, deren Klasse unter dem Namen JdbcOdbcDriver verfügbar ist. Damit können wir uns also zu allen Datenquellen mit einer ODBC-Schnittstelle verbinden.
Alle Datenbanktreiber müssen an einer Stelle gesammelt werden, dem Treibermanager. Dazu dient eine besondere Java-Klasse, die Klasse DriverManager. Wie wir später sehen werden, bietet der Treibermanager eine Methode getConnection() an, mit der wir eine Verbindung zur Datenbank aufbauen.
|
Die statische Methode getDrivers() der Klasse DriverManager liefert eine Aufzählung der angemeldeten Treiber. Alle Methoden der Klasse sind statisch, da sich ein Exemplar dieser Klasse nicht erzeugen lässt; der Konstruktor ist privat. Ein normales Programm hat in der Regel keinen angemeldeten Datenbanktreiber. Eine Aufzählung bekommen wir durch folgende Zeilen:
for ( Enumeration e = DriverManager.getDrivers();
e.hasMoreElements(); )
System.out.println( e.nextElement().getClass().getName() );
Die Elemente, die durch die Enumeration ausgelesen werden, sind Treiber-Objekte. Jeder Datenbanktreiber ist als Treiber-Objekt implementiert. Da Driver aber eine Schnittstelle ist, gibt es keine sinnvolle toString()-Methode, und wir bekommen den Klassennamen und einen Hashwert. Schöner ist der Klassename, den wir über ein Class-Objekt erfragen können. getClass() liefert das Klassenobjekt und getName() den Namen. Ohne geladenen Treiber bekommen wir keine Ausgabe. Laden wir den JDBC-ODBC-Treiber, etwa durch
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
sun.jdbc.odbc.JdbcOdbcDriver
Zu Testzwecken bietet sich eine Ausgabe der Informationen an, Informationen des Treibers und der Datenbank in einen speziellen Ausgabekanal zu schreiben. Wir können die Log-Informationen so umlenken, dass sie in den Standard-Ausgabestrom geschrieben werden. Dazu dient die statische Methode setLogWriter(). Die bekommt einen PrintWriter als Parameter. Vor Java 1.2 hieß die Methode noch setLogStream(), diese nahm als Parameter einen PrintStream. Sie ist nun veraltet.
Folgende Zeile gibt alle Informationen auf dem Bildschirm aus, die in das Logbuch geschrieben werden:
DriverManager.setLogWriter( new PrintWriter(System.out) );
Es bietet sich eine Ausgabe an, da so interessante Aussagen über die Funktionsweise der Treiber offenbar werden. Zur Set-Methode existiert die passende getLogWriter()-Methode, die den PrintWriter zurückgibt. Eine Anfrage an getLogWriter() gibt null zurück, was verrät, dass standardmäßig keine Ausgabe stattfindet. Eine versteckte Datei wird also nicht erzeugt.
Testen wir die Ausgabe, die die beiden Programmzeilen erzeugen:
DriverManager.setLogWriter( new PrintWriter(System.out) );
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
Zunächst setzen wir die Ausgabe auf den Standard-Ausgabekanal. Dann laden wir die Treiberklasse für die JDBC-ODBC-Brücke. An dieser Stelle greifen wir schon etwas vor, doch zeigt die Ausgabe, was an dieser Stelle geschieht:
DriverManager.initialize: jdbc.drivers = null
JDBC DriverManager initialized
registerDriver: driver[className=sun.jdbc.odbc.JdbcOdbcDriver,\
sun.jdbc.odbc.JdbcOdbcDriver@1fcc69]
Die Methode setLogWriter() ist die erste Methode, die die Klasse DriverManager benutzt. Daher bekommt der Klassenlader die Aufgabe, die Klasse DriverManager zu laden. setLogWriter() speichert dann das PrintWriter-Objekt in einer privaten Variablen und macht sonst nichts.
Erst das Laden eines Treibers führt zum Aufruf der statischen initialize()-Methode. Sie führt die private Methode loadInitialDrivers() aus, die zur ersten Ausgabezeile führt. Hier sind noch keine Treiber angemeldet, da in den Eigenschaften »jdbc.drivers« nichts eingetragen ist. Diese Eigenschaft wird in der Regel dann gesetzt, wenn von außen über den Schalter »-D« eine Klasse angesprochen wird. Nach dem Suchen in den Eigenschaften folgt die Ausgabe »JDBC DriverManager initialized«. Nun hat der Treiber die DriverManager-Klasse hochgefahren und der Treibermanager kann den Treiber anmelden. Der Treiber ist vom Typ Driver. Dieser wird zusammen mit dem zugehörigen Class-Objekt und einem Namen in der internen Klasse DriverInfo in einem internen Vector gespeichert. Die Ausgabe »registerDriver:[...]« kommt von der Anmeldung des Treibers. Wir sehen genau die Informationen, die in der Klasse DriverInfo gespeichert sind. Die Ausgabe wird auch von toString() von DriverInfo generiert.
Nicht nur Treiber und SQL-Klassen nutzen den Log-Stream, auch wir können Zeichenketten ausgeben. Dazu dient die statische Methode println(), die als Parameter nur einen String annimmt. println() ist so implementiert, dass bei einem nicht gesetztem Log-Stream die Ausgabe ausbleibt.
Der Datenbanktreiber ist eine ganz normale Java-Klasse, die sich bei einem Treibermanager automatisch anmeldet. Unsere Aufgabe ist es nur, ein Treiber-Objekt einmal zu erzeugen. Um eine Klasse zur Laufzeit zu laden und so ein Laufzeit-Objekt zu erschaffen, gibt es mehrere Möglichkeiten. Eine davon geht über die native statische Methode forName() der Klasse Class. Die Syntax für das Laden der JDBC-ODBC-Bridge lautet somit:
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
Um einen Oracle-JDBC-Treiber zu laden, schreiben wir folgende Zeile:
Class.forName( "oracle.jdbc.driver.OracleDriver" );
Der Klassenname des Datenbanktreibers für mSQL-JDBC heißt com.imaginary. sql.msql.MsqlDriver. Um möglichst unabhängig zu bleiben, sollte die Klasse auch nicht hart einkodiert werden. Besser ist es, den Klassennamen in eine Property zu schreiben. Dennoch bleiben wir in unseren Beispiel bei einem einkodierten Treiber.
Class.forName( "com.imaginary.sql.msql.MsqlDriver" );
Da wir die Klasse nur Laden, aber die Referenz auf den Klassen-Deskriptor nicht benö-tigen, belassen wir es bei einem Aufruf und beachten den Rückgabewert nicht. Diese Operation löst eine ClassNotFoundException aus, falls die Klasse nicht gefunden wurde.
Die Klasse muss nicht zwingend zur Laufzeit geladen werden. Sie kann auch in der Kommandozeile über den Schalter »-D« eingebunden werden. Dazu setzen wir mit der Eigenschaft jdbc.drivers einen Datenbanktreiber fest.
java -Djdbc.drivers=sun.jdbc.odbc.JdbcOdbcDriver <Javaklasse>
final class java.lang.Class |
| static Class forName( String clazz ) throws ClassNotFoundException Sucht, lädt und bindet die Klasse mit dem qualifizierten Namen clazz ins Laufzeitsystem ein. Es wird ein Class-Objekt zurückgegeben, falls die Klasse geladen werden kann, andernfalls wird dies mit einer ClassNotFoundException quittiert. |
Wenn wir selbst einen Treiber schreiben müssten, würden wir zunächst einmal die Schnittstelle Driver implementieren. Dann müssten wir alle sechs Methoden programmieren. Zur Demonstration soll einmal ein fiktiver Treiber umgesetzt werden, der Treiber usql5 .
Einen Konstruktor brauchen wir nicht implementieren, da Class.forName() oder ähnliche Programmanweisungen die Klasse laden, das Objekt aber erst einmal nicht erzeugen.
import java.sql.*;
import java.util.Properties;
public class UsqlDriver implements Driver
{
Die erste Methode ist acceptsURL(), nach der später der Treibermanager entscheidet, ob ein Treiber eine Datenbank ansprechen kann oder nicht. Der Treibermanager wird alle Treiber mit einer URL fragen, ob sie den Job übernehmen. Daher muss acceptsURL() nun testen, ob unser usql-Treiber sich nun für eine Datenbank verantwortlich fühlt, wenn als Protokoll »usql« in der URL steht. Die URL beginnt immer mit dem Präfix »jdbc:«. Dahinter steht der Name des Treibers. Also testen wir die Stelle 5 bis 9 und schauen, ob dort »usql« steht. Wenn ja, dann sollten wir den Job übernehmen und true zurückgeben. Dann wird der Treibermanager nicht mehr andere Treiberklassen fragen, sondern uns den Auftrag erteilen.
public boolean acceptsURL( String s ) throws SQLException
{
s = s.trim();
if ( s.length() < 11 )
return false;
else
return s.substring(5, 9).equals("usql");
}
Bekommen wir den Auftrag, dann dürfen wir die Verbindung aufbauen. Da wir allerdings auch hintenherum ohne den Treibermanager mittels connect() eine Verbindung aufbauen können, testen wir noch einmal zur Sicherheit, ob die URL wirklich uns meint. Ist alles in Ordnung, erzeugen wir ein UsqlConnection-Objekt, das an anderer Stelle programmiert werden muss.
public synchronized Connection
connect( String s, Properties properties ) throws SQLException
{
if ( !acceptsURL(s) )
return null;
else
return new UsqlConnection( this, s, properties );
}
Die Klasse DriverPropertyInfo nimmt Informationen für die Verbindung auf, die von getPropertyInfo() abgefragt werden können. Wir wollen hier keine Eigenschaften angeben und nur ein leeres Feld zurückgeben.
public DriverPropertyInfo[]
getPropertyInfo( String s, Properties prop )
throws SQLException
{
return new DriverPropertyInfo[0];
}
|
Zwei weitere Methoden erlauben den Programmierern die Treiber-Version zu erfragen. Da sich unser Treiber am Anfang seiner Entwicklung befindet, ist die Version 0.1. So etwas wie 0.001 funktioniert leider nicht, da der Rückgabewert immer ein Integer ist.
public int getMajorVersion() {
return 0;
}
public int getMinorVersion() {
return 1;
}
Da wir ein echter JDBC-Treiber sind, der das komplette JDBC-API sowie SQL-92 unterstützt, geben wir bei jdbcCompliant() selbstbewusst true zurück.
public boolean jdbcCompliant() {
return true;
}
Nun ist keine Methode mehr zu implementieren, und das Spannendste wird im statischen Initialisierungsblock gemacht.
static {
try {
DriverManager.registerDriver( new UsqlDriver() );
}
catch ( SQLException e ) { System.out.println( e ); }
}
}
Wir sehen hier, dass der Treiber sich selbstständig im DriverManager anmeldet. Dazu bildet er ein Exemplar von sicht selbst über den Standard-Konstruktor. Sollte hier etwas schief gehen, dann fangen wir eine SQLException auf. Dies müssen wir auch, da uns die Methode registerDriver() vom Treibermanager diese Ausnahmebehandlung vorschreibt.
Nun können wir eine Verbindung zur Datenbank mit Hilfe des Connection-Objekts aufbauen, welches von DriverManager.getConnection() zurückgegeben wird. Eine Verbindung wird mit speziellen Optionen parametrisiert, unter anderem mit dem Treiber, der die Datenquelle anspricht.
| Alle Datenquellen sind durch eine besondere URL qualifiziert, die folgendes Format besitzt: |
| Für ODBC-Datenquellen ist das Subprotokoll mit »odbc« zu detaillieren: |
| Eine URL für mSQL-JDBC hat immer das Format: |
Die getConnection()-Methode liefert nun ein Connection-Objekt, das mit der Quelle verbunden ist. Die nachfolgende Anweisung verbindet uns mit einer Datenbank, die den Namen Pflanzen trägt. Diesen Namen haben wir im ODBC-Datenquellen-Administrator festgelegt.
con = DriverManager.getConnection( "jdbc:odbc:Pflanzen",
"user",
"passwd" );
Die Methode getConnection() erwartet bis zu drei Parameter: Die URL der Datenbank, zu der die Verbindung aufgenommen werden soll, ist der Pflichtparameter. Der Anmeldename und das Passwort sind optional. Der Benutzername und das Passwort können auch leere Strings ("") sein. Dieses Vorgehen findet bei Text-Dateien, die als ODBC-Quellen eingesetzt werden, Verwendung, da Texte keine solchen Attribute besitzen. Meldet getConnection() keinen Fehler, so liefert sie uns eine geöffnete Datenbankverbindung.
class java.sql.DriverManager |
| static Connection getConnection( String url, Properties info ) throws SQLException Versucht eine Verbindung zur Datenbank aufzubauen. Die Klasse DriverManager sucht dabei einen aus der Liste der registrierten JDBC-Treiber passenden Treiber für die Datenbank. Im Properties-Objekt sollten die Felder »user« und »password« vorhanden sein. |
| static Connection getConnection( String url, String user, String password ) throws SQLException Versucht eine Verbindung zur Datenbank aufzubauen. user und password werden zur Verbindung zur Datenbank verwendet. |
| static Connection getConnection( String url ) throws SQLException Versucht eine Verbindung zur Datenbank aufzubauen. |
|
Es lohnt sich, einmal hinter die Kulissen der Methode getConnection() zu schauen. Das DriverManager-Objekt wird veranlasst, die Verbindung zu öffnen. Dabei versucht es einen passenden Treiber aus der Liste der JDBC-Treiber auszuwählen. Seine Treiber verwaltet die Klasse DriverManager in einem privaten Objekt DriverInfo. Dieses enthält ein Treiber-Objekt (Driver), ein Objekt (securityContext) und den Klassennamen (className). Während getConnection() die Liste (intern als Vector implementiert) der DriverInfo-Objekte abgeht, versucht dieser sich über die connect()-Methode anzumelden. Merkt der Treiber, dass er mit der URL nicht viel anfangen kann, gibt er null zurück, und getConnection() versucht den nächsten Treiber. Ging alles daneben und keiner der angemeldeten Treiber konnte etwas mit dem Subprotokoll anfangen, bekommen wir eine SQLException("No suitable driver", "08001").
Die Klasse DriverManager besitzt keine close()-Methode, wie wir erwarten können. Vielmehr kümmert sich das Connection-Objekt selbst um die Schließung. Würde der Garbage-Collector also das Objekt von der Halde räumen, schließt er automatisch die Verbindung. Wollen wir selbst das Ende der Verbindung herbeiführen, rufen wir
con.close();
auf und die Verbindung wird beendet. Auch hier kann eine SQLException auftauchen.
interface java.sql.Connection |