Datenbanken 11. Übung Aufgabe: Emdedded SQL mit Java SQLJ Lösungsschritte: SQLJ1 Einführung: Einbettungsprinzip - Einbettung von statischen SQL-Anweisungen direkt in den Java-Quelltext Statisch bedeutet: Die Anweisungen sind zur Übersetzungszeit definiert und können während der Abarbeitung des Programms nicht geändert werden. Vorübersetzung des erweiterten Quelltextes in "echten" Java Code durch den Präcompiler (Translator) SQLJ in Oracle Übersetzen: Quelldateien mit der Extension .sqlj werden dem SQLJ-Übersetzer übergeben. Dieser sucht nach den Zeilen mit #sql ab und ersetzt sie durch gültigen Java-Code, der mit Hilfe von JDBC die notwendigen Operationen durchführt. (SQLJ definiert keine eigene Datenbankschnittstelle, sondern ist nur als Aufsatz auf die bereits bestehende JDBC-API zu sehen, die Verbindung zur Datenbank erfolgt mit dem verfügbaren JDBC-Treiber.) Darüber hinaus kann der SQLJ-Übersetzer die Syntax der eingebetteten SQL-Anweisungen überprüfen und bereits beim Kompilieren Aussagen über fehlerhafte SQL-Anweisungen machen. Prinzipiell besteht auch die Möglichkeit eine Verbindung zur Darstellung während der Übersetzung herzustellen, damit Namen der Tabellen und Spalten in SQLAnweisungen überprüft werden können. Der SQLJ-Translator - übersetzt ein gegebenes SQLJ-Programm in ein Java Programm ohne SQL-Klauseln. Die SQLJ-Quelltexte werden mit dem Kommandozeilenprogramm sqlj kompiliert. es werden SQLJ-Profile angelegt, die Informationen über die eingebetteten SQL-Operationen beinhalten. sqlj optionen dateien optionen: Eine durch Leerzeilen getrennte Liste von Parametern dateien: Eine oder mehrere Dateien, die kompiliert werden sollen. Der Platzhalter * kann bei den Dateien verwendet werden. Der SQLJ-Translator kann die Syntax der SQL-Anweisungen direkt beim Kompilieren überprüfen. Dazu setzt man bspw. den folgenden Kommandozeilenbefehl ab: sqlj –user=xyz12345/xyz12345 –url= jdbc:oracle:thin:@rfhs8012:1523:ora10g Nach dem Kompilieren übersetzt ein Java-Compiler die generierten Java-Dateien in Java-Byte-Code. 1 Der SQLJ-Standard wurde von Compaq, IBM, Informix, Micro Focus, Micosoft, Oracle, Sun und Sybase entwickelt 1 Datenbanken Abb.1 : Schematischer Ablauf beim Einbinden von Embedded SQL in Java-Anwendungen Festlegen der Verbindungsdaten mit der Klasse Oracle. Über die Klasse oracle.sqlj.runtime.Oracle kann ein Verbindungskontext initialisiert werden. Dazu werden die Verbindungsdaten in einer externen Datei ("connect.properties") im Property-Format von Java festgelegt. sqlj.url=jdbc:oracle:thin:@rfhs8012:1523:ora10g sqlj.user=xyz12345 sqlj.password=xyz12345 Damit die SQLJ-Umgebung diese Verbindungsdaten nutzt, ist im Java-Quelltext die Methode connect() der Klasse oracle.sqlj.runtime.Oracle aufzurufen: void connect(Class class, String propertyFile). In dem folgenden Quelltext wird über die Klasse Oracle eine einfache Verbindung zur Datenbank hergestellt import java.sql.*; import oracle.sqlj.runtime.Oracle; public class HalloWelt { public static void main(String args[]) throws Exception { java.sql.Date current_date; try { // Verbindung zur Datenbank Oracle.connect(HalloWelt.class,"connect.properties"); // Bestimme das aktuelle Datum aus der Datenbank #sql { SELECT sysdate INTO :current_date FROM dual }; // Ergebnis ausgeben System.out.println("Hallo Welt am " + current_date); } catch (SQLException e) { System.out.println("SQLException " + e); } finally { try { Oracle.close(); } catch (SQLException e) { System.err.println("SQLException " + e); } 2 Datenbanken } } } Verbindungskontexte mit DefaultContext. Einen Verbindungskontext, der innerhalb eines SQLJBlocks genutzt werden kann, erstellt man über die Klasse sqlj.runtime.refDefaultContext. Einen DefaultContext legt man mit Hilfe einer gewöhnlichen JDBC-Verbindung, also mit einem Objekt vom Typ java.sql.Connection über folgenden Konstruktor an: DefaultContext(java.sql.Connection conn) (conn: Die Datenbankverbindung, die durch diesen Kontext verwendet werden soll). Alternativ können dem Konstruktor auch die Verebindungsdaten übergeben werden. Die Klasse Defaultkontext stellt dann intern eine Verbindung anhand der übergebenen Daten her: DefaultContext(java.lang.String url, java.lang.String user, java.lang.String password, boolean autoCommit2) Mit der statischen Methode setDefaultContext() wird der Defaultkontext bei der SQLJImplementierung als standardmäßig genutzte Verbindung registriert: DefaultContext.setDefaultContext(DefaultContext context). Alle darauf folgenden SQLJ-Blöcke nutzen dann die im Parameter context gekapselte Verbindung. Wurden mehrere Verbindungskontexte angelegt, dann kann bei einem SQLJ-Block festgelegt werden, welchen Kontext dieser verwenden soll: #sql [Kontextname] SQLJ-Anweisung Ausführen und Auswerten über Anfragen. Ein SQLJ-Block wird grundsätzlich als Anweisung interpretiert, die über die zuvor definierte SQLJ-Anweisung ausgeführt wird. #sql { SQL-Anweisung }; SQL-Anweisung: eine beliebige, gültige SQL-Anweisung Eine Abfrage mit mehreren Datensätzen als Ergebnis erfordert in der Regel eine Schleife und spezielle Methoden der Auswertung der Ergebnismenge. In der JDBC kann man über ResultSet ein solches mehrere Zeilen umfassendes Ergebnis gut auswerten. SQLJ definiert dazu so genannte Iteratoren. Abfrage der Anzahl der geänderten Datensätze: Über die Klasse ExecutionContext, die nach einer SQLJ-Anweisung aus der DML implizit intialisiert wird, kann festgestellt werden, wie viele Datensätze von einer Änderung betroffen sind. Eine Referenz auf den aktuellen ExecutionContext erhält man von der Klasse DefaultContext, und zwar über deren Methode getExecutionContext. Die Anzahl der geänderten Datensätze erhält man dort über die Methode getUpdateCount. Datenaustausch über Host-Variablen: Der Datenaustausch zwischen SQL- und Java-Code erfolgt über Host-Variablen (und Iteratoren). Host-Variablen bieten Zugriff auf Ergebnisse von SelectAnweisungen, die nur eine Zeile als Ergebnis liefern, darüber hinaus können sie Parameter für beliebig andere SQL-Anweisungen setzen. Es handelt sich um Variablen der jeweiligen Host-Sprache, ( d.h. Java), die in SQL-Anweisungen auftreten können. In SQL-Anweisungen werden Host-Variablen durch eine Doppelpunkt >>:<< gekennzeichnet. Host-Variablen können in beiden Richtungen des Datenaustauschs eingesetzt werden, d.h. zur Übergabe von Java-Werten an: - die SQL-Anweisung: (IN), Standardform oder als Ergebnisse: OUT bzw. in beide Richtungen gleichzeitig: (INOUT) #sql { INSERT INTO kunden VALUES (:IN (++kundenNR), :IN nachname, :IN sysdate) }; Eie SQL-Klausel kann auch Werte zurückliefern, wie z.B. beim Aufruf einer gespeicherten Funktion: #sql Variable = { VALUES( Funktionsaufruf }; Variable: Eine Hostvariable passenden Typs, die den Rückgabewert aufnehmen soll. Funktionsaufruf: Der Aufruf der gespeicherten Funktion mit Parametern, etc. Bsp.: Abfrage des aktuellen Datums auf dem Oracle-Server über die Funktion SYSDATE. java.sql.Date datum; Falls alle SQL-Anweisungen direkt als eine isolierte Transaktion ausgeführt werden sollen, dann ist true zu übergeben. Falls die Transaktionssteuerung übernommen werden soll, ist false zu übergeben. 2 3 Datenbanken #sql datum = { VALUES(SYSDATE)) }; System.out.println(datum); Gespeicherte Prozeduren werden in SQLJ durch das vorangestellte Schlüsselwort CALL aufgerufen werden: #sql { CALL proc(Parameterliste) };. Diesen Prozeduren können IN-, OUT-, INOUT-Parameter übergeben werden. Datenaustausch über Iteratoren: beim Zugriff auf mehrzeilige Ergebnismengen kommen Iteratoren zum Einsatz3. SQLJ bietet zwei Arten von Iteratoren an: benannte Iteratoren (Named Iterator mit Zugriff auf das Ergebnis über Methoden, die die Namen der Tabellenspalten tragen) und PositionsIteratoren. Ein Iterator definiert man allgemein über einen SQLJ-Block in folgendem Format: #sql [modifikatoren] iterator iteratorname [implements interface] (parameterliste) Alle von der SQLJ-Implementierung generierten Iteratoren erfüllen die Vorgaben der Schnittstelle sqlj.runtime.ResultSetIterator. Programmierer haben u.a. Zugriff auf folgenden Methoden: close() scließt einen Iterator bzw. die darunter liegende Ergebnismenge isClosed() Die Methode liefert true, falls die darunterliegende Ergenismenge geschlossen wurde. getResultSet() Über diese Methode kann man eine Referenz auf das dem Iterator zugrundeliegende Abfrageergebnis in Form eines java.sql.ResultSet erhalten. next() Der Cursor der Ergebnismenge wechselt zum nächsten Datensatz. Die Methode liefert true, wenn ein weiterer Datensatz in der Ergebnismenge vorhanden ist, bzw. false, wenn der Iterator bereits beim letzten Datensatz angelegt ist. endFetch() Diese Methode liefert true, nachdem die letzte Zeile erreicht wurde. Benannte. Iteratoren. Der Iterator wird in einem SQLJ-Block deklariert. Nach dem Namen des Iterators (z.B. angestellte) folgt eine Liste der über den Iterator abzufragenden Attribute, inklusive des bevorzugten Datentyps. Der SQLJ-Translator generiert beim Kompilieren eine Java-Klasse, die die Vorgaben der Deklaration und der im Paket sqlj.runtime definierten Interfaces erfüllt. Ein benannter Iterator definiert seine Parameter namentlich Die Attributnamen des Iterators müssen identisch mit den Namen der Spalten des AbfrageErgebnisses sein. Die Reihenfolge der Spalten bei der Abfrage oder bei der Iterator-Deklaration ist egal, die Verknüpfung erfolgt nur über die Namen. Bsp.: AngSchemaDemo.sqlj import java.sql.SQLException; import oracle.sqlj.runtime.Oracle; #sql iterator Angestellte (String name, String abt_id); class AngSchemaDemo { public static void main(String[] args) throws SQLException { Oracle.connect(AngSchemaDemo.class, "connect.properties"); Angestellte ang; #sql ang = { SELECT name, abt_id FROM angestellte }; while (ang.next()) { System.out.println("Angestellte: " + ang.name() + " (" + 3 Die Verarbeitung von Anfrageergebnissen mit Hilfe von Host-Ausdrücken ist nur auf wenige SQL-Konstrukte beschränkt, wie die SELECT ... INTO Klausel oder den Aufruf von gespeicherten Prozeduren. 4 Datenbanken ang.abt_id() + ")"); } } } Positions-Iteratoren. Die Definition von Positions-Iteratoren legt nur die Datentypen fest, nicht jedoch die Namen. Die Reihenfolge bei der Definition des Iterators muß in diesem Fall mit der Spaltenreihenfolge in der SQL-Abfrage übereinstimmen. Unbenannte Iteratoren werden durch ihre Position bestimmt. Der Datenzugriff weicht auch vom benannten Iterator ab. Via Fetch-Anweisung lassen sich die aktuellen Werte des Iterators in Host-Variablen einlesen: #SQL { FETCH :positer INTO : ..., : ... }; import java.sql.SQLException; import oracle.sqlj.runtime.Oracle; // Deklaration des Iterators #sql iterator AngIter (String, String, String); class AngPosIterDemo { public static void main(String[] args) throws SQLException { // Connect herstellen Oracle.connect(AngPosIterDemo.class, "connect.properties"); // Host-Variable String angID = "";; String angName = "";; String angGebdatum = ""; // Variable auf Iterator definieren AngIter angPosIter; // Abfrage durchfuehren #sql angPosIter = { SELECT ang_id, name, gebdatum FROM angestellte }; // Ergebnis ausgeben while (true) { #sql { FETCH :angPosIter INTO :angID, :angName, :angGebdatum }; if (angPosIter.endFetch()) break; System.out.println(angID + ", " + angName + ", " + angGebdatum); } angPosIter.close(); } } Iteratoren über eigenes Interface verwenden. Die Syntax der Iterator-Deklaration ermöglicht es, über das Schlüsselwort interface ein eigenes Java-Interface für den Iterator zu verwenden. Kontexte: Eine Verbindung wird in SQLJ grundsätzlich durch einen Verbindungskontext, eine Instanz der Klasse sqlj.runtime.ConnectionContext repräsentiert. Der Verbindungskontext spezifiziert die Datenbank mit den assoziierten Schemata und die Verbindungsinformationen. Die Verbindungsinformationen bestehen aus: Benutzername, Passwort, Auto-Commit-Modus: #SQL [Kontextname] SQLJ-Anweisung. Kontextname: Der Name der Variablen des Verbindungskontextes im Java-Quelltext SQLJ-Anweisung: Ein gültige SQLJ-Anweisung Bsp.: Verwendung verschiedener Verbindungskontexte in einem SQLJ-Programm Transaktionssteuerung mit SQLJ. Eine Transaktion ist eine Folge von SQL- und PL/SQLAnweisungen, die nach dem Prinzip alles oder nichts ausgeführt werden, d.h. Falls eine der Anweisungen scheitert, die Transaktion also nicht ordentlich beendet wird, werden alle Anweisungen dieser Transaktion wieder rückgängig gemacht. Eine Transaktion beginnt automatisch mit dem ersten SQLJ-Block, der eine SQL-Anweisung an die Datenbank sendet, und endet mit einem SQLJ-Block mit der Anweisung COMMIT ( #sql { COMMIT 5 Datenbanken }; ). Falls nach einem Abbruch einer Transaktion alle Änderungen, die bisher in der Transaktion vorgenommen wurden, widerrufen werden sollen, geschieht dies über ROLLBACK ( #sql { ROLLBACK }; ) Transaktionssicherheit festlegen. Die Einstellungen zur Transaktionssicherheit legen fest, wie parallel ablaufende Transaktionen voneinander "isoliert" werden: #sql { SET TRANSACTION [ Zugriffsmodus ] [,] [ISOLATION LEVEL Isolationsstufe ] }; Zugriffsmodus: read only oder read write. Legt fest, welche Art von SQL-Anweisungen in einer Transaktion erlaubt sind. Standardmäßig ist auf read write eingestellt. Isolationsstufe: read committed oder serializable. Oracle nutzt standardmäßig die Einstellung read committed. Während einer Transaktion können dann allerdings nicht wiederholbare Lesevorgänge auftreten, d.h.: Falls in einer längeren Transaktion zweimal die gleiche SELECT-Anweisung ausgeführt wurde, und die betroffenen Datensätze in der Zwischenzeit von einer anderen Transaktion verändert wurden, dann erhält man zwei verschiedenen Ergebnisse. Das erste Ergebnis ist also nocht wiederholbar. Die Isolationsstufe serializable sorgt hingegen durch entsprechende Sperren und Zwischenspeicher dafür, dass man die Daten nur in dem Zustand erhält, der am Anfang der Transaktion galt. Bsp.: Experimente mit Einstellungen zur Transaktionssicherheit import oracle.sqlj.runtime.Oracle; import sqlj.runtime.ref.DefaultContext; public class ExpTransSicher { public static void main(String args[]) { try { DefaultContext trans1 = Oracle.connect(ExpTransSicher.class, "connect.properties"); DefaultContext trans2 = new DefaultContext( "jdbc:oracle:thin:@rfhs8012:1523:ora10g", "saj39122","saj39122", false); DefaultContext.setDefaultContext(trans1); /* Der folgende Block ist nur dann zu aktivieren, falls der nicht wiederholbare Lesevorgang vermieden werden soll #sql [trans1] { SET TRANSACTION read write ISOLATION LEVEL serializable }; */ String vorname = null, phantom = null; // Den Namen erstmals lesen #sql [trans1] { SELECT name INTO :vorname FROM angestellte WHERE ang_id = 'A1' }; System.out.println("[trans1]: Name '" + vorname + "'"); // Veraenderung vom Namen in einer 2. Verbindung #sql [trans2] { UPDATE angestellte SET name = 'Phantom' WHERE ang_id = 'A1' }; // Die Transaktion beenden #sql [trans2] { COMMIT }; // Mit trans1 zum 2. Mal den Namen erfragen #sql [trans1] { SELECT name INTO :phantom FROM angestellte WHERE ang_id = 'A1' 6 Datenbanken }; System.out.println("[trans1]: Name jetzt '" + phantom + "'"); // Auf den alten Namen zuruecksetzen #sql [trans2] { UPDATE angestellte SET name = 'Fritz' WHERE ang_id = 'A1' }; // Speichern #sql [trans2] { COMMIT }; // Mal wieder mit trans1 den Namen lesen #sql [trans1] { SELECT name INTO :phantom FROM angestellte WHERE ang_id = 'A1' }; System.out.println("[trans1]: Name jetzt '" + phantom + "'"); } catch(Exception e) { e.printStackTrace(); } } } connect.properties AngPosIterDemo.sqlj AngSchemaDemo.sqljj HalloWelt.sqlj InfoAbtDemo.sqlj Macheverbindung.sqlj MultiSchemaDemo.sqlj SelectSQLJDemo.sqlj ExpTransSicher.sqlj 7