Problemseminar - Datenbankeinsatz im Internet
Java und Datenbanken
Bearbeiter:
Betreuer:
Dr. Sosna
2.2 zweischichtige und dreischichtige Modelle
3.1 SQLJ Teil 0: Embedded SQL in Java
3.2 SQLJ Teil 1: Java Methoden als SQL Prozeduren
3.2.1 Installation von Java Klassen in SQL
3.2.2 Vergabe von SQL Namen für Funktionen und Prozeduren
3.2.3 Überladen von Java Methoden und SQL Namen
3.2.4 Verarbeitung von Ausgabeparameter
3.2.5 Verarbeitung von Resultsets
3.3 SQLJ Teil 2: Java Klassen als SQL Datentypen
3.3.1 Vergabe von SQL Namen für SQL Datentypen
Mit der Verbreitung des Internets in den letzten Jahren ist auch der Wunsch gewachsen über das Internet bzw. Intranet Zugriff auf Datenbanken zu bekommen.
Java ist eine hervorragenden Sprachbasis für Datenbankanwendungen, weil es robust, sicher, einfach zu nutzen, einfach zu verstehen und in einem Rechnernetz automatisch ladbar ist [HC97]. Darüber hinaus bietet Java die Vorteile, daß die Anwendungen auf den verschiedensten Plattformen laufen, die Entwicklungszeit für neue Datenbankanwendungen kürzer werden und die Installation stark vereinfacht wird. Dies hat dazu geführt, daß sich Java als Programmiersprache für Datenbankanwendungen im Internet und Intranet durchgesetzt hat.
Die Entwickler von Java haben bereits sehr früh erkannt, daß in der Industrie ein starkes Interesse an einer Schnittstelle von Java zu Datenbanken besteht. Um zu verhindern, daß einige Hersteller eigene Datenbank-APIs entwickeln, wurde Ende 1995 damit begonnen eine solche Schnittstelle zu entwickeln. Dies war die Geburtsstunde von JDBC. Mit JDK 1.2 wurde 1998 die aktuelle Version JDBC 2.0 veröffentlicht.
Ziel war es eine low-level API zu definieren, die eine Basisfunktionalität von SQL zur Verfügung stellt. Hierbei baute die Arbeit auf X/Open SQL CLI (Call Level Interface) und ODBC (Object Database Connectivity) auf.
JDBC ist eine in Java programmierte API, die SQL-Anweisungen ausführt. Sie besteht aus einer Menge von Klassen und Schnittstellen, die sich im Paket java.sql befinden.
Überblick über das Paket java.sql
Schnittstellen: | Klassen: | Ausnahmen: |
CallableStatement | Date | DataTruncation |
Connection | DriverManager | SQLException |
DatabaseMetaData | DriverPropertyInfo | SQLWarning |
Driver | Time | |
PreparedStatement | Timestamp | |
ResultSet | Types | |
ResultSetMetaData | ||
Statement |
JDBC bietet Klassen und Methoden zur Unterstützung von:
JDBC als Basis-API zur Ausführung von SQL-Anweisungen unterstützt den ANSI SQL-2 Entry Level. Jeder Hersteller von Datenbanksystemen, der für JDBC Treiber entwickeln möchte, muß dafür sorgen, daß jeder JDBC-konforme Treiber diesen Standard unterstützt.
Leider gibt es bei der Umsetzung des SQL-Standard durch die Hersteller von Datenbanksystemen noch einige Probleme. Zwar unterstützen heute alle gängigen DBMS die Grundfunktionalität von SQL, aber speziell bei Datentypen, gespeicherten Prozeduren und äußeren Verbundoperationen gibt es zwischen den verschiedenen Systemen gravierende Unterschiede bei Implementation und Syntax. Dies bedeutet für JDBC eine Reihe von Problemen.
Aus diesem Grund gibt es drei Arten diese Probleme zu lösen:
Die wichtigsten SQL-Befehle, die von JDBC unterstützt werden, sind:
zweischichtige und dreischichtige Modelle
Beim Zugriff einer Java Anwendung auf eine Datenbank sind das zwei- und das dreischichtige Modell zu unterscheiden.
Im zweischichtigen Modell (two-tier model) kommuniziert die Java Anwendung direkt mit der Datenbank. Dies erfordert, daß der JDBC-Treiber mit dem spezifischen DBMS kommunizieren kann. Bei diesem Modell werden die SQL-Anweisungen der Anwendung an die Datenbank gesendet und die Ergebnisse zurückgesendet. Diese Variante wird als Client/Server-Konfiguration bezeichnet.
Im dreischichtigen Modell (three-tier model) werden die Anfragen an eine mittlere Schicht übergeben, die ihrerseits SQL-Anweisungen an die Datenbank sendet. Die Datenbank verarbeitet die Anweisungen und sendet die Ergebnisse zurück an die mittlere Schicht, die diese zurück an die Anwendung schickt. Vorteil dieser Variante ist, daß der Zugriff auf die Datenbank stärker kontrolliert werden kann. Ein weiterer Vorteil der mittleren Schicht ist die Verwendung einer höheren API auf Seiten der Anwendung, die dann durch die mittlere Schicht in eine niedrigere API umgesetzt werden kann. Da die mittlere Schicht typischerweise in Sprachen wie C oder C++ programmiert ist, ist auch eine höhere Performance gegenüber dem zweischichtigen Modell zu erwarten.
JDBC besteht aus zwei Teilen:
Zentraler Teil von JDBC ist der JDBC-Treibermanager. Seine Aufgabe ist es die Java Anwendung mit dem korrekten JDBC-Treibers zu verbinden.
Der andere wichtiger Teil von JDBC ist die JDBC-ODBC-Brücke, die es Java Anwendungen ermöglicht ODBC-Treiber als JDBC-Treiber zu verwenden. Sie hat dafür gesorgt, daß bereits sehr früh eine Anbindung für die wichtigsten Datenbanksysteme bestand. Auch der Zugriff auf weniger populäre DBMS , für die eine Implementierung von JDBC-Treibern nicht geplant ist, ist dadurch möglich. Damit hat dieser Teil hat stark zur Akzeptanz von JDBC beigetragen.
Bei den JDBC-Treibern lassen sich vier Kategorien von Treibern unterscheiden:
Mit Hilfe des JDBC-ODBC-Brückentreibers ist es möglich auf eine ODBC-fähige Datenbank über JDBC zuzugreifen, ohne einen eigenen JDBC-Treiber zur Verfügung zu haben. Hierbei ist zu beachten, daß der ODBC-Binärcode auf jeden Client-Rechner geladen werden muß. Diese Art von Treiber eignet sich darum besonders für Unternehmen, wo die Installation der Software auf dem Client kein Problem darstellt.
Diese Art Treiber übersetzt die JDBC-Aufrufe in Aufrufe einer nativen Datenbank-API. Aber auch hier ist es notwendig, daß wie beim JDBC-ODBC-Brückentreiber Binärcode auf jeden Client-Rechner geladen werden muß. Dies führt in der Regel zu Performanceverlusten.
Diese Art Treiber übersetzt die JDBC-Aufrufe in ein vom DBMS unabhängiges Netzprotokoll, das dann durch einen Server in ein DBMS-Protokoll übersetzt wird. Diese Middleware erlaubt es dem Client die verschiedensten Datenbanken anzusprechen. Damit ist diese Art von Treibern die flexibelste JDBC-Lösung.
Diese Art der Treiber übersetzt die JDBC-Aufrufe direkt in das von dem DBMS verwendeten Netzprotokoll. Da damit der Client-Rechner direkt mit dem DBMS-Server kommuniziert stellt dies wohl die effizienteste Lösung dar. Damit ist es eine exzellente Lösung für das Internet.
Am Anfang einer Datenbankverbindung steht
Dies läßt sich auf zwei Arten realisieren:
- Bei der Initialisierung der Klasse DriverManager wird in der Systemeinstellung nach dem Eintrag sql.drivers gesucht, der eine Liste der registrierten Treiber enthält. Anschließend lädt der Treibermanager jeden dort aufgeführten Treiber.
- Im Programm läßt sich mit Hilfe der Methode Class.forName explizit ein Treiber laden.
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
// Laden des JDBC-ODBC-Brückentreibers
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
// Aufbau der Verbindung zur Datenbank Test
Für die Ausführung von SQL-Anweisungen ist es nötig, daß ein SQL-Statement erzeugt wird, dem die SQL-Anweisung übergeben wird und das diese an die Datenbank sendet. Das Objekt der Klasse Statement wird dabei auf einer bestimmten Datenbankverbindung erzeugt.
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
// Aufbau der Verbindung zur Datenbank Test
Statement stmt = con.createStatement();
// erzeugen eines Statement-Objekts
Für die verschiedenen Arten von SQL-Anweisungen gibt es vier verschiedene Arten von Statements:
ExecuteQuery | PreparedStatement |
ExecuteUpdate | CallableStatement |
Allen ist dabei gemein, daß ihnen ein String mit einem syntaktisch korrekten SQL-Statement übergeben wird. Sie unterscheiden sich lediglich durch die Ergebnismenge und durch Ein-/Ausgabeparameter.
Die Methode executeQuery wird dazu verwendet, um Daten aus einer Datenbank zu selektieren. Das Ergebnis der Methode ist ein Objekt der Klasse ResultSet.
Beispiel:
Statement stmt = con.createStatement();
// erzeugt ein Statement auf der Verbindung
ResultSet rs = stmt.executeQuery("SELECT * FROM Table");
// führt das SQL-Statement aus und übergibt das
// Ergebis an das ResultSet rs
Die Methode executeUpdate wird dazu verwendet, um Manipulationen auf der Datenbank auszuführen. SQL-Statements, wie INSERT, UPDATE, DELETE lassen sich nur mit Hilfe dieser Methode ausführen. Die Anzahl der manipulierten Datensätze wird als Ergebnis der Methode zurückgegeben.
Beispiel:
Statement stmt = con.createStatement();
// erzeugt ein Statement auf der Verbindung
int i = stmt.executeUpdate("DELETE FROM Table");
// führt das SQL-Statement aus
Die Klasse PreparedStatement bietet sich dann an, wenn ein SQL-Statement mehrfach mit verschiedenen Parameterwerten ausgeführt werden soll. Vorteil dieser Klasse ist, daß bei der Erzeugung des Objekts das Statement an die Datenbank gesendet und dabei gegebenenfalls optimiert wird. Dies bedeutet in der Regel einen Performancegewinn.
Für jeden Parameter in einem SQL-Statement wird der Platzhalter ? verwendet. Die Klasse PreparedStatement verfügt über Methoden setXXX, die für jeden Datentyp das Setzen der Parameter ermöglichen (XXX steht für den Datentyp).
Nachdem die Parameter gesetzt wurden läßt sich das SQL-Statement durch Aufruf der Methoden execute, executeQuery oder executeUpdate ausführen.
Beispiel:
PreparedStatement stmt = con.prepareStatement("UPDATE Table SET x = ? WHERE y = ?");
// erzeugt ein PreparedStatement und übergibt ihm
// ein SQL-Statement mit zwei Parametern
stmt.setInt(1, 123);
// setze den ersten Parameter auf 123
stmt.setString(2, "Hi");
// setze den zweiten Parameter auf "Hi"
stmt.executeUpdate();
// führe das SQL-Statement aus
Die Klasse CallableStatement wird zur Ausführung von gespeicherten Prozeduren benutzt. CallableStatement ist hierbei von PreparedStatement abgeleitet. Anders als die Basisklasse verfügt CallableStatement aber über Ein- und Ausgabeparameter. Das füllen der Eingabeparameter funktioniert wie bei PreparedStatement. Für die Verwendung eines Ausgabeparameters ist es nötig, daß der entsprechende Parameter mit dem Typ registriert wird. Für das registrieren der Ausgabeparameter wird die Methode registerOutParameter verwendet.
Beispiel:
CallableStatement stmt = con.prepareCall("{call Test(?, ?)}");
// Aufruf der stored procedure mit zwei Ausgabe -
// parametern
stmt.registerOutParameter(1, java.sql.Types.TINYINT);
// registriert den ersten Parameter als Integer
stmt.registerOutParameter(2, java.lang.String);
// registriet den zweiten Parameter als String
stmt.executeUpdate();
// führe das SQL-Statement aus
byte x = stmt.getByte(1);
// liest den ersten Parameter aus
String y = stmt.getString(2);
// liest den zweiten Parameter aus
JDBC unterstützt Transaktionen und verwendet standardmäßig einen Auto-Commit- Modus. Dies bedeutet, daß jedes Statement als separate Transaktion ausgeführt wird. Für die Ausführung mehrerer Statements innerhalb einer Transaktion muß dieser Mechanismus ausgeschaltet werden. Dazu wird der Aufruf Connection.setAutoCommit(false) verwendet. Anschließend muß eine Transaktion mit den Methoden Connection.commit und Connection.rollback explizit abgeschlossen werden.
Beispiel:
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
Statement stmt = con.createStatement();
try {
con.setAutoCommit(false);
// autocommit Modus ausschalten
stmt.executeUpdate(...);
// Statement
stmt.executeUpdate(...);
// Statement
con.commit();
// bei Erfolg Transaktion abschließen
} catch (SQLException e) {
con.rollback();
// bei Fehler Transaktion rückgängig machen
}
Das Ergebnis einer Anfrage an die Datenbank ist ein Objekt der Klasse ResultSet. Die Struktur dieses Objekts ist eine Tabelle aus einer Anzahl von Datensätzen und den selektierten Spalten. Um die Daten des Ergebnis zeilenweise auszulesen, gibt es in der Klasse ResultSet die Methode next. Mit dieser Methode bewegt man den Ergebniscursor jeweils um eine Zeile weiter. Jede Zeile des Ergebnis enthält Spalten unterschiedlichen Typs. Deshalb stellt die Klasse ResultSet für jeden in JDBC verfügbaren Datentyp entsprechende Methoden getXXX zum Auslesen zur Verfügung. Bei Verwendung der Methoden ist es nötig, daß entweder der Name der Spalte oder die Nummer der Spalte als Argument übergeben wird. GetObject nimmt unter den Methoden eine speziell Rolle ein, da sie als Container für beliebige Daten dienen kann deren Typ nicht bekannt ist.
Arten der getXXX()-Methoden:
GetByte | GetShort |
GetInt | GetLong |
GetFloat | GetDouble |
GetBigDecimal | GetBoolean |
GetString | GetByte |
GetDate | GetTime |
GetTimestamp | GetAsciiStream |
GetUnicodeStream | GetBinaryStream |
GetObject |
Beispiel:
Statement stmt = con.createStatement();
// erzeugt ein Statement auf der Verbindung
ResultSet rs = stmt.executeQuery("SELECT * FROM Table");
// schickt ein SQL-Statement an die Datenbank
// und übergibt das Ergebnis an das ResultSet rs
while (rs.next()) // solange ein Datensatz im ResultSet ist
{
String x = rs.getString(1);
// liest die erste Spalte als String aus
Int y = rs.getInt("number");
// liest die Spalte number als integer aus
}
Abbildung von JDBC- und SQL-Datentypen
Bei der Arbeit mit einer Datenbank ist das Thema "Abbildung von Datentypen" schon immer ein ganz wichtiger Punkt gewesen, so auch bei Java und JDBC. Grund hierfür ist, daß Java und SQL nicht die selben Datentypen bereitstellen. Darum muß dafür gesorgt werden, daß die Daten korrekt abgebildet werden. Das hat nicht unbedingt zu bedeuten, daß die Abbildung von Java- und SQL-Datentypen isomorph sein muß. Ein Beispiel ist zum Beispiel der Java-Datentyp String, der nicht vollständig auf SQL-Datentyp CHAR abgebildet werden kann.
Bei Date, Time und Timestamp handelt es sich um spezielle Java Datentypen. Sie wurden speziell für JDBC definiert. Der Grund dafür liegt in der Definition der ursprünglichen Datumstypen in Java, die nicht zu den SQL-Typen paßten.
Bei einer fehlerhaften Abbildung der Datentypen wird entweder eine Warnung oder eine Exception ausgelöst.
Abbildung von SQL-Datentypen auf Java-Datentypen:
SQL Typ | Java Typ |
CHAR | String |
VARCHAR | String |
NUMERIC | Java.math.BigDecimal |
DECIMAL | Java.math.BigDecimal |
BIT | Boolean |
TINYINT | Byte |
SMALLINT | Short |
INTEGER | Int |
BIGINT | Long |
REAL | Float |
FLOAT | Double |
DOUBLE | Double |
BINARY | Byte[] |
VARBINARY | Byte[] |
LONGVARBINARY | Byte[] |
DATE | Java.sql.Date |
TIME | Java.sql.Time |
TIMESTAMP | Java.sql.Timestamp |
Abbildung von Java-Datentypen auf SQL-Datentypen:
Java Typ | SQL Typ |
String | VARCHAR oder LONGVARCHAR |
java.math.BigDecimal | NUMERIC |
Boolean | BIT |
Byte | TINYINT |
Short | SMALLINT |
Int | INTEGER |
Long | BIGINT |
Float | REAL |
Double | DOUBLE |
byte[] | VARBINARY oder LONGVARBINARY |
java.sql.Date | DATE |
java.sql.Time | TIME |
java.sql.Timestamp | TIMESTAMP |
In der Regel geht man davon aus, daß der Programmierer über das zugrundeliegende Datenbankschema Bescheid weiß. In vielen Fällen wird aber ein dynamischer Datenbankzugriff benötigt, weil Informationen über das DBMS und das Schema der Datenbank fehlen. Um Anwendungen zu programmieren, die unabhängig von der konkreten Struktur einer bestimmten Datenbank arbeiten, werden dafür Strukturinformationen benötigt.
JDBC stellt hierfür die beiden Klassen ResultSetMetaData und DatabaseMetaData zur Verfügung.
Mit der Klasse ResultSetMetaData ist es möglich, Informationen über die Struktur eines ResultSet Objekts zu erhalten. Dazu wird die Methode getMetaData der Klasse ResultSet verwendet.
Beispiel:
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
// Aufbau der Verbindung zur Datenbank Test
Statement stmt = con.createStatement();
// erzeugen eines Statement Objekts
ResultSet rs = stmt.executeQuery("SELECT * FROM Table");
// Ausführung einer SQL-Anweisung
ResultSetMetaData rsmd = rs.getMetaData();
// Bindung des ResultSet Objekts mit einem Objekt
// vom Typ ResultSetMetaData
Mit Hilfe der Methoden getColumnCount, getColumnLabel, getColumnName, getColumnType, getTableName, getColumnDisplaySize und weiterer Methoden lassen sich detaillierte Informationen zum Typ und den Eigenschaften jeder Spalte einer Ergebnismenge abrufen.
Beispiel:
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
// Aufbau der Verbindung zur Datenbank Test
Statement stmt = con.createStatement();
// erzeugen eines Statement Objekts
ResultSet rs = stmt.executeQuery("SELECT * FROM Table");
// Ausführung einer SQL-Anweisung
ResultSetMetaData rsmd = rs.getMetaData();
// Bindung des ResultSet Objekts mit einem Objekt
// vom Typ ResultSetMetaData
while (rs.next()) {
for (int i = 0; i < rsmd.getColumnCount(); i++) {
Object temp = rs.getObject(i);
Sysem.out.println(temp.toString());
}
}
Ähnlich wie ResultSetMetaData über eine Ergebnismenge Informationen einholen kann, so kann die Klasse DatabaseMetaData Informationen über die verwendete Datenbank einholen. Dazu wird die Methode getMetaData der Klasse Connection verwendet.
Beispiel:
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
// Aufbau der Verbindung zur Datenbank Test
DatabaseMetaData dbmd = con.getMetaData();
// Bindung des Connection Objekts mit einem
// Objekt vom Typ DatabaseMetaData
Die Klasse DatabaseMetaData bietet eine Vielzahl von Methoden (mehr als 130 Methoden), um auf Informationen zuzugreifen. Nach ihrem Rückgabewert lassen sich die Methoden in vier Kategorien unterteilen:
Einige ausgewählte Methoden sind z.B. getCatalogs, getColumns, getDriverName, getMaxConnections, getStringFunctions, getURL, isReadOnly, getSQLKeywords, supportsANSI92FullSQL, supportsFullOuterJoins.
Beispiel:
Connection con = DriverManager.getConnection("jdbc:odbc:Test", null, null);
// Aufbau der Verbindung zur Datenbank Test
DatabaseMetaData dbmd = con.getMetaData();
// Bindung des Connection Objekts mit einem
// Objekt vom Typ DatabaseMetaData
boolean ANSI = dbmd.supportsANSI92FullSQL();
// liefert Wahrheitswert, ob DBMS SQL 92
// unterstützt
1997 gründete sich ein Konsortium verschiedener Firmen der IT-Branche, mit dem Ziel, die Zusammenarbeit von Java und relationalen Datenbankbanken zu verbessern. Zu den beteiligten Firmen gehören Compaq (Tandem), IBM, Informix, Micro Focus, Microsoft, Oracle, Sun und Sybase.
Das Ziel von SQLJ wurde in drei Teile gegliedert:
Einbindung von statischen SQL Statements in ein Java-Programm
Nutzung von statischen Java Methoden als SQL Stored Procedures
Verwendung von Java Klassen als SQL Abstract Data Types
Bei der Implementierung von SQLJ wird auf JDBC als low level API aufgesetzt.
SQLJ Teil 0: Embedded SQL in Java
Teil 0 von SQLJ erlaubt die Einbindung von statischen SQL Statements in Java. Dabei funktioniert es auf die selbe Weise, wie es SQL 92 erlaubt, SQL Statements in C und COBOL einzubetten.
Mit der Arbeit an Embedded SQL wurde vor den Teilen 1 und 2 begonnen. Ende 1998 ist SQLJ Teil 0 von der ANSI standardisiert und unter ANSI X3.135.10:1998 veröffentlicht worden. SQLJ Teil 1 und 2 sind im Augenblick als Working Drafts veröffentlicht worden und sollen im Laufe des Jahres 1999 standardisiert werden.
Beispiel nach [EM98]:
try {
#sql {
DELETE
FROM employee
WHERE emp_id = 17
};
// SQLJ Statement
}
catch (SQLException sqe) {
System.out.println(sql.getMessage());
}
Für die Verwendung von SQLJ ist ein Übersetzer nötig. Der Übersetzer sucht die eingebetteten SQL Statements und ersetzt sie durch JDBC Statements, die diese ausführen. Das Ergebnis dieser Übersetzung ist ein Java Programm, daß normal kompiliert werden kann.
Während der Laufzeit des Übersetzers ist es möglich, die Syntax und Semantik der SQL Anweisungen zu überprüfen. Dies kann sowohl offline, als auch online auf der Datenbank geschehen.
Darüber hinaus ist es möglich, daß die Hersteller von DBMS sogenannte custumizer zur Verfügung stellen. Diese Tools erzeugen aus den SQLJ Statements datenbankspezifische SQL Statements (binaries oder profiles). Der generierte Code wird Teil der SQLJ Applikation. Zur Laufzeit wird für das verwendete DBMS entschieden, ob eine entsprechende costumization existiert, ansonsten wird der originale JDBC Code verwendet.
Ein Connection Context Objekt wird verwendet, um ein SQL Statement mit einer bestimmten Datenbankverbindung zu verknüpfen. Connection Context kann dabei explizit oder implizit verwendet werden. Bei der Ausführung eines Statements wird ein Standard Connection Context verwendet, wenn nicht explizit ein Connection Context benutzt wird. Anwendungen mit mehreren Datenbankverbindungen müssen explizite Connection Context Objekte benutzen. Mit Hilfe der Methode ConnectionContext.getDefaultContext kann auf den Standard Connection Context zugegriffen werden.
Beispiel nach [EM98]:
#sql context EmpContext;
// erzeugt Klasse von Connection Context
String url = "jdbc:sybase:Tds:localhost:2638";
EmpContext empCtxt = new EmpContext(url, "dba", "sql", false);
// erzeugt Objekt von der Klasse EmpContext
#sql [empCtxt] {
DELETE
FROM employee
WHERE emp_id = 17
};
// Verwendung des Connection Contexts für die
// Ausführung der SQL-Anweisung
Ein Execution Context Objekt ermöglicht die Ausführung des Statements zu kontrollieren und Informationen über die Ausführung zu erhalten. Wie Connection Context kann der Execution Context implizit oder explizit verwendet werden. Ein Connection Context besitzt einen Standard Execution Context, dieser wird verwendet, wenn kein expliziter Execution Context spezifiziert wurde. Mit Hilfe der Methode ConnectionContext.getExecutionContext kann darauf zugegriffen werden.
Beispiel nach [EM98]:
Sqlj.runtime.ExecutionContext execCtxt = new sqlj.runtime.ExecutionContext();
// erzeugt Objekt der Klasse ExecutionContext
#sql [empCtxt, execCtxt]
{
DELETE
FROM employee
WHERE emp_id = 17
};
// Verwendung des Connection Contexts für die
// Ausführung der SQL-Anweisung
System.out.println("Deleted " + execCtxt.getUpdateCount() + " rows.");
// gibt die Anzahl der geänderten Datensätze aus
SQLJ Teil 0 ermöglicht die Verwendung von Host Variablen und Ausdrücken. Die Art der Parameter (IN, OUT, INOUT) wird implizit durch ihre Verwendung spezifiziert. Die
Syntax der Host Variablen ist folgende:
<host expression> ::=
: [ <parameter mode> ] <expression>
<parameter mode> ::= IN | OUT | INOUT
<expression> ::= <variable> | ( <complex expression> )
Beispiel nach [EM98]:
int id;
#sql { SELECT emp_id
INTO :id
FROM employee
WHERE emp_fname LIKE :(argv[0] + '%')
};
// Verwendung von Host Variablen
// id als Ausgabeparameter und
// (argv[0] + '%') als Eingabeparameter
System.out.println("Employee id " + id);
// gibt die ID des Mitarbeiters aus
Um gespeicherte Routinen aufzurufen wird das SQL CALL Statement benutzt.
Beispiel nach [EM98] (Prozedur):
int count = 0;
#sql { CALL emp_count (:in (argv[0]),
:in (argv[1]),
:out count)
};
// Aufruf der gespeicherten Prozedur emp_count
// mit Übergabe von zwei Eingabeparametern und
// einem Ausgabeparameter
System.out.println("The result is " + count);
// gibt die Anzahl der Mitarbeiter aus
Beispiel nach [EM98] (Funktion):
int count = 0;
#sql { count = { VALUES emp_count2
(:in (argv[0]),
:in (argv[1])
)
}
};
// Aufruf der gespeicherten Prozedur emp_count2
// mit Übergabe von zwei Eingabeparametern,
// Rückgabewert wird Variable count übergeben
System.out.println("The result is " + count);
// gibt die Anzahl der Mitarbeiter aus
Das wohl am häufigsten verwendete Statement in Anwendungen ist das SELECT Statement. Um auf das Ergebnis eines solchen Statements zuzugreifen wird ein Cursor (iterator) verwendet. SQLJ Teil 0 unterstützt zwei Arten von Iteratoren:
Beispiel nach [EM98] (named iterator):
#sql iterator Employee
(
int emp_id,
String emp_lname,
Java.sql.Date start_date
);
Employee emp;
#sql emp = { SELECT emp_lname, emp_id, start_date
FROM employee
WHERE emp_fname LIKE 'C%'
};
while (emp.next()) {
System.out.println(emp.start_date() + ", " + emp.emp_id() + ", " + emp.emp_lname().trim());
}
emp.close();
Beispiel nach [EM98] (positioned iterator):
#sql iterator Employee (int, String, String);
int emp_id = 0;
String emp_lname = null;
String emp_fname = null;
Employee emp;
#sql emp = { SELECT emp_id, emp_lname, emp_fname
FROM employee
WHERE emp_fname LIKE 'C%'
};
while (true) {
#sql { FETCH emp INTO :emp_id,
:emp_lname,
:emp_fname };
if (emp.endFetch()) break;
System.out.println(emp_fname.trim() + " "
+ emp_lname.trim()
+ ", " + emp_id
);
}
SQLJ Teil 1: Java Methoden als SQL Prozeduren
Teil 1 von SQLJ ermöglicht es in einem DBMS statische Java Methoden als SQL stored procedures zu benutzen. Funktionalität und Verwendung dieser Java Methoden ist dabei identisch zu SQL stored procedures.
Damit Java Methoden in SQL benutzt werden können, muß zuerst die Java Klasse installiert und anschließend daraus eine SQL Prozedur bzw. Funktion definiert
werden.
Vor der Installation der Java Klassen müssen diese kompiliert und anschließend in ein JAR File gepackt werden.
Beispiel nach [SQLJ98/2]:
public class Routines1 {
public static Integer region(String s) throws SQLException {
if (s =="MN" || s == "VT") return 1;
else if (s == "FL" || s == "GA" ||s == "AL") return 2;
else if (s == "CA" || s == "AZ" || s== "NV") return 3;
else return 4;
}
public static void correctStates(String oldSpelling, String newSpelling) throwsSQLException {
Connection con =
DriverManager.getConnection(JDBC:DEFAULT:CONNECTION");
PreparedStatement stmt =
con.prepareStatement("UPDATE emps SET state = ? WHERE state = ?");
stmt.setString(1, newSpelling);
stmt.setString(2, oldSpelling);
stmt.executeUpdate();
con.close();
return;
}
}
Installation von Java Klassen in SQL
Um Java Klassen in SQL zu nutzen wird als erstes das jar file durch Aufruf der Prozedur sqlj.install_jar in das DBMS geladen.
Für das Beispiel nach [SQLJ98/2] Routines1 könnte dies folgendermaßen aussehen:
sqlj.install_jar('file:~/classes/Routines1.jar', 'routines1_jar')
Der erste Parameter dieser Prozedur gibt das JAR File der zu installierenden Java Klasse und der zweite den SQL Namen der installierten Klasse an. Der SQL Name wird in der Folge für die Adressierung der Klasse verwendet.
Zur Deinstallation bzw. zum Ersetzen der JAR Files werden die Prozeduren sqlj.remove_jar bzw. sqlj.replace_jar verwendet.
Für das Beispiel nach [SQLJ98/2] Routines1 könnte dies folgendermaßen aussehen:
// ersetzt das installierte jar file durch das angegebene neue file
sqlj.replace_jar('file:~/classes/Routines1.jar', 'routines1_jar')
// entfernt das installierte jar file
sqlj.remove_jar('routines1_jar')
Vergabe von SQL Namen für Funktionen und Prozeduren
Nachdem eine Klasse installiert wurde, müssen die Methoden der Klasse dem DBMS bekannt gemacht werden. Dies geschieht, indem Namen für die Java Methoden definiert werden. Dafür werden die SQL Statements create procedure bzw. create function benutzt. Durch diesen Mechanismus ist es auch möglich mehrere verschiedene Namen für eine Java Methode zu definieren.
Das Löschen des Namens ist mit Hilfe des SQL Statements drop möglich.
Beispiel nach [SQLJ98/2] für die beiden Methoden region und correctState der Klasse Routines1:
create procedure correct_states(old char(20), new char(20))
modifies sql data
external name 'routines1_jar:Routines1.correctState'
language java parameter style java;
create function region_of(state char(20)) return integer
no sql
external name 'routines1_jar:Routines1.region'
language java parameter style java;
Zuerst wird der SQL Name der Methode zusammen mit den Ein- und Ausgabeparametern übergeben. Dabei wird auch die Art und Weise der Ausgabeparameter spezifiziert (siehe 3.2.4 Verarbeitung von Ausgabeparametern und 3.2.5 Verarbeitung von Resultsets).
Anschließend wird definiert, in wie weit die Java Methode Zugriff auf SQL Daten hat. Dazu werden die folgenden Klauseln verwendet:
no sql: keinerlei SQL Operationen
contains sql: SQL Operationen, aber kein lesen und modifizieren
reads sql data: SQL Operationen, lesen, aber kein modifizieren
modifies sql data: SQL Operationen, lesen und modifizieren
Im Anschluß daran wird die Quelle der Methode durch Angabe der Namen von JAR Files, Klasse und Methode angegeben.
Zum Schluß wird dann noch die Sprache der Methode spezifiziert (Java, SQLJ, ...).
Überladen von Java Methoden und SQL Namen
In SQLJ ist es möglich Prozeduren und Funktionen zu überladen und damit ein und den selben Namen mehrmals zu benutzen. Dies geschieht völlig unabhängig von Java. Damit überladene Prozeduren und Funktionen in SQL genutzt werden können, müssen einerseits die Java Methoden überladen werden und anderseits bei der Definition des Funktions- und Prozedurnamen dieser auch überladen werden.
Beispiel nach [SQLJ98/2]:
public class Over {
public static boolean isOdd(Integer) {...};
public static boolean isOdd(Float) {...};
}
create function odd(integer) return bit
external name 'over_jar:Over.isOdd'
language java parameter java;
create funtion odd(real) return bit
external name 'over_jar:Over.isOdd'
language java parameter java;
Verarbeitung von Ausgabeparameter
Bei der Arbeit mit stored procedures werden oft Ausgabeparameter benötigt. SQL unterstützt diese, Java hingegen macht keinen Unterschied zwischen Ein- und Ausgabeparametern. Darum verwendet SQLJ Arrays als Ausgabeparameter. Diese Arrays beinhalten nur ein Element, den Rückgabewert. Bei der Verwendung dieser Prozeduren sind diese Arrays allerdings nicht sichtbar, sondern nur in der Java Methode. Beim Aufruf einer solchen Prozedur wird ein normaler skalarer Wert zurückgeliefert. Das Mapping zwischen Java Array und skalarem Datentyp geschieht hierbei implizit.
Beispiel:
public class Routines1 {
public static void emp_age(int id, int[] age) throws SQLException {
Connection con = DriverManager.getConnection("jdbc:default:connection");
PreparedStatement stmt =
con.prepareStatement("SELECT age FROM emp WHERE id = ?");
stmt.setInteger(1, id);
Result rs = stmt.executeQuery();
rs.next();
age[0] := rs.getInt("age");
return;
}
}
Ein spezieller Fall des Ausgabeparameter sind Resultsets. SQL stored procedures können diese verwenden, darum muß es auch möglich sein, daß Java Methoden Resultsets als Ausgabeparameter benutzen können. Dazu wird in der Methode ein zusätzlicher Parameter eingeführt. Dieser Parameter ist ein Array der Klasse ResultSet oder einer Klasse, die vom SQLJ Iterator abgeleitet wurde. Durch diese Vorgehensweise ist es möglich, daß eine Java Methode mehrere Resultsets zurückliefern kann.
Beispiel nach [SQLJ98/2]:
public class Routines1 {
public static void orderedEmps(int regionParm, ResultSet[] rs) throws SQLException {
Connection con = DriverManager.getConnection("jdbc:default:connection");
PreparedStatement stmt =
con.prepareStatement("SELECT name, region_of(state) as region, sales
FROM emp WHERE region_of(state) > ? and
sales IS NOT NULL
ORDER BY sales DESC");
stmt.setInt(1, regionParm);
rs[0] = stmt.executeQuery();
return;
}
}
Vergabe von SQL Zugriffsrechen
Zugriffsrechte können für die installierten JAR Files, Prozeduren und Funktionen definiert werden. Dies wird durch die SQL Statements grant und revoke realisiert.
Beispiel nach [SQLJ98/2]:
// Benutzungsrecht für das jar file routines1_jar
grant usage on routines1_jar to Smith
// Ausführungsrecht für die Prozedur/Funktion correct_states
grant execute on correct_states to Smith
Bei der Fehlerbehandlung in SQLJ wird zwischen den SQLJ Prozeduren und den Java Methoden unterschieden.
Fehler, die in einer Java Methode auftreten, können in der Methode durch Java Exceptions abgefangen werden. Wenn ein Fehler nicht abgefangen wird, so wird der String der Fehlermeldung (SQLException.toString()) an das DBMS zurückgegeben.
Fehler, die bei der Verwendung von SQLJ Prozeduren entstehen, werden durch SQL Exceptions abgefangen. Dabei wird ein spezifischer SQLSTATE Code ausgegeben.
SQLSTATE
Condition | Class | Subcondition | Subclass |
Java DLL | 46 | Invalid URL | 001 |
Java DLL | 46 | Invalid jar name | 002 |
Java DLL | 46 | Invalid jar deletion | 003 |
Java DLL | 46 | Invalid jar name | 004 |
Java DLL | 46 | Invalid replacement | 005 |
Java DLL | 46 | Invalid grantee | 006 |
Java DLL | 46 | Invalid signature | 007 |
Java DLL | 46 | Invalid methode specification | 008 |
Java DLL | 46 | Invalid REVOKE | 009 |
Java execution | 46 | Invalid null value | 101 |
Java execution | 46 | Invalid jar name in path | 102 |
Java execution | 46 | Unresolved class name | 103 |
Java execution | 46 | Too many result sets | 104 |
Uncaught Java exception | 46 | (no subclass) | 200 |
Uncaught Java exception | 46 | User-defined (see above) | mmm |
User-defined (see above) | nn | User-defined (see above) | mmm |
Bei der Installation von Java Klassen in SQL sind ein oder mehrere create procedure/function Aufrufe notwendig. Analog sind ein oder mehrere drop Aufrufe notwendig, um die Java Klassen wieder zu entfernen. Um diesen Aufwand zu reduzieren wurde das Konzept des Deployment Descriptors eingeführt. Diese Deployment Descriptors befinden sich im JAR File. Sie beinhalten eine Anzahl von SQL Statements, die bei der Installation bzw. Deinstallation automatisch ausgeführt werden.
Beispiel nach [SQLJ98/2]:
SQLAction[] = {
"BEGIN INSTALL
create procedure correct_states(old char(20), new char(20))
modifies sql
external name 'thisjar:Routines1.correctStates'
language java parameter style java;
grant execute on correct_states to Baker;
create function region_of(state char(20)) return integer
no sql
external name 'thisjar:Routines1.region'
language java parameter style java;
grant execution on region_of to public;
END INSTALL"
"BEGIN REMOVE
revoke execute on correct_states from Baker;
drop procedure correct_states restrict;
revoke execution on region_of from public;
drop function region_of;
END REMOVE"
}
Verwendung von Java Klassen als SQL Prozeduren
Wie bereits am Anfang dieses Kapitels ausgeführt lassen sich die Java Klassen nach der Installation verwenden, als ob es normale stored procedures wären. Für den Aufrufer der Methode ist nicht ersichtlich, daß es sich dabei um eine Java Methode handelt.
Beispiel nach [SQLJ98/2]:
SELECT name, region_of(state) as region
FROM emps
WHERE region_of(state) = 4
SQLJ Teil 2: Java Klassen als SQL Datentypen
Teil 2 von SQLJ beschäftigt sich damit, wie man Java Klassen als SQL Typen verwenden kann. Die Programmierung von Datenbankanwendungen mit Java wird damit deutlich erleichtert, da das Mapping von Java und SQL Datentypen entfällt.
Voraussetzung für die Verwendung einer Java Klasse als SQL Datentyp ist, daß die Klasse das Interface von java.io.Serializable oder java.sql.SQLData implementiert.
Damit eine Java Klasse als SQL Datentyp verwendet werden kann, muß diese zuerst installiert und anschließend daraus ein SQL Datentyp definiert wird. Die Installation der Java Klasse geschieht auf die gleiche Weise, wie in Kapitel 3.2.1 Installation von Java Klassen in SQL beschrieben ist. Natürlich ist es auch für SQL Datentypen möglich Zugriffsrechte zu vergeben. Dies ist in Kapitel 3.2.6 Vergabe von SQL Zugriffsrechten beschrieben.
Beispiel nach [SQLJ98/3]:
public class Adress implements java.io.Serializable {
public String street;
public String zip;
public static int recommendedWidth = 25;
public Adress() {
street = "Unknown";
zip = "None";
}
public Adress(String s, String z) {
street = s;
zip = z;
}
public String toString() {
return "Street= " + street + " Zip= " + zip;
}
}
Vergabe von SQL Namen für SQL Datentypen
Nachdem das JAR File mit den Java Klassen erfolgreich installiert wurde müssen die Klassen noch als Datentypen registriert werden, damit sie in SQL benutzt werden können. Dies wird mit dem SQL Statement create type realisiert.
Beispiel nach [SQLJ98/3]:
create type addr external name 'Adress' language java
(
zip_part char(10) external name 'zip',
street_part varchar(50) external name 'street',
static method rec_width_part() return integer external variable name 'recommendedWidth',
method another_addr() return addr external name 'Adress'
method another_addr(s_parm varchar(50), z_parm char(10)) return addr external name 'Adress',
method string_rep() return varchar(255) external name 'toString'
)
Als erstes wird der Name des neuen Datentyp und die Quelle zusammen mit der Sprache angegeben. Im Anschluß daran werden die gewünschten Variablen und Methoden mit ihren Schnittstellen und der Quelle beschrieben. Dabei müssen nicht zwangsläufig alle Variablen und Methoden verwendet werden.
Interessant ist die Variable recommendedWidt, die in Java als statisch deklariert ist. Da SQLJ keine statischen Variablen kennt wird diese Variable in SQLJ als statische Methode rec_width_part benutzt.
Verwendung von Java Klassen als SQL Datentypen
Die Arbeit mit Java Klassen als SQL Datentypen funktioniert nicht ganz so, wie man das von den üblichen SQL Datentypen gewohnt ist. Die Benutzung dieser Datentypen lehnt sich stark an die Benutzung von Objekten in Java an. Wichtig ist, daß bei der Benutzung eines solchen Datentyps immer eine Instanz der Java Klasse erzeugt werden muß. Dies wird mit Hilfe des new Operators gemacht, der einen passenden Konstruktor der Java Klasse aufruft. Für den Zugriff auf Variablen und Methoden des Datentyps wird der Punkt Operator verwendet.
Beispiel:
INSERT INTO emps VALUES('Bob Smith',
new addr(432 Elm Street', '95123'))
SELECT name, home_addr.zip_attr
FROM emps
WHERE home_addr.street_attr = '456 Shoreline Drive'
UPDATE emps
SET home_addr.street_attr = '457 Shoreline Drive',
home_addr.zip_attr = '99323',
WHERE home_addr.to_String() like '%456%Shore%'
Bei der Verwendung von abgeleiteten Klassen in SQL kann es zu einigen Problemen kommen. Denkbar wäre, daß man einen Datentyp von der abgeleiteten Klasse erzeugt, ohne einen entsprechenden Datentyp der Superklasse zu besitzen. In diesem Fall ist es sogar möglich Variablen und Methoden der Superklasse für den Datentyp zu verwenden. Mit der Definition under im create procedure/function Statement lassen sich diese Probleme verhindern. Es sorgt dafür, daß nur die Variablen und Methoden der aktuellen Klasse verwendet werden können.
Java Blend ist ein Tool von Sun Microsystems Inc. Java Blend ermöglicht es zu Java Klassen das entsprechende Datenbankschema bzw. zu einem existierenden Datenbankschema die entsprechenden Java Klassen automatisch zu erzeugen.
Bei den bisher erwähnten Methoden des Datenbankzugriffs war es nötig, daß der Entwickler sowohl Kenntnisse über die Programmiersprache Java, also auch Kenntnisse über SQL besitzen mußte. Sun versucht mit einer neuen Technologie namens Object/Relational Mapping dieses Problem zu lösen, indem der Entwickler sich gar nicht mehr um die Datenbank oder um SQL Statements kümmern muß. Der Entwickler schreibt die Java Anwendung und Java Blend setzt die Klassen anschließend in ein entsprechendes Datenbankschema um.
Eine solche Umsetzung könnte im einfachsten Fall so aussehen:
SQL | Java |
CREATE
TABE customer ( custid INTEGER NOT NULL, adress VARCHAR(50), rep INTEGER, PRIMARY KEY (custid), FOREIGN KEY (rep) REFERENCES salesrep) |
Class
Customer Implements Persistence Capable { Int custID; String adress; SalesRep rep; } |
Nach [SUN98/1]
Java Blend bietet dabei folgende weitere Möglichkeiten:
- Caching
- Multi-Threading
- Eigenen Query Prozessor
Mit der Entwicklung von JDBC im Jahre 1996 wurde für Java eine standardisierte Datenbank-API geschaffen. Die Integration von ODBC hat zu einer großen Akzeptanz der API in der Industrie geführt, da von Anfang an eine große Anzahl an DBMS unterstützt wurden. Damit wurde verhindert, daß verschiedene Hersteller eigene inkompatible Datenbank-APIs für Java schaffen. In der Zwischenzeit hat sich JDBC als Standard durchgesetzt.
Die alternative Methode des Datenbankzugriffs ist keine konkurrierende API, sondern setzt auf JDBC auf. SQLJ bietet Entwicklern eine high-level API, die vergleichbar mit anderen APIs für C, C++ oder COBOL ist. Durch die Zusammensetzung des Konsortiums aus den wichtigsten Herstellern von Software und Datenbanksystemen ist eine große Akzeptanz von SQLJ zu erwarten. Mittlerweile gibt es auch bereits die ersten Implementationen, z.B. Oracle 8i, Sybase Adaptive Server Anywhere 6.0.
Abschließend läßt sich sagen, daß Java eine hervorragende Basis für Datenbankanwendungen ist. Java bietet die Vorteile der Programmiersprache, wie Sicherheit, Stabilität und Robustheit und verbindet diese mit der Möglichkeit des einfachen und sicheren Datenbankzugriffs mittels JDBC. Weiterer Vorteil von Java ist die Portierbarkeit auf verschiedene Plattformen, was die Entwicklungszeit für Datenbanksoftware drastisch reduziert. Speziell für Firmen mit Intranet ist Java durch die Verwendung von Applets sehr interessant geworden. Dies alles wird dazu führen, daß in den nächsten Jahren vermehrt Datenbankanwendungen in Java geschrieben werden.
JDBC - Datenbankzugriff mit Java, Addison-Wesley, 1998
JDBC: A Java SQL API, 1997
SQLJ Part 0, now known as SQL/OLB (Object-Language Bindings), 1998
SQLJ: Java and Relational Databases, 1998
Working Draft, 1998
Working Draft, 1998