Datenbanken 12. Übung JDBC JDBC: Datenbankzugriff-API - Interface für den (entfernten) Datenbankzugriff auf Java-Programme Applikation kann unabhängig vom darunterliegenden DBMS programmiert werden setzt die Idee von ODBC auf Java um gemeinsame Grundlage ist der X/Open SQL CLI (Call Level Interface) Standard Durch das JDBC-API werden folgende Funktionen zur Verfügung gestellt: 1. Aufbauen einer Verbindung zur Datenbank (DriverManager, Connection) 2. SQL-Anfragen und Update-Anweisungen an die Datenquelle senden 3. Zugriff auf die Ergebnisse einer SQL-Anfrage (ResultSet) das JDBC-API besteht aus Klassen und Interfaces. - Kern: Treiber-Manager darunter: Treiber für die einzelnen DBS Angestrebt: - DBMS-Client-Server-Netzwerkprotokoll mit pure Java-Treiber JDBC-Netz mit pure Java-Treiber Übergangslösungen: - - JDBC-ODBC-Bridge und ODBC-Treiber: Über die JDBC-ODBC-Bridge werden die ODBCTreiber verwendet Natives API: Der JDBC-Treiber nutzt Bibliotheken, die vom DBMS-Hersteller angeboten werden. Auch dieser Treibertyp kommt nicht ohne Binärcode aus und kann daher bspw. nicht in Java-Applets verwendet werden. JDBC-Net-Treiber: Zwischen JDBC-Treiber und DBMS wird eine zusätzliche datenbankunabhängige Komponente (Middleware) eingeführt. Der Treiber kommuniziert über ein DBMS unabhängiges Protokoll mit der Middlewre, er kann also auf der Client-Seite völlig unabhängig von einem konkreten DBMS gestaltet und vollständig in Java implementiert werden. Ein Treiber dieser Art eignet sich für den Einsatz in Java-Applets. Zwischen der Middleware und einem konkreten DBMS wird ein DBMS-spezifisches Protokoll verwendet. 1 Datenbanken - Native Protokoll Treiber: Ein Treiber dieser Art kommuniziert direkt mit einer netzwerkfähigen Datenbank im jeweiligen, DBMS-spezifischen Protokoll. Da indiesem Fall der Client direkt mit dem Server verbunden ist, erlaubt diese Treibervariante einen effizienten Datenbankzugriff. JDBC-Treiber-Manager Driver Manager - verwaltet (registriert)Treiber wählt bei Verbindungswunsch den passenden Treiber aus und stellt Verbindung zur Datenbank her Ein JDBC-Treiber ist eine Sammlung von Klassen, die u.a. die Interfaces des JDBC-API implementieren, zur Ansteuerung einer konkreten Datenquelle. Eine Art Basisklasse für den Treiber stellt das Interface Driver dar. Jede Driver-Implementierung besitzt einen Konstruktor, der eine Instanz der Klasse erstellt und diese über die Methode DriverManager.registerDriver() die Liste der verfügbaren Treiber hinzufügt. Es gibt zwei Methoden, die dafür sorgen, dass der Konstruktor der Klasse Driver ausgeführt wird: 1. Aufruf der Methode Class.forName(String KlassenName) 2. Hinzufügen der Driver.Klasse zu der Systemeigenschaft (Property) jdbc.drivers Verbindungsaufbau - Aufruf an den DriverManager: Connection name = DriverManager.getConnection(jdbcurl,user-id,passwd) - Datenbank wird eindeutig durch JDBC-URL bezeichnet. -- jdbc:subprotokoll:subname -- subprotokoll bezeichnet die Datenbank -- subname bezeichnet die Treiberart Bsp.: jdbc:oracle:thin:@rfhs8012.de:1523:ora10g Connection connection = DriverManager.getConnection(url, "xyz12345") liefert eine offene Verbindungsinstanz connection. "xyz12345" , Verbindungsaufbau beenden: connection.close(); Versenden von SQL-Anweisungen Statement-Objekte - werden durch Aufruf von Methoden einer bestehenden Verbindung connection erzeugt Statement: einfache SQL-Anweisungen ohne Parameter PreparedStatement: Vorkompilierte Anfrage, Anfrage mit Parametern CallableStatement: Aufruf von gespeicherten Prozeduren Das Statement-Interface Über die Connection wird eine treiberspezifische Implementierung des Statement-Interfaces angefordert Statement name = connection.createStatement(); Über das Statement wird dann eine Anweisung an das DBMS gesendet - ResultSet statement.executeQuery(string1) 1 string: SQL-Statement ohne Semikolon 2 Datenbanken Anfragen an die Datenbank. Dabei wird eine Ergebnismenge zurückgegeben. - int statement.executeUpdate(string) Einfüge-, Änderungs-, Löschanweisung. Der Rückgabewert stellt die Anzahl der Zeilen dar, die von der Anweisung bearbeitet wurden - statement.execute(string) statement gibt mehr als eine Ergebnismenge zurück (Folge von Tupeln). Ergebnismengen sind über das Statement-Objekt abfragbar. Ein Statement-Objekt kann beliebig oft wiederverwendet werden, um SQL-Anweisungen zu übermitteln. Mit der Methode close() kann ein Statement-Objekt geschlossen werden. Behandlung von Ergebnismengen Klasse ResutSet ResultSet name = statement.executeQuery(string); - - ResultSet-Objekt unterhält einen Cursor, der mit result-set.next() auf das nächste Tupel gesetzt wird. Ein Cursor ist ein Iterator über einer Liste von Tupeln, d.h. ein Zeiger der vor- (und seit JDBC 2.0 auch zurück-) gesetzt werden kann. Das Resultset-Objekt bietet verschiedene Methoden zur Steuerung der Positionen des Cursors an, mit denen der Cursor relativ zu seiner vorherigen Position (z.B. next()) oder absolut (z.B. first() oder absolut(int row) positioniert werden kann. result-set.next() liefert den Wert false, wenn alle Tupel gelesen wurden. Zugriff auf die einzelnen Spalten des Tupels unter dem Cursor mit resultset.get<type>(attribute). <type> ist ein Java-Datentyp zur Spezifikation der get-Methode (getInt(), getFloat(), getBoolean(), getDate(), getTime(), getString()). Mit get<type> werden die Daten des Ergebnistupels (SQL-Datentypen) in Java-Typen konvertiert. getString() funktioniert immer und deckt die SQL-Typen CHAR, VARCHAR ab. "attribute" kann entweder durch Attributnamen oder durch Spaltennummern angegeben sein. Falls eine get<type> Methode des Result-Objekts aufgerufen wird, versucht der JDBC-Treiber den jeweiligen SQL-Typ in den gewünschten Java-Typ zu konvertieren. Diese Konvertierung ist jedoch nicht für alle Typkombinationen möglich2. JDBC-Datentypen - JDBC steht zwischen Java (Objekttypen) und SQL. java.sql.types definiert generische SQL-Typen, mit denen JDBC arbeitet Java-Typ String java.math.BigDecimal boolean byte short int long float double java.sql.Date java.sql.Time JDBC-SQL-Typ CHAR, VARCHAR NUMBER, NUMERIC, DECIMAL BIT TINYINT SMALLINT INTEGER BIGINT REAL FLOAT, DOUBLE DATE(Tag,Monat,Jahr) TIME(Stunde,Minute,Sekunden) Abb.: Abbildungsvorschrift zwischen Java- und JDBC-Typen Informationen über die Spalten von Ergebnismengen 2 Es sollte die Abbildungsvorschrift zwischen Java-Typen und JDBC-Typen eingehalten werden. 3 Datenbanken ResutSetMetaData name = resultSet.getMetaData() erzeugt ein ResultSetMetaDataObjekt, das Informationen über die Ergebnismenge enthält. Methode int getColumnCount() String getColumnLabel(int) String getTableName(int) String getSchemaName(int) int getColumnType(int) String getColumnTypeName(int) - Beschreibung Spaltenanzahl der Ergebnismenge Attributname der Spalte Tabellenname der Spalte Schemaname der Spalte JDBC-Typ der Spalte Unterliegender DBMS-Typ der Spalte keine NULL-Werte in Java. resutSet.wasNull() testet, ob der zuletzt gelesene Spaltenwert NULL war. Die Ausgabe der aktuellen Zeile eines ResultSets kann man damit so vornehmen: ResultSetMetaData rsetmetadata = rset.getMetaData(); int numCols = rsetmetadata.getColumnCount(); for (int i = 1; i <= numCols; i++) { String returnValue = rset.getString(i); if (rset.wasNull()) System.out.println("null"); else System.out.println(returnValue); } - Mit der Methode close() kann ein ResultSet-Objekt explizit geschlossen werden. Neben dem Statement-Interface gibt es im JDBC-API zwei Erweiterungen: CallableStatementInterface und PreparedStatement-Interface Prepared Statements Dieses Interface fügt zu den bereits im Statement-Interface definierten Methoden die Möglichkeit hinzu, eine SQL-Anweisung ohne konkrete Parameter an die Datenbank zu senden. PreparedStatement name = connection.prepareStatement(string); - - SQL-Anweisung string wird vorcompiliert. Damit ist die Anweisung fest im Objektzustand enthalten. Das ist effizienter als Statement, wenn ein SQL-Statement häufig ausgeführt werden soll. Abhängig von string ist nur eine der parameterlosen Methoden prepared-statement.executeQuery() prepared-statement.executeUpdate() prepared-statement.execute() anwendbar Parameter - Eingabeparameter werden durch "?" repräsentiert, z.B. PreparedStatement pstmt = conn.prepareStatement("SELECT Population FROM country WHERE Code = ?"); "?"-Parameter werden mit prepared-statement.set<type>(pos,value) gesetzt, bevor ein PreparedStatement ausgeführt wird. <type> : Java-Datentyp pos : Position des zu setzenden Parameters value : Wert Bsp.: pstmt.setString(1,"D"); ResultSet rset = pstmt.executeQuery(); … pstmt.setString(1,"CH"); ResultSet rset = pstmt.executeQuery(); 4 Datenbanken … - Nullwerte werden durch setNULL(pos,<type>) gesetzt, wobei <type> den JDBC-Typ dieser Spalte bezeichnet, z.B. pstmt.setNULL(1,Types.String); Callable Statements Ein Callable-Statement erweitert ein Prepared-Statement um die Möglichkeit Datenbankprozeduren aufzurufen, und die Ergebnisse dieser Prozeduren von der Datenbank anzufordern. - - Erzeugen von Prozeduren und Funktionen mit statement.executeUpdate(string); (string ist von der Form CREATE PROCEDURE ... ) Bsp.: String s = "CREATE PROCEDURE bla() IS BEGIN ... END"; stmt.executeUpdate(s); der Aufruf der Prozedur wird als CallableStatement-Objekt erzeugt die Aufrufsyntax von Prozeduren ist bei den verschiedenen Datenbanksystemen unterschiedlich JDBC verwendet eine generische Syntax. CallableStatement name = connection.prepareCall("{call procedure}"); cstmt = connection.prepareCall("{call bla()}"); Callable Statements mit Parametern: CallableStatement name = connection.prepareCall("{call procedure(?,…,?)}"); Rückgabewert bei Funktionen: CallableStatement name = connection.prepareCall("{? = call procedure(?,…,?)}"); Ob die einzelnen "?"-Parameter IN, OUT, oder INOUT-Parameter sind, hängt von der Prozedurdefinition ab und wird von der JDBC anhand der Metadaten der Datenbank selbstständig analysiert. Für OUT-Parameter bzw. den Rückgabewert muß zuerst der JDBC-Datentyp der Parameter mit cstmt.registerOutParameter(pos,java.sql.Types.<type>) registriert werden. Zum Lesen des Parameters wird die entsprechende get<type>()-Methode verwendet. IN-Parameter werden über set<type> gesetzt, z.B. cstmt.setString(2,"Regensburg"); Für INOUT-Parameter muß ebenfalls registerOutParameter aufgerufen werden. Sie werden entsprechend mit set<type() gesetzt bzw. mit get<type>() gelesen. Aufruf mit ResultSet name = callable-statement.executeQuery() oder callable-statement.executeUpdate() oder callable-statement.execute() Sequentielle Ausführung - - - SQL-Statements, die mehrere Ergebnismengen zurückliefern: statement.execute(string), prepared-statement.execute(), callablestatement.execute(). Nach der Ausführung mit execute() kann mit getResultSet() bzw. mit getUpdateCount() die erste Ergebnismenge bzw. der erste Update-Zähler entgegengenommen werden. Um weitere Ergebnismengen zu holen, muß getMoreResults() und dann wieder getResultSet() bzw. getUpdateCount() aufgerufen werden. getResultSet() bzw. getUpdateCount(): nächsten Rückgabewert bzw. Update-Zähler aufrufen. Falls beim Aufruf getResultSet() das nächste Ergebnis eine Ergebnismenge ist, wird diese zurückgegeben. Ist kein nächstes Ergebnis mehr vorhanden, oder das nächste Ergebnis keine Ergebnismenge sondern ein Update-Zähler, so wird null zurückgegeben. Falls das nächste Ergenis ein Update-Zähler ist, wird dieser (eine Zahl n >= 0) zurückgegeben.. Ist 5 Datenbanken - das nächste Ergebnis eine Ergebnismenge oder liegt kein weiteres Ergebnis mehr vor, wird – 1 zurückgegeben. Ist der Aufruf von getMoreResults() true , dann liegt eine Ergebnismenge vor, bei false ist es ein Update-Zähler bzw. es sind keine weiteren Ergebnis abholbereit. Alle Ergebnisse sind verarbeitet, wenn ((statement.getResultSet() == null) && (statement.getUpdateCount() == -1)) bzw. ((statement.getMoreResults() == false) && (statement.getUpdateCount() == -1) ist. SQL-Exception - Nahezu jede JDBC-Methode kann eine SQL-Exception als Anwort auf einen Datenzugriffsfehler auswerfen Falls mehr als ein Fehler auftritt, werden diese Fehler verkettet Eine SQL-Exception enthält -- die Fehlerbeschreibung, die mit getMessage() beschafft werden kann -- den SQL-Status, der die Annahme festlegt und mit getSQLState() beschafft werden kann. -- Einen vom Datenbank-Hersteller spezifizierten Fehler-Code, der mit getErrorCode() ermittelt werden kann. -- Eine Verkettungsanweisung, die auf die nächste SQLException mit getNextException() verweist. Transaktionen - - - - Defaultmäßig wird nach jeder ausgeführten SQL-Anweisung automatisch ein COMMIT an die Datenbank gegeben commit kann ausgeschaltet werden mit connection.setAutoCommit(false) Mit commit können die Veränderungen der Datenbank permanent gesichert werden. Falls Fehler auftreten, soll mit rollback der Zustand vor Eintreten des Fehlers wiederhergestellt werden. rollback() löst auch alle Locks, die vom zuständigen Connection-Objekt gehalten werden. Zur Vermeidung von Konflikten während einer Transaktion verwendet ein DBMS Sperren (locks). Daten, auf die eine Transaktion zugreift, bleiben für andere Transaktionen gesperrt Gesetzte Sperre bleibt solange in Kraft, bis die Transaktion festgeschrieben oder zurückgerollt wurde. Isolationsgrad für Transaktionen bestimmt, wie Sperren gesetzt werden. Das Interface Connection enthält Konstanten, die die Isolationsgrade für Transaktionen repräsentieren, die man in JDBC verwenden kann: TRANSACTION_NONE, TRANSACTION_READ_COMMITTED, TRANSACTION_SERIALIZABLE Das Interface Connection stellt eine Methode zur Verfügung, mit der man den Isolationsgrad abfragen kann: int isoGrad = connection.getTransactionIsolation(). AngJobAbt.java HalloJdbcAnw.java InsertBsp.java LobExample.java OraJdbc.java PLSQLExample.java PLSQLIndexTab.java ShowBlobImage.java ShowClobText.java ueb12a1.sql ueb12a2.sql 6 Datenbanken Dokumentation:3 1. Treiber für den JDBC-Zugriff 2. Installation des JDBC-Treibers 3. Grundlagen der JDBC-Anwendung 3.1 Laden des JDBC-Treibers 3.2 Öffnen einer Datenbank 3.3 Senden von SQL-Anweisungen an die Datenbank 3.4 Transaktionsbetrieb 3.5 Hinweise zur Fehlerbehandlung 4. Klassen und Interfaces der JDBC-Schnittstelle 4.1 Die Klasse DriverManager 4.2 Das Interface Connection 4.3 Das Interface CallableStatement 4.4 Das Interface PreparedStatement 4.5 Das Interface Statement 4.6 Das Interface Resultset 5. JDBC-Treiber von Oracle 3 Roland Falkenberg: Schnittstellen zur Programmierung von (Oracle-) Datenbanken, Diplomarbeit an der F.H. Regensburg 7 Datenbanken Dokumentation 1. Treiber für den JDBC-Zugriff Jeder Treiber stellt dem Client eine einheitliche Schnittstelle zur Verfügung. Der Aufbau der einzelnen Treiber ist intern unterschiedlich und auf ein bestimmtes Zugriffsverfahren, oder aber eines der vorher genannten Zweioder Dreischichtenmodelle zugeschnitten. Insgesamt gibt es vier verschiedene Typen von JDBC-Treibern. Java-Anwendung JDBC-Treibermanager JDBC-ODBCBridge Treiber Treiber für DBMS B (Java) ODBC-Treiber Treiber für DBMS B (C) Standard - JDBCTreiber Treiber für DBMS D (Java) Middleware DBMS A Typ 1 DBMS B Typ 2 Abb.1: JDBC-Treibertypen 1. 2. 3. 4. DBMS C DBMS D Typ 3 100 % Java Typ 4 100 % Java Typ 1 ist eine schnelle Lösung für jede beliebige ODBC-Datenbank. Die JDBC-ODBC-Bridge stützt sich nach unten hin auf einen bereits vorhandenen ODBC-Treiber und greift dabei einfach auf dessen Funktionalität zurück. Dadurch kann man bereits zu einem frühen Zeitpunkt auf jede x-beliebige ODBC-Datenbank zugreifen. Allerdings hat diese Methode einen gewichtigen Nachteil: der JDBC Treiber besteht nicht aus reinem JavaBytecode, sondern zu einem Großteil auch aus dem ODBC-Treiber, d.h. die Portabilität des Treibers ist nicht gewährleistet. Clients, die diesen Treiber verwenden sind dadurch an die Plattform gebunden, auf der der ODBC-Treiber läuft und sind zusätzlich auf die Installation des ODBC-Treibers auf jedem ClientRechner angewiesen. Ein Typ 1 Treiber ist daher nur eine Übergangslösung für ODBC-Datenbanken, für die noch kein JDBCTreiber existiert. Ein Typ 2 Treiber ist eine attraktive Lösung für diejenigen Datenbankhersteller, die noch nicht in der Lage waren, einen eigenen JDBC-Treiber Typ 3 oder Typ 4 zur Verfügung zu stellen. Dieser Treibertyp ist als aufgesetzte Schicht schnell und einfach zu implementieren, da die Grundfunktionalität bereits in einem alten, z.B. in C geschriebenen Treiber, vorhanden ist. Auch hier handelt es sich demnach nur um eine plattformspezifische Übergangslösung. Typ 3 ist ein vollständig in Java geschriebener Treiber und bietet somit die größtmögliche Flexibilität. Er fügt sich nahtlos in das Dreischichtenmodell ein. Er kommuniziert über das Netzwerk mit einer Middleware. Diese bearbeitet sämtliche Anfragen. Ein Beispiel für einen Typ 3 Treiber ist der Treiber der Firma OpenLink. Dieser Treiber kann jede Datenbank ansprechen, die die Middleware unterstützt. Der vierte Typ von JDBC-Treibern ist ebenfalls rein in Java implementiert. Er unterstützt allerdings nicht das Dreischichten- sonder nur das Zweischichtenmodell. Auch der in der Beispielanwendung verwendete JDBC-Treiber von Oracle ist in diese Kategorie einzuordnen. Diese Treiber werden von den Herstellern für ihr DBMS selbst entwickelt und ersetzen nach und nach immer mehr die althergebrachten Typ 2 Treiber, die als Übergangslösungen am Markt existierten. Während ein Typ 4-Treiber aus der Sicht des Anwendungsentwicklers als ein monolithisches Stück Java-Software erscheint, ist die interne Struktur komplexer. Schließlich kann der Treiber in seiner Eigenschaft als Java-Klasse nicht unmittelbar Methoden 8 Datenbanken des Datenbanksystems aufrufen – bislang ist kein relationales DBMS in Java implementiert.4 Auch hier liegt zwischen Treiber und Datenbank eine Middleware, nur mit dem Unterschied, daß es sich um eine proprietäre Schicht des jeweiligen Herstellers handelt, die nach außen hin unsichtbar und dem Entwickler nicht zugänglich ist. Trotz der Vereinheitlichung in der Schnittstelle und der damit verbundenen Portabilität können auch durch JDBC Treiber nicht alle Probleme behoben werden. Diese Schwierigkeiten resultieren aus den unzähligen Datenbanksystemen, die auf dem Markt vorhanden sind. Datenbank ist nicht gleich Datenbank. So unterstützt z.B. jedes DBMS seine eigenen SQL-Features. Während einfache, grundlegende SQL-Anweisungen, wie etwa ein SELECT in jedem System gleich ist, sieht es bei proprietären Konstrukten ganz anders aus. Z.B. definiert der SQL-Standard einen SQLSTATE als Fehlerstatus der Datenbank. Oracle jedoch bietet zusätzlich dazu noch einen SQLCODE, der nicht nur andere Fehlernummern als SQLSTATE vergibt, sondern zusätzlich dazu statt einem String einen numerischen Wert zurückliefert. Weiterhin existieren in einigen Datenbanken Konstrukte wie Stored Procedures, bei weitem aber nicht in allen! Davon abgesehen wurden mittlerweile verschiedene Standards für SQL definiert, die in ständiger Überarbeitung auch weiterhin erweitert und verbessert werden. Der derzeit offiziell gültige Stand ist SQL2, aber das bedeutet nicht, daß alle DBMS diesen Stand voll unterstützen. Alleine für SQL2 wurden drei verschiedene Level defniert: Entry Level: Dieser Level enthält die wenigstens Features. Die Implementierung der Datenbank genügt geringsten Ansprüchen. Intermediate Level Full SQL: Der volle Sprachumfang wird zur Verfügung gestellt. „Um in dem Wirrwarr eine halbwegs einheitliche Linie fahren zu können, fordert Sun Microsystems von JDBCTreibern bzw. von den dahinterliegenden Datenbanksystemen, mindestens SQL2 Entry Level zu unterstützen.“ 5 Sun Microsystems bietet den Herstellern von JDBC-Treibern die Möglichkeit, ihr Produkt auf Standardkonformität zu testen. Bei Erfolg erhält es das Label „JDBC compliant“ und der Benutzer kann sicher sein, daß sein Treiber SQL2 Entry Level unterstützt. Natürlich können über einen solchen Treiber die verbleibenden SQL-Eigenschaften einer Datenbank genutzt werden, die diesem Standard noch nicht entspricht. Aus all diesen Gesichtspunkten resultieren einige Regeln, die der Anwendungsentwickler berücksichtigen sollte, wenn sein Produkt möglichst portierbar sein soll: - - - Bei der Ausprogrammierung sollte möglichst auf die Verwendung von DBMS-Spezifika verzichtet werden! Sobald eine spezielle Funktion der Datenbank genutzt wird, ist die Anwendung an dieses System gebunden. SQL2 Entry Level sollte eingehalten werden. Ist die darunterliegende Datenbank mit diesem Stand noch nicht ausgerüstet, so muß die Entwicklung auf die verbliebenen Eigenschaften des Datenbanksystems beschränkt werden. Immer auf das Label „JDBC compliant“ achten! JDBC und ODBC sind eng verwandt. Auch in JDBC existieren SQL-Escapes mit deren Hilfe DBMSunabhängige Anwendungen geschrieben werden können. Solche Escapes erleichtern z.B. das Einfügen eines Wertes in ein Datumsfeld, das von jedem DBMS in einem anderen Format verwaltet wird. Eine große Hilfe bei der „Erforschung“ der Möglichkeiten einer Datenbank sind die Metadaten. Über den JDBC-Treiber können vom DBMS Informationen über den Namen des Produkts, den Hersteller und sogar über Features des Systems abgefragt werden. Ein Aufruf von supportsStoredProcedures() beispielsweise gibt Aufschluß darüber, ob das Datenbanksystem Stored Procedures verwalten kann. Ob das System SQL2 Entry Level erfüllt, erfährt man über supportsANSI92EntryLevelSQL(). Die Oracle Corporation stellt für ihr DBMS einen JDBC-Treiber Typ 4 zur Verfügung. Dieser Treiber wurde für die Entwicklung des Java-Clients, der in einem späteren Kapitel vorgestellt wird, sowohl unter UNIX als auch unter Windows95 und Windows NT installiert. 4 5 Rainer Klute: JDBC in der Praxis, Addison-Wesley, S. 41 Rainer Klute: JDBC in der Praxis, Addison-Wesley, S. 45 9 Datenbanken 2. Installation des JDBC-Treibers Der JDBC-Treiber kann bei Oracle bezogen werden6. Die thin Versionen werden für einen Datenbankzugriff über das Internet benötigt. Die normale Version ermöglicht den Zugriff über ein lokales Netz. Die Versionsangaben bei den Files beziehen sich auf die Oracle Serverversion, für die der Treiber gedacht ist. Das Softwarepaket enthält eine ausführliche Installationsanleitung für die Betriebssysteme UNIX und Windows. Es sei darauf hingewiesen, daß die Pfadangaben, die in diesen Installationshilfen gemacht werden, unbedingt einzuhalten sind, d.h. daß die Files, die laut Beschreibung im [ORACLE HOME] Verzeichnis stehen sollen, auch unbedingt dort hinkopiert werden müssen! Weiterhin ist unter beiden Betriebssystemen die Umgebungsvariable CLASSPATH um den Pfadeintrag auf die classes111.zip ( ab JDK 1.1 ) zu ergänzen, also z.B. /soft/oracle7.3/jdbc/lib/classes111.zip unter UNIX. Um in einem Applet auf eine Datenbank zugreifen zu können muß sich eine Kopie der classes111.zip unbedingt im selben Verzeichnis befinden wie die Klasse, die den Zugriff auf die Datenbank initiiert! In der HTML-Datei, die ein Applet startet, muß unter den Tags für den Applet-Aufruf ein archive=“classes111.zip“ eingefügt werden. Für die Beispielanwendung wurde ein JDK-1.1.x kompatibler Treiber gewählt. Unter Unix beinhaltet das Softwarepaket eine Datei liboci73jdbc.so, deren Pfad entweder in der Umgebunsgvariablen LD_LIBRARY_PATH eingetragen werden muß, oder aber in ein Verzeichnis kopiert werden sollte, das in dieser Systemvariable bereits eingetragen ist. In gleicher Weise wird mit den Dateien oci73jdbc.dll und oci73jdbc_g.dll unter Windows verfahren. Der Eintrag erfolgt in die %PATH%-Umgebungsvariable. Ein geeigneter Ort, die Kopien dieser Dateien abzulegen, ist %ORACLE_HOME%/Bin, da dieser Pfad standardmäßig vom Oracle Installer in den Umgebungsvariablen eingetragen wird. 3. Grundlagen der JDBC-Anwendung 3.1 Laden eines JDBC-Treibers Das Laden der JDBC-Klassen erfolgt über zwei Import-Anweisungen: import java.sql.*; import java.math.*; Der erste Import stellt die reinen JDBC-Klassen zur Verfügung, der zweite fügt die BigDecimal Klassen hinzu. Weiterhin muß der Treiber selbst geladen werden. Dies geschieht über die Anweisung: Class.forName( "oracle.jdbc.driver.OracleDriver" ); Der Treiber selbst ist eine gewöhnliche Java-Klasse. Das ungewöhnliche an dieser Vorgehensweise ist, daß diese erst zur Laufzeit des Programms hinzugebunden wird. Hierfür bedient sich die Java Virtual Machine des Classloaders. Die Klasse, die geladen werden soll, muß sich in einem Pfad, einer ZIP- oder JAR- Datei befinden, die im CLASSPATH eingetragen ist (siehe Installationshinweise). Ist der Classloader nicht in der Lage, dieTreiberklasse hinzuzubinden, erzeugt er eine java.lang.ClassNotFoundException. Wird der Treiber erfolgreich geladen, meldet er sich mit seinem Initialisierungscode beim JDBC-Treibermanager an. Selbstverständlich kann ein Programm viele verschiedene JDBC-Treiber gleichzeitig laden, um verschiedene Datenbanken parallel anzusprechen. Dabei sind die einzelnen Treiber meistens herstellerspezifisch. Im Falle des Oracle-Treibers wird empfohlen, den JDBC-OCI Treiber zu registrieren. Dies erfolgt mit Hilfe des Aufrufs von 6 Bei direktem Ansteuern der Treibersoftware über den Browser ist die Einsendung einer Registrierung erforderlich: http://www.oracle.com Oracle stellt allerdings auch einen direkten Download über den hauseigenen FTP-Server zur Verfügung: ftp://ftp.oracle.com/pub/www/jdbc 10 Datenbanken DriverManager.registerDriver (new oracle.jdbc.driver.OracleDriver()); 3.2 Öffnen einer Datenbank Das Öffnen der Datenbank kann unter Verwendung zweier Methoden erfolgen. Die erste Methode, die nur für den direkten Aufruf des OCI –Connects geeignet ist, verwendet den Datenbankalias oder TNSName, der in der tnsnames.ora spezifiert wird. Connection conn = DriverManager.getConnection ("jdbc:oracle:oci7:@mydatabase","scott", "tiger"); Scott ist in diesem Fall der Username, Tiger das Passwort und @mydatabase der TNSName der Datenbank. Weiterhin ist angegeben, daß es sich um einen Aufruf des OCI7 Treibers handelt. Eine weitere Möglichkeit, das Öffnen der Datenbank durchzuführen, falls keine sauber erstellte Datei tnsnames.ora vorliegt, ist die Ergänzung des Strings um eine gültige SQL*Net Namensspezifikation: Connection conn = DriverManager.getConnection ("jdbc:oracle:oci7:@(description=(address=(host=myhost) (protocol=tcp)(port=1521))(connect_data=(sid=orcl)))","scott", "tiger"); Man erkennt die Ähnlichkeit zu den Eintragungen in der Datei tnsnames.ora. Im Connectstring werden direkt der Hostname, das Protokoll und der Port angegeben. Weiterhin enthält er eine SID (System Identifier), hier den String ‚orcl‘. Username und Passwort bleiben gleich. Diese Aufrufe gelten aber nur für eine Verbindung über das Oracle Call Interface von einem normalen JavaProgramm aus. Im Falle eines Applets wird der Zugriff auf die Datenbank über das Internet gewünscht. Selbstverständlich ist für diese Art der Verbindung das SQL*Net nutzlos, da nicht jeder Client-Rechner über eine Oracle-Installation verfügen muß, um online auf eine Datenbank zuzugreifen. Für diese spezielle Zugriffsmöglichkeit wurde der Oracle JDBC Thin Treiber konzipiert. Connection conn = DriverManager.getConnection ("jdbc:oracle:thin:scott/[email protected]:1527:oradb" ); Im Connectstring wird das ‚OCI7‘ durch ‚thin‘ ersetzt. Name und Passwort bleiben gleich, die Änderung in der Zusammensetzung des Strings ist optional. Nach dem @ wird der komplette Hostname eingetragen. Der Port 1527 ist derjenige, auf dem die Datenbank mit dem Identifizierer oradb über ihre Listener auf Anfragen wartet. Das verwendete Protokoll ist selbstverständlich TCP/IP. Auch für diesen String gibt es eine zweite Möglichkeit: Connection conn = DriverManager.getConnection ("jdbc:oracle:thin:@(description=(address=(host=myhost)(protocol=tcp) (port=1521))(connect_data=(sid=orcl)))", "scott", "tiger"); Der Connectstring im Falle eines JDBC-Aufrufs nennt sich JDBC-URL. Die Methode getConnection() gehört zur Klasse DriverManager. „Der Treibermanager überprüft, welcher der geladenen Treiber den angegebenen JDBC-URL öffnen kann, und ruft die entsprechende Methode dieses Treibers auf.“7 Als Ergebnis liefert die Methode ein Objet vom Typ Connection. Dieses Connection Objekt ist für alle weiteren Datenbankoperationen notwendig. 7 Rainer Klute: JDBC in der Praxis, Addison-Wesley, S. 74 11 Datenbanken 3.3 Senden von SQL-Anweisungen an die Datenbank Der Client sendet seine SQL-Anweisungen über ein Objekt vom Typ Statement oder PreparedStatement an die Datenbank. Um dieses Objekt zu generieren, wird eine Methode der Klasse Connection aufgerufen. Im nachfolgenden Beispiel sei die Variable con eine Instanz der Klasse Connection: Statement stmt = con.createStatement(); Die eigentliche SQL-Anweisung, INSERT, DELETE oder UPDATE, werden in einer Stringvariablen definiert, z.B. String SQL = “DELETE FROM Benutzer“; Achtung: im Falle des JDBC-Treibers von Oracle werden abschließende Strichpunkte innerhalb der SQLAnweisung als Fehler gewertet. Dabei handelt es sich mit großer Wahrscheinlichkeit um einen Fehler des Treibers, da der schließende Strichpunkt dem Standard von SQL (und auch des RDBMS von Oracle) entspricht! Der oben definierte String wird über das stmt-Objekt vom Typ Statement an die Datenbank geschickt: ResultSet rset = stmt.executeUpdate( SQL ); Die Methode executeUpdate() liefert 0 oder die Anzahl der betroffenen Datensätze zurück. Sie wird für INSERT, DELETE und UPDATE verwendet, sowie für DDL-Befehle, die keine Ergebnismenge erzeugen. Eine SELECT-Anweisung wird über die Methode: ResultSet rset = stmt.executeQuery( SQL ); auf der Datenbank abgesetzt. Dabei wird eine Ergebnismenge im ResultSet zur Verfügung gestellt. 3.3 Transaktionsbetrieb Normalerweise wird jedes SQL-Statement, das via JDBC an die Datenbank übermittelt wird, sofort und dauerhaft auf dem Datenbestand durchgeführt, das heißt, es wird ein automatischer COMMIT abgesetzt. In manchen Situationen ist es für den Erhalt der Datenkonsistenz zwingend erforderlich, daß mehrere Aktionen erfolgreich abgeschlossen sein müssen, bevor die Änderungen wirksam werden können, d.h. entweder alle SQLAnweisungen müssen erfolgreich ausgeführt werden, oder aber keine! Die Anwendung muß, um in den Transaktionsbetrieb zu schalten, die AutoCommit Funktion abschalten, die beim Verbindungsaufbau standardmäßig aktiviert wird. Da es sich um eine Eigenschaft der Verbindung zur Datenbank handelt, wird hierfür eine Methode der Klasse Connection verwendet: Connection.setAutoCommit(false); Um nun zu einem definierten Zeitpunkt die Änderungen wirksam werden zu lassen oder sie gänzlich zu verwerfen, kann auf der Datenbank mit Connection.commit() jegliche Änderung bestätigt werden, oder mit Connection.rollback() der Urzustand des Datenbestandes wieder hergestellt werden. Ob nun AutoCommit ein- oder ausgeschaltet ist, läßt sich über Connection.getAutoCommit() feststellen. „Die sogenannte Transaktions-Isolation regelt, wie Transaktionen voneinander abgeschottet sind. JDBC definiert fünf Abstufungen, die sich per Connection.setTransactionIsolation() einstellen lassen. Die aktuelle Einstellung verrät ein Aufruf von Connection.getTransactionIsolation().“8 Die einzelnen Stufen der Isolation sind als Konstanten in der Connection Klasse definiert. 8 Rainer Klute: JDBC in der Praxis, Addison-Wesley, S. 134 12 Datenbanken TRANSACTION_NONE TRANSACTION_READ_UNCOMMITTED TRANSACTION_READ_COMMITTED TRANSACTION_REPEATABLE_READ TRANSACTION_SERIALIZABLE Keine Unterstützung von Transaktionen Transaktion B kann Daten lesen, die Transaktion A geändert, aber noch nicht per commit() dauerhaft an die Datenbank übergeben hat („Dirty Read“). „Dirty Reads“ sind nicht möglich. Die Transaktion B kann nur solche Daten lesen, die Transaktion A bereits per commit() dauerhaft an die Datenbank übergeben hat. Da während der Ausführung von B jedoch parallele Transaktionen die gleichen Daten mehrfach ändern können, erhält B beim wiederholten Lesen eines Tupels nicht unbedingt immer die gleichen Daten („Non-repeatable read“). „Dirty Reads“ und „Non-repeatable reads“ erfolgen nicht. Es kann aber passieren, daß Transaktion A ein neues Tupel in eine Tabelle einträgt, das Transaktion B beim wiederholten Lesen erhält („Phantom read“). „Dirty reads“, „Non-repeatable reads“ und “Phantom reads“ erfolgen nicht. Transaktionen, die auf die gleichen Daten zugreifen, werden serialisiert, also nacheinander ausgeführt. Abb. 2: Stufen der Transaktions-Isolation in JDBC9 Die Datenbank arbeitet am schnellsten, wenn sich die Transaktionen nicht gegenseitig behindern, d.h. im Modus TRANSACTIONS_NONE. Maximale Datenintegrität wird nur durch den letzten Modus, TRANSACTION_SERIALIZABLE, gewährleistet. Der JDBC-Treiber initialisiert keinen dieser Modi, sondern verwendet die Voreinstellung der Datenbank. Eine Änderung des Modus löst einen COMMIT auf der Datenbank aus. Während einer laufenden Transaktion darf der Modus demnach nicht geändert werden. Informationen über die Art und Weise, wie die Datenbank Transaktionen unterstützt, können über die Methoden der Klasse DatabaseMetaData erhalten werden. 9 supportsTransactions() Database Management Systems nach SQL2 Standard unterstützen auf jeden Fall Transaktionen. supportsMultipleTransactions() gibt an, ob ein DBMS auch über verschiedene Verbindungen mehrere Transaktionen gleichzeitig unterstützt. getDefaultTransactionIsolation() Welche Isolationsstufe ist standardmäßig für die Datenbank eingestellt? supportsTransactionIsolationLevel(n) liefert die Information, ob ein DBMS eine bestimmte Isolationsstufe n unterstützt. dataDefinitionIgnoredInTransactions(), dataDefinitionCausesTransactionCommit(), supportsDataDefinitionAndDataManipulationTransactions(), supportsDataManipulationTransactionsOnly() liefern Informationen darüber, welche Operationen bei Datendefinitionsanweisungen innerhalb einer Transaktion durchgeführt werden. Rainer Klute: JDBC in der Praxis, Addison-Wesley, S.134 13 Datenbanken 3.5 Hinweise zur Fehlerbehandlung Jede Funktion, die eine Operation in Verbindung mit der Datenbank durchführen soll, muß entweder eine throws SQLException Anweisung beinhalten, oder den Anweisungsteil in einem try{}-Block durchführen. Generell existiert für jeden möglichen SQL-Fehler ( Exception ) eine eigene Fehlernummer und -meldung. Die standardisierte Fehlernumerierung erfolgt üblicherweise durch eine Variable SQLSTATE. Im Fall des JDBCTreibers von Oracle scheint das RDBMS diesen Standard jedoch nicht zu unterstützen (die entsprechende Funktion für das Auslesen des SQLSTATES existiert zwar im SQL-Package von Java, wird aber nicht verwendet). Oracle bietet produktspezifisch eine Ausgabe des sogenannten SQLCODE. Leider wird dadurch die Portabilität eines JDBC-Clients, der unter Oracle entwickelt wurde, vermindert. Die entsprechende Funktion zur Abfrage des SQLCODE ist eine Methode von SQLException und lautet getErrorCode(). Während SQLSTATE gewöhnlich durch eine CHAR Repräsentation der Fehlernummer dargestellt wird, liefert der SQLCODE einen Integer Wert. 4. Klassen und Interfaces der JDBC-Schnittstelle 4.1 Die Klasse DriverManager Der DriverManager ist in der Lage, eine größere Anzahl von JDBC-Treibern zu verwalten. Sollte in der Systemumgebung ein JDBC-Treiber als Standard definiert sein, so versucht der DriverManager, diesen zu laden. Zudem können dynamisch jederzeit weitere Treiber nachgeladen werden. Dies geschieht über den Aufruf des ClassLoaders über Class.forName(). Anhand der JDBC-URL versucht der DriverManager bei Aufruf der Methode getConnection() den richtigen Treiber ausfindig zu machen und die Datenbankverbindung zu initialisieren. Die wichtigsten Methoden: deregisterDriver(Driver) Ein Treiber wird aus der Liste der registrierten Treiber gelöscht. getConnection(…) Die Methode erwartet entweder einen einzelnen String, drei Strings oder einen String und ein Objekt vom Typ Properties als Übergabeparameter. Der einzelne String ist ein JDBC-URL des Typs „jdbc:oracle:thin:scott/[email protected]:1527:oradb", bei drei Stringparametern wird dieser Block aufgesplittet in URL, Name und Passwort. Die dritte Form erwartet den JDBC-URL und eine Parameterliste in Form von Tags oder Strings, die zumindest die Username und Password-Properties gesetzt haben sollten. Die Methode öffnet die im URL angegebene Datenbank und liefert ein Objekt vom Typ Connection zurück. getDriver(String) Über die Methode kann derjenige Treiber gefunden werden, der den URL interpretieren kann. getDrivers() Die Methode liefert eine Liste aller registrierten Treiber zurück. Der Returnwert ist vom Typ Enumeration. Der Name der Treiberklasse kann über d.getClass().getName() in Erfahrung gebracht werden, wobei d vom Typ Driver ist. 14 Datenbanken getLoginTimeout() Die Treiber warten maximal eine bestimmte Anzahl von Millisekunden, bevor ein Verbindungsversuch als erfolglos gewertet wird. Diese Methode liefert die Anzahl an Millisekunden zurück. registerDriver(Driver) Eine neu geladene Treiberklasse wird über diese Methode beim Treibermanager registriert. setLoginTimeout(int) Die Methode legt fest, wie lange (in Millisekunden) ein Treiber versucht, die Verbindung zur Datenbank aufzubauen, bevor der Versuch als erfolglos gewertet wird. 4.2 Das Interface Connection Eine Connection repräsentiert immer eine Sitzung mit einer spezifischen Datenbank. Jede Connection definiert einen Kontext. Im Gültigkeitsbereich dieses Kontexts werden SQL-Anweisungen an eine Datenbank gesandt und gegebenenfalls Ergebnismengen zurückgeliefert. Weiterhin können über eine Connection Metadaten über die Datenbank erhalten werden. Jede Connection ist per Default in den AutoCommit Modus geschaltet, d.h., daß jede Änderung des Datenbestands sofort dauerhaft gültig wird. Die Attribute einer Connection beziehen sich ausnahmslos auf die Transaction-Isolation, wie bereits im Kapitel über Transaktionen besprochen wurde. Die wichtigsten Methoden: clearWarnings() Die zuletzt von der Datenbank generierte Warnung wird auf null gesetzt, also gelöscht. close() Diese Methode schließt die Verbindung zur Datenbank sofort und gibt alle JDBC Sourcen frei. commit() Über den Aufruf dieser Methode wird ein expliziter COMMIT an die Datenbank gesandt, so daß alle Änderungen am Datenbestand wirksam und eventuelle LOCKS entfernt werden. createStatement() Durch diesen Methodenaufruf wird für die aktive Datenbanksitzung ein Statement-Objekt generiert. Über Instanzen von diesem Typ können SQL-Anweisungen ohne Parameter an die Datenbank übergeben werden. getAutoCommit() Der momentane AutoCommit-Status der Datenbank wird über diesen Aufruf abgefragt und bekanntgegeben. getMetaData() Eine Datenbank kann Informationen über ihre Tabellen, die unterstütze SQL-Grammatik, Stored Procedures und viele weitere Möglichkeiten der aktuellen Verbindung bereitstellen. getTransactionIsolation() liefert den aktuellen Transaktions-Isolations-Modus. getWarnings() Die erste Warnung, die von der Datenbank generiert wurde, ist gecached und wird über diese Methode abgerufen. isClosed() überprüft, ob die aktuelle Verbindung geschlossen ist. 15 Datenbanken isReadOnly() überprüft, ob die Verbindung nur Lesezugriff erlaubt. prepareCall(String) Diese Methode liefert ein Objekt vom Typ CallableStatement zurück. Der übergebene String ist in aller Regel ein Aufruf einer StoredProcedure, deren Übergabeparameter mit einem ‚?‘ bekanntgemacht werden. Die Definition des Strings ist im allgemeinen eine JDBC-Escape Sequenz. Durch das Vorbereiten eines solchen Strings wird der Aufruf der StoredProcedure optimiert, da das CallableStatment eine vorkompilierte Version der Anweisung beinhaltet. rollback() Alle kürzlichen Änderungen werden zurückgenommen. Die LOCKS auf Datensätzen oder Tabellen werden entfernt. setAutoCommit(boolean) Der AutoCommit Status der Datenbank kann ein- oder ausgeschaltet werden. setReadOnly(boolean) Die Connection kann auf reinen Lesezugriff umgestellt werden. setTransactionIsolation(int) setzt das Transaction-Isolation Level auf den übergebenen Wert. Nähere Informationen zu den Transaktionsstufen finden sich im Kapitel über Transaktionen. 4.3 Das Interface CallableStatement CallableStatements erben Attribute und Methoden eines PreparedStatement. Über CallableStatements werden Stored Procedures aufgerufen. Zu diesem Zweck wurde eine JDBC-Escape Sequenz definiert, die einen Standard für solche Aufrufe, unabhängig vom RDBMS, zur Verfügung stellt. Es gibt zwei verschiedene Arten der Syntax. Eine der beiden erwartet einen Rückgabewert, die andere nicht. Der Ergebnisparameter muß als OUT-Parameter gekennzeichnet werden. Weiterhin existieren reine IN und ein INOUT Parametertyp, der sowohl Ergebnis als auch Eingabeparameter sein kann. Die Parameterliste wird sequentiell über Integer-Werte abgearbeitet. Der erste Parameter hat die Nummer 1. Syntax der Escape Sequenzen: { ? = call [,, … ]} { call [,, … ]} Um IN-Parameter zu setzen werden die vererbten set-Methoden aus PreparedStatement verwendet. Der Datentyp eines OUT-Parameter muß noch vor der Ausführung der Stored Procedure registriert werden. Sie werden über die get-Methoden des CallableStatements ausgelesen. Die Ergebnismengen werden in Form von einem oder mehreren ResultSets zurückgeliefert, die über die von Statement ererbten Methoden bearbeitet werden können. Anstatt die Werte eines OUT-Parameters zu verarbeiten, sollte lieber auf das ResultSet zurückgegriffen werden, da dadurch höhere Portabilität erreicht wird. Beispiel: //CallableStatement erzeugen: cstmt = con.prepareCall(„{call proc_2( ?, ?, ? )}“); //Eingabeparameter setzen: cstmt.setByte( 1, 100 ); cstmt.setLong( 3, 1234567890 ); //Ausgabeparameter mit JDBC-Datentypen anmelden: cstmt.registerOutParameter( 2, Types.SMALLINT ); cstmt.registerOutParameter( 3, Types.INTEGER ); 16 Datenbanken //Datenbankprozedur ausführen: cstmt.executeUpdate(); //Ausgabeparameter abrufen: int I = cstmt.getInt(2); long l = cstmt.getLong(3);“10 Der erste Parameter der gerufenen Stored Procedure ist ein reiner Eingabewert, der zweite ein reiner OUTParameter, während der dritte die INOUT Operation unterstützt. Der erste Übergabewert der get und registerMethoden bezieht sich auf den Index des Parameters im Aufruf der Stored Procedure. Die wichtigsten Methoden: 10 getBigDecimal(int, int) Liefert den Wert eines NUMERIC-Parameters als ein Objekt der Klasse java.math.BigDecimal. Der erste Übergabeparameter ist die Nummer des Parameters im Aufruf der Stored Procedure, der zwite die Anzahl der Nachkommastellen. getBoolean(int) Ein BIT-Parameter wird als Boolean zurückgeliefert. Der Übergabeparameter entspricht der Nummer des Parameters in der Aufrufliste der Stored Procedure. getByte(int) Ein TINYINT-Parameter wird als Java-Byte zurückgegeben. Der Übergabeparameter entspricht dem Index des OUT-Parameters, der an die Stored Procedure übergeben wird. getBytes(int) Liefert den Wert eines SQL BINARY oder VARBINARY-Parameters als Java byte[] Feld zurück. Auch hier ist der Übergabewert in der Parameterliste der Index des OUT-Parameters der Stored Procedure. getDate(int) Der Wert eines SQL DATE wird als java.sql.Date object ausgelesen. Der Übergabeparameter gibt Aufschluß über den Index des zu konvertierenden OUT-Parameters der Stored Procedure. getDouble(int) Ein DOUBLE Parameter wird als Java Double ausgegeben. Der Parameter ist wie üblich indiziert. getFloat(int) wandelt Float-Parameter in Java-floats um. getInt(int) Datenbank-Integers werden auf Java Integer gemapt. getLong(int) BIGINT-Parameter werden zu Java Long. getObject(int) konvertiert einen Parameter in ein Java Object. getShort(int) SMALLINT-Parameter entsprechen Java Short-Werten. getString(int) CHAR, VARCHAR und LONGVARCHAR werden nach Java String konvertiert. Über den Übergabewert der Funktion kann auf den Index des Parameters in der Stored Procedure verwiesen werden. Rainer Klute: JDBC in der Praxis, Addison-Wesley, S. 141 17 Datenbanken getTime(int) Ein SQL Time Parameter wird als java.sql.Time Object ausgelesen. getTimestamp(int) Der Wert eines SQL TIMESTAMP Parameters wird als java.sql.Timestamp Objekt zurückgegeben. registerOutParameter(int, int) Mit dieser Funktion muß vor der Ausführung einer Stored Procedure der java.sql.Type eines jeden OutParameters der Procedure explizit registriert werden (siehe Beispiel in der Interfacebeschreibung). Der erste Übergabewert entspricht dem Index des Parameters in der Stored Procedure, der zweite enthält den java.sql.Type Code. registerOutParameter(int, int, int) Diese Version der Methode wird für NUMERIC oder DECIMAL Parameter verwendet. Der erste Übergabewert ist der Index des Parameters in der Stored Procedure, der zweite entweder java.sql.Type.DECIMAL oder java.sql.Type.NUMERIC, der dritte gibt die Anzahl der Nachkommastellen im Ausgabewert an. wasNull() Beim Aufruf einer Stored Procedure kann der Wert eines OUT-Parameters SQL-Null annehmen. Da es sich dabei um einen undefinierten Zustand handelt, wird eine spezielle Behandlung dieses Falls notwendig. Die wasNull() Methode überprüft, ob der Parameter den undefinierten Zustand angenommen hat. 4.4 Das Interface PreparedStatement Eine SQL-Anweisung kann vorkompiliert, also präpariert, werden. Das Ergebnis dieses Vorgangs wird in einem java.sql.PreparedStatement-Objekt abgelegt. Dieses Objekt kann nun verwendet werden, um diese SQLAnweisung mehrmals auf der Datenbank abzusetzen. Diese Statements sind parametrisiert und erwarten Eingabewerte, die über die setXXX()-Methoden des Interface eingegeben werden können. Dabei muß jeweils die set-Methode verwendet werden, die dem sql.Type entspricht, den der Parameter erwartet, z.B. für SQL Integer muß setInt(…) verwendet werden. Sollen bei der Eingabe explizite Konvertierungen Anwendung finden, wird die Verwendung der setObject()Methode mit dem Ziel eines SQL Typen empfohlen. Die wichtigsten Methoden: clearParameters() Einmal gesetzte Parameter werden weiterhin im Speicher gehalten. Das Umsetzen eines Parameters löscht den alten Wert implizit. Trotzdem kann es sinnvoll sein, die gehaltenen Resourcen durch Aufruf dieser Methode sofort freizugeben. execute() Einige SQL-Anweisungen liefern mehrere Ergebnisse zurück. Solch komplexe Statements werden über die Methode execute() bearbeitet, genauso wie einfachere Formen von Anweisungen von executeUpdate() und executeQuery() verarbeitet werden. executeQuery() Eine vorkompilierte SQL-Anweisung wird abgesetzt und liefert ein ResultSet zurück. executeUpdate() führt eine SQL INSERT, UPDATE oder DELETE Anweisung auf der Datenbank aus. setAsciiStream(int, InputStream, int) Wenn ein sehr großer ASCII –Wert als Eingabe für einen LONGVARCHAR Parameter vorgesehen ist, kann er über diese Methode als java.io.InputStream eingegeben werden. JDBC liest alle Zeichen aus dem InputStream, bis der End-Of-File Eintrag erreicht ist. Notwendige Konvertierungen werden vom Treiber automatisch durchgeführt. 18 Datenbanken Der erste Parameter der setAsciiStream()-Methode ist der Index des Parameters im SQL-Statement, der zweite ein Objkt vom Typ InputStream und der dritte die Anzahl der Bytes im Stream. setBigDecimal(int, BigDecimal) Setzt den Parameter auf einen BigDecimal Wert. setBinaryStream(int, InputStream, int) Auch bei sehr großen Binärwerten ist es möglich, die Eingabe in den LONGVARBINARY-Parameter über einen java.io.InputStream durchzuführen. Die Methode liest den Stream bis zum End-Of-File Zeichen. Der erste Methodenparameter ist der Index des Eingabewerts im Statement, der zweite ein Objekt vom Typ java.io.InputStream und der dritte die Anzahl der Bytes im Strom. setBoolean(int, boolean) Übergibt einen java Boolean Wert an die SQL-Anweisung. setByte(int, byte) Setzte einen Parameter auf einen Java Byte Wert. setBytes(int, byte[]) Ein Parameter kann auch auf einen Array von Bytes gesetzt werden. setDate(int, Date) Die Methode setzt einen Parameter auf den Wert eines java.sql.Date. setDouble(int, double) übergibt einen Java double Wert an das SQL-Statement. setFloat(int, float) setzt einen Java float Wert ins Statement ein. setInt(int, int) übergibt einen Java int an die SQL-Anweisung. setLong(int, long) setzt einen long Wert. setNull(int, int) setzt einen Parameter auf SQL Null. Der zweite Übergabewert in der Liste enthält den SQL-Typen, der auf SQL NULL gesetzt werden soll. setObject(int, Object) Setzt den Wert eines Parameters über den Umweg eines java Objects. In der JDBC Spezifikation ist ein Mapping eines java Objects auf entsprechende SQL Typen vorgesehen. Das Objekt wird konvertiert, bevor es an die Datenbank gesandt wird. Über diese Methode können datenbankspezifische abstrakte Datentypen abgesetzt werden, indem man einen treiberspezifischen Java Typen verwendet. setObject(int, Object, int) Diese Methode ist äquivalent zur bereits beschriebenen. Der dritte Parameter in der Liste entspricht dem SQL-Zieltypen auf den abgebildet werden soll. setObject(int, Object, int, int) Auch diese Methodendefinition arbeitet analog. Abstrakte Datentypen werden im dritten Parameter als java.sql.Types.OTHER angegeben, um datenbankspezifische abstrakte Datentypen zu mappen. Der vierte Parameter gilt nur für NUMERIC und DECIMAL Werte. Er gibt die Anzahl der Nachkommastellen an. 19 Datenbanken setShort(int, short) übergibt einen Java short Wert an das Statement. setString(int, String) setzt den Parameter auf einen Java String. setTime(int, Time) der Parameter nimmt den Wert eines java.sql.Time Objektes an. setTimestamp(int, Timestamp) übergibt den Wert eines java.sql.Timestamp Objekts an das Statement. setUnicodeStream(int, InputStream, int) Sinnvollerweise übergibt man sehr große UNICODE Werte an einen LONGVARCHAR Parameter über einen java.io.InputStream. Der Stream wird bis zum End-Of-File Delimiter gelesen. Der Treiber führt automatisch notwendige Konvertierungen durch. Der dritte Übergabewert der Methode enthält die Anzahl der Bytes im Strom. 4.5 Das Interface Statement Dieses Interface verarbeitet einfache, statische SQL-Anweisungen und liefert die Ergebnismengen als ResultSet zurück. Nur ein einzelnes ResultSet kann pro Statement zur selben Zeit geöffnet sein. Sollen mehrere Ergebnismengen gleichzeitig bearbeitet werden, muß für jedes ein separates Statement-Objekt zur Verfügung stehen. Sollte eine der executeXXX()-Methoden aufgerufen werden, während bereits ein ResultSet existiert, wird dieses implizit geschlossen. Die wichtigsten Methoden: cancel() wird benutzt, um ausgehend von einem Thread das Statement eines anderen Threads abzubrechen, falls dieses gerade ausgeführt wird. clearWarnings() Alle Warnungen, die von der Datenbank an den Treiber übergeben wurden, werden gespeichert. Eine neue Warnung löscht implizit die alte, oder aber ein Aufruf dieser Methode setzt die Warnung auf null. close() Die close-Methode schließt explizit eine Datenbankverbindung und gibt die benötigten Resourcen frei. Dies geschieht auch implizit bei Beendigung eines Programms. Trotzdem kann es sich als sinnvoll erweisen, den Verbindungsabbau explizit durchzuführen. execute(String) Führt ein SQL-Statement aus, das mehrere Ergebnismengen produzieren kann. executeQuery(String) Führt eine SQL-Anweisung aus, die nur ein einzelnes ResultSet zurückliefert. executeUpdate(String) Führt ein SQL INSERT, UPDATE oder DELETE Statement auf der Datenbank aus. getMaxFieldSize() Das Limit maxFieldSize (in Bytes) gibt die maximale Anzahl der Daten an, die für einen Spaltenwert zurückgegeben werden können. Dies trifft nur auf BINARY, VARBINARY, LONGVARBINARY, CHAR, VARCHAR und LONGVARCHAR Attribute zu. 20 Datenbanken getMaxRows() Der Wert von maxRows zeigt an, wieviele Datensätze ein ResultSet maximal beinhalten kann. getMoreResults() navigiert zur nächsten Ergebnismenge innerhalb des Statements. getQueryTimeout() Der Timeout-Wert ist die Anzahl an Sekunden, die der Treiber maximal auf Ausführung eines Statements wartet, bevor die Operation als erfolglos eingestuft wird. getResultSet() liefert das aktuelle Ergebnis als ResultSet zurück. Dies kommt z.B. zur Anwendung, wenn über die execute()-Methode mehrere Ergebnismengen erzeugt wurden. getUpdateCount() liefert das aktuelle Ergebnis als Zähler zurück, der anzeigt, wieviele Datensätze von einer Änderung betroffen waren. Wenn der Rückgabewert ein ResultSet ist, oder mehrere Ergebnisse vorliegen, wird –1 ausgegeben. getWarnings() liefert die erste Warnung, die bei Aufruf des Statements erzeugt wurde. setCursorName(String) gibt dem Cursor einen Namen, der bei weiteren Aufrufen der execute-Methoden des Statements verwendet wird. setEscapeProcessing(boolean) Nur wenn diese Option eingeschaltet ist, ersetzt der Treiber SQL-Escape-Sequencen durch Datenbanksyntax, bevor das Statement auf der Datenbank abgesetzt wird. setMaxFieldSize(int) Die maxFieldSize (in Byte) beschränkt die Größe der Daten, die für ein Attribut zurückgeliefert werden können. Dies betrifft nur Datenfelder vom Typus BINARY, VARBINARY, LONGVARBINARY, CHAR, VARCHAR und LONGVARCHAR. setMaxRows(int) limitiert die Anzahl der Datensätze, die ein ResultSet enthalten kann. setQueryTimeout(int) liefert die Anzahl der Sekunden, die der Treiber maximal auf die Ausführung eines Statements wartet. 4.6 Das Interface ResultSet Die Ausführung eines Statements generiert eine Art virtuelle Tabelle, die in einem ResultSet abgespeichert ist. Die Datensätze sind sequentiell angeordnet. Innerhalb eines Datensatzes kann in beliebiger Reihenfolge auf ein Attribut positioniert werden. Ein ResultSet stellt einen Cursor zur Verfügung, der auf den jeweils aktuellen Datensatz verweist. Nach der Initialisierung des ResultSet steht dieser Cursor allerdings VOR dem ersten Datensatz. Mit der Methode next() kann zum ersten Datensatz gesprungen werden. Die Spalten in einem Datensatz können über den Index, beginnend bei 1, oder aber den Spaltennamen angesprochen werden. Der Spaltenname wird case insensitive behandelt. Das Auslesen der Werte erfolgt über eine Vielzahl von getXXX()-Methoden, entsprechend dem zu lesenden Datentypen. Diese Methoden führen eine Konvertierung des datenbankspezifischen Datentyps in den zugehörigen Java-Typen durch. Ein ResultSet wird vom ausführenden Statement geschlossen sobald das Statement geschlossen wird, wenn es neu ausgeführt wird, oder aber wenn zur nächsten Ergebnismenge gesprungen wird, für den Fall daß mehrere Ergebnismengen vorliegen. 21 Datenbanken Der Index, die Typen und Eigenschaften der Spalten eines ResultSet können durch das ResultSetMetaDataObjekt abgefragt werden, das von der getMetaData()-Methode bereitgestellt wird. Die wichtigsten Methoden: clearWarnings() Alle gespeichertern Warnungen für dieses ResultSet werden gelöscht und auf null gesetzt. Dies ändert sich erst, wenn das RDBMS eine neue Warnung erzeugt. close() schließt explizit die Datenbank des ResultSets und gibt alle JDBC Resourcen frei. Dies kann unter Umständen sinnvoller sein, als auf ein implizites Schließen der Datenbank zu warten. findColumn(String) liefert den Index einer Spalte im ResultSet anhand des Spaltennamens zurück. getAsciiStream(int) Ein Spaltenwert wird als Stream von ASCII Zeichen eingelesen und aus dem Strom heraus blockweise verarbeitet. getAsciiStream(String) Ein Spaltenwert wird als Stream von ASCII Zeichen eingelesen und aus dem Strom heraus blockweise verarbeitet. getBigDecimal(int, int) liest den Wert einer Spalte als java.lang.BigDecimal Objekt. Der zweite Übergabeparameter setzt die Anzahl der Nachkommastellen fest. getBigDecimal(String, int) liest den Wert einer Spalte als java.lang.BigDecimal Objekt. Der zweite Übergabeparameter setzt die Anzahl der Nachkommastellen fest. getBinaryStream(int) Ein Spaltenwert kann als uninterpretierter Strom von Bytes eingelesen und anschließend aus dem Strom heraus blockweise verarbeitet werden. getBinaryStream(String) Ein Spaltenwert kann als uninterpretierter Strom von Bytes eingelesen und anschließend aus dem Strom heraus blockweise verarbeitet werden. getBoolean(int) liefert den Spaltenwert als Java boolean zurück. getBoolean(String) liefert den Spaltenwert als Java boolean zurück. getByte(int) liefert ein Java byte. getByte(String) liefert ein Java byte. getBytes(int) liefert einen Array von Java bytes. 22 Datenbanken getBytes(String) liefert einen Array von Java bytes. getCursorName() gibt den Namen des Cursors aus, der von diesem ResultSet verwendet wird. getDate(int) Der Spaltenwert wird als java.sql.Date Objekt ausgelesen. getDate(String) Der Spaltenwert wird als java.sql.Date Objekt ausgelesen. getDouble(int) liefert einen Java double. getDouble(String) liefert einen Java double. getFloat(int) liefert den Spaltenwert des aktuellen Datensatzes als Java float. getFloat(String) liefert den Spaltenwert des aktuellen Datensatzes als Java float. getInt(int) liefert den Spaltenwert des aktuellen Datensatzes als Java int. getInt(String) liefert den Spaltenwert des aktuellen Datensatzes als Java int. getLong(int) liefert den Spaltenwert des aktuellen Datensatzes als Java long. getLong(String) liefert den Spaltenwert des aktuellen Datensatzes als Java long. getMetaData() Spaltenindex, Typen und Eigenschaften der Spalten eines ResultSets können über ein Objekt vom Typ ResultSetMetaData abgefragt werden, das von dieser Methode erzeugt wird. getObject(int) liefert den Spaltenwert des aktuellen Datensatzes als Java object. getObject(String) liefert den Spaltenwert des aktuellen Datensatzes als Java object. getShort(int) liefert den Spaltenwert des aktuellen Datensatzes als Java short. getShort(String) liefert den Spaltenwert des aktuellen Datensatzes als Java short. 23 Datenbanken getString(int) liefert den Spaltenwert des aktuellen Datensatzes als Java String. getString(String) liefert den Spaltenwert des aktuellen Datensatzes als Java String. getTime(int) liefert den Spaltenwert des aktuellen Datensatzes als java.sql.Time Objekt. getTime(String) liefert den Spaltenwert des aktuellen Datensatzes als java.sql.Time Objekt. getTimestamp(int) liefert den Spaltenwert des aktuellen Datensatzes als java.sql.Timestamp Objekt. getTimestamp(String) liefert den Spaltenwert des aktuellen Datensatzes als java.sql.Timestamp Objekt. getUnicodeStream(int) Ein Spaltenwert kann als Strom von UNICODE Zeichen eingelesen werden und wird dann aus dem Strom heraus blockweise verarbeitet. getUnicodeStream(String) Ein Spaltenwert kann als Strom von UNICODE Zeichen eingelesen werden und wird dann aus dem Strom heraus blockweise verarbeitet. getWarnings() Die erste Warnung beim Aufruf dieses ResultSets wird ausgegeben. next() Der Cursor auf den aktuellen Datensatz im ResultSet wird sequentiell weitergeschaltet. Bei Initialisierung des ResultSets steht der Cursor VOR dem ersten Datensatz. Durch Aufruf dieser Methode wird auf die erste gültige Reihe positioniert. wasNull() Ein Spaltenwert kann als Ergebnis SQL NULL enthalten. Dies bedeutet einen undefinierten Zustand. Über die Methode wasNull() kann in Erfahrung gebracht werden, ob ein Spaltenwert diesen Zustand angenommen hat. 5. Besonderheiten des JDBC-Treibers von Oracle Da für jedes Datenbanksystem proprietäre JDBC-Treiber existieren, besitzen diese Treiber Eigenschaften, die speziell auf dieses DBMS zugeschnitten sind. Die Oracle JDBC Treiber unterstützen JDBC 1.22, die Version die mit dem JDK 1.1.1 ausgeliefert wird und auch als Add-On für JDK 1.0.2 verfügbar ist. Weiterhin werden alle SQL-Datentypen unterstützt, die von diesem Standard verlangt werden. Zusätzlich dazu unterstützt der Treiber die oraclespezifischen Datentypen ROWID und REFCURSOR. Datentypen Folgende Tabelle zeigt, wie die JDBC-Typecodes auf Oracletypen gemappt werden. JDBC-Typecodes Types.CHAR Types.VARCHAR Types.LONGVARCHAR Oracle Datentypen CHAR VARCHAR2 LONG 24 Datenbanken Types.VARBINARY Types.LONGVARBINARY Alle numerischen Datentypen Alle Datumstypen RAW LONG RAW NUMBER DATE Abb. 3: Mapping von Java Typecodes auf Oracle Weiterhin wird folgendes Oracle-spezifisches Mapping durchgeführt: Oracle Type Code OracleTypes.ROWID OracleTypes.REFCURSOR Oracle Datentypen ROWID REFCURSOR Abb. 4: Mapping von speziellen Datentypen Die ROWID wird als Java String und der REFCURSOR als ResultSet zurückgegeben. Multibyte Character Sets Die Thin-Version des Treibers von Oracle kann jede Datenbank, völlig ungeachtet des in der Datenbank verwendeten Character Sets, ansprechen. Dies wird dadurch erreicht, daß alle Zeichen in Unicode 1.2 umgewandelt werden. Da Java selbst Unicode 2.0 verwendet, entstehen Probleme nur für die koreanische Schrift. Streaming Der JDBC-Treiber unterstützt Streaming in beide Richtungen zwischen Client und Server. Alle StreamKonvertierungen, BINARY, ASCII und UNICODE, werden zur Verfügung gestellt. Stored Procedures Stored Procedures und anonymous Blocks werden von der Thin-Version des Treibers voll unterstützt. Hierfür kann sowohl der Standard der SQL92 Escape Sequenzen verwendet werden, als auch die speziellen Oracle Escape-Sequenzen. SQL92 Syntax: CallableStatement cs1 = conn.prepareCall ( "{call proc (?,?)}" ) ; CallableStatement cs2 = conn.prepareCall ( "{? = call func (?,?)}" ) ; Oracle Syntax CallableStatement cs3 = conn.prepareCall ( "begin proc (:1, :2); end;" ) ; CallableStatement cs4 = conn.prepareCall ( "begin :1 := func(:2,:3); end;" ) ; Metadaten Alle Standardmethoden zur Abfrage von Metadaten sind implementiert. Zu diesem Zweck werden Abfragen auf die Metadaten-Tabellen des RDBMS durchgeführt. Zudem enthält die vertriebene Version des Treibers den Sourcecode der Klasse OracleDatabaseMetadata, so daß eigene Methoden und Methodenaufrufe generiert werden können. SQL92 Syntax SQL92 Escape-Sequenzen werden voll unterstütz, bis auf den Gebrauch von „Outer Joins“. Prefetching von Reihen 25 Datenbanken In einer Abfrage kann angegeben werden, wieviele Reihen auf einmal in der Abfrage zum Client übertragen werden sollen. Die Defaulteinstellung ist 10. Durch diese Option werden Round Trips zum Server reduziert. Die Anzahl der „vorbestellten“ Reihen kann entweder für die Connection oder für das Statement festgelegt sein. Batching von Statements Der JDBC-Treiber erlaubt es, mehrere Inserts und Updates im Client anzusammeln und dann via Batch an den Server zu schicken. Dadurch werden wiederum Round Trips zum Server vermieden. Die Batchsize kann für ein Statement eingestellt werden (Defaulteinstellung ist 1). Vordefinition von Spalten einer Abfrage Es ist möglich, den Server schon vor der Ausführung einer Abfrage über die notwendigen Datentypen zu informieren. Dadurch werden Round Trips zum Server vermieden. Einschränkungen des Treibers Einige Anforderungen des JDBC 1.22 Standards werden vom Treiber nicht erfüllt: CursorName Aufrufe der Methoden getCursorName() und setCursorName() können vom Treiber nicht unterstützt werden, da sie nicht auf die existenten Oracle-Konstrukte gemappt werden können. Oracle setzt deshalb die Verwendung von ROWID verbindlich fest. SQL92 Outer Join Escapes Die Syntax für solche Sequenzen wird nicht unterstützt. Stattdessen muß die Oracle Syntax („+“) verwendet werden. PL/SQL BOOLEAN und RECORD Typen Der Treiber untertützt diese Datentypen weder als IN noch als OUT Parameter von PL/SQL Anweisungsblöcken. Ein möglicher Workaround wäre z.B die Definition einer zweiten Stored Procedure, die den BOOLEAN-Wert als NUMERIC oder CHAR-Wert akzeptiert und an die erste Procedure als BOOLEAN weiterreicht. IEEE 754 Floating Point Kompatibilität Die arithmetischen Operationen der NUMBER-Datentypen von Oracle sind nicht mit denen der IEEE Spezifikation kompatibel. Dadurch können Abweichungen in den Ergebnissen der Berechnung einer OracleDatenbank und der selben Berechnung durch ein Java Programm auftreten. Oracle speichert Zahlen in einem Format, das zur Dezimalarithmetik kompatibel ist und stellt 38 Dezimalzahlen als Präzision zur Verfügung. 0, negativ unendlich und positiv unendlich werden exakt dargestellt. Für jede positive Zahl existiert eine exakte negative Repräsentation des Betrags. Jede positive Zahl zwischen 10 -38 und (110-38)*10126 werden mit der vollen Präzision abgebildet. JDBC OCI Features Die JDBC OCI Treiber sind Treiber vom Typ2, die native Methoden von Java verwenden, um die C Einsprungpunkte der OCI Bibliothek anzusprechen. Aus diesem Grund ist dieser Treiber nicht mehr plattformunabhängig. Diese Treiber existieren für Windows, Solaris und noch einige andere Plattformen. OCI Treiber können nicht für Applets verwendet werden, die auf unbekannten Systemumgebungen gestartet werden. Sie eignen sich jedoch hervorragend für reine Java Applikationen oder zur Verwendung im Dreischichtenmodell mit einer Java-Schicht als Middleware. Für diese Treiber muß clientseitig eine SQL*Net Version 2.3 oder höher installiert werden. Da als Schnittstelle das Oracle Call Interface verwendet wird, können diese Treiber für alle SQL*Net-Adapter verwendet werden, IPC, Named Pipes, TCP/IP, DECnet und noch einige mehr. Weiterhin können alle Features der Advanced Networking Option, z.B. verschlüsseltes SQL*Net, in vollem Umfang genutzt werden. 26 Datenbanken Die OCI Treiber konvertieren CHAR Daten aus dem Multibyte Character Set in Java Unicode Zeichen. Dies wird clientseitig über die OCI Routinen durchgeführt. JDBC Thin Features Dieser Treiber ist zu 100% vom Typ4. Er verbindet sich direkt mit Oracle über einen Java Socket, ohne javaspezifische Middleware zu benötigen. Die Verbindung kommt nur dann zustande, wenn ein TNS Listener existiert, der TCP/IP Sockets nach Aufrufen „abhorcht“. Der Thin Treiber ist nur soweit plattformspezifisch, wie dies für Java selbst zutrifft. Jedes System, auf dem eine korrekte Implementierung von Java zur Verfügung steht, kann diesen Treiber verwenden. Der Treiber wird als Teil der Applikation oder des Applets in jeden Browser „down“ geloaded. Die Verwendung solcher Treiber kann allerdings durch Firewalls eingeschränkt sein. Bekannte Bugs Die Methoden getQueryTimeout(), setQueryTimeout() und cancel() können den Treiber während der Ausführung eines Statements zum Absturz bringen. Der Oracle OCI Treiber arbeitet derzeit noch nicht mit den Entwicklungsumgebungen Symantec Visual Café oder Microsoft Visual J++ zusammen. 27