Artikel JavaMagazin - reeCon IT

Werbung
Artikel JavaMagzin 10/2010
Kommunikation zwischen ORACLE und Non-ORACLE
Datenbanken
oder
Mit JEE eine ORACLE-Brücke bauen
Der Datenaustausch zwischen ORACLE Datenbanken ist so einfach
wie eine simple SQL-Abfrage. Auch Non-ORACLE Datenbanken
bieten gute direkte Zugriffsmöglichkeiten auf die ORACLE RDBMS
(wie SQL Server, MySQL oder auch MS Access, z.B. via ODBC).
Doch wie sieht es beim Datenbankgiganten selbst aus? Welche
Möglichkeiten bieten sich hier, um an „artfremde“ Informationen zu
kommen?
Andreas Reese
In heterogenen IT-Landschaften, wie sie oft bei großen Unternehmen zu finden sind, gibt
es in der Regel auch Datenbanken von unterschiedlichen Anbietern. Während man im
Systemumfeld zwischen SAP und NON-SAP unterscheidet, bietet sich dieser Blickwinkel im
Datenbanksektor in Bezug auf ORACLE. Große SAP-Systeme oder entsprechend aufgestellte
Individualentwicklungen basieren häufig auf einer ORACLE Datenbank. Daneben existieren
aber auch branchenspezifische Produkte, die beispielsweise einen SQL Server aus dem Hause
Microsoft im Gepäck haben.
Von besonderem Interesse für diesen Artikel sind die ORACLE-basierten
Individualentwicklungen, die bei den Unternehmen etwa aufgrund von fehlenden oder nicht
ausreichenden Standardprodukten entstehen und nicht zuletzt wichtige Informationen aus
verschiedenen Systemen zusammenbringen sollen. Da die Anwendung als Schnittstelle zum
Benutzer in der Regel nichts vom Datenaustausch mitbekommen soll, findet dieser sehr oft
auch auf Ebene der Datenbank statt. Wie können nun ORACLE- und NON-ORACLE
Datenbanken miteinander kommunizieren? Indirekt geschieht das in der Regel über
Schnittstellen, die jedoch nicht Thema dieses Artikels sein sollen. Aber kann man auch direkt
aus einer ORACLE-Session heraus in einer Access-Datenbank oder gar einem SQL Server
Informationen lesen oder gar Daten manipulieren?
Für ein besseres Verständnis zunächst ein kurzer Blick auf die möglichen
Konstellationen:
ORACLE <-> ORACLE
Hier bietet die Datenbank das bequeme Feature des Datenbanklinks. Nach Anlage eines
entsprechenden Links kann direkt per SQL-Abfrage auf die Objekte (wie Tabellen) der
entfernten Datenbank zugegriffen werden.
NON-ORACLE
ORACLE
Auch diese Konstellation ist eher unproblematisch, da viele Datenbanken bereits eine
einfache Möglichkeit mitbringen, um auf ORACLE zuzugreifen (wie z.B. per ODBC von MS
Access aus).
ORACLE
NON-ORACLE
Wie zu erwarten ist, bietet natürlich auch ORACLE einen Service an, mit dem eine
ODBC-Verbindung möglich ist. Das Produkt nennt sich Heterogeneous Services und stellt
nach erfolgreicher Installation und Konfiguration eine überaus bequeme Möglichkeit der
Datenabfrage und –manipulation auf der Datenbank des Drittanbieters zur Verfügung. Dies
geschieht dann ähnlich komfortabel per SQL-Abfrage und Datenbanklink.
Abb.1: ORACLE Heterogeneous Services
Dazu wird zunächst eine ODBC-Verbindung auf dem ORACLE Datenbankserver
eingerichtet, sowie ein entsprechender ODBC-Agent auf dem Server des Drittanbieters
installiert (siehe Abb. 1). Nun müssen noch der ORACLE TNS-Listener sowie die eine oder
andere Konfigurationsdatei angepasst werden und schon kann der Zugriff über einen neu
anzulegenden Datenbanklink erfolgen. Die genaue Vorgehensweise kann in diversen
Anleitungen sowie in der entsprechenden ORACLE-Dokumentation nachgelesen werden.
Dieses Produkt funktioniert in den meisten Fällen. Doch was tun, wenn Betriebssystem,
Serverprozessor und der Heterogeneous Service nicht miteinander harmonieren? Unnötig zu
erwähnen, dass das oben beschriebene Vorgehen unter UNIX (insbesondere das Einrichten
einer ODBC-Quelle) nichts für Anfänger ist.
Eine geschickte Kombination der folgenden Tools und Datenbankobjekte liefert eine
Lösung, die immer funktioniert:
•
•
•
•
•
JDBC-Treiber des jeweiligen Drittanbieters
ORACLE JavaStoredProcedures
ORACLE PL/SQL
ORACLE Collections und Objekte
ORACLE Views und Trigger
Das Zusammenspiel dieser Komponenten soll anhand eines lesenden Zugriffs auf einen
SQL Server verdeutlicht werden (siehe Abb. 2).
Lesender Zugriff auf einen SQL Server
Abb. 2: Lesender Zugriff auf einen SQL Server aus der ORACLE RDBMS heraus
Die relevanten Tabellen des SQL Servers werden zunächst 1:1 als ORACLE Objekte in
der RDBMS erzeugt. Nehmen wir dazu eine Tabelle tab1 mit mehreren Spalten
unterschiedlicher Datentypen. Dann kann das adäquate ORACLE-Objekt wie folgt erzeugt
werden:
CREATE OR REPLACE TYPE sqls_tab_obj AS OBJECT
(
COL1
VARCHAR2(50),
COL2
NUMBER(10),
COL3
DATE
);
Da in der Praxis stets mehrere Zeilen benötigt werden, muss dem Objekt eine weitere
Dimension in Form einer Collection hinzugefügt werden:
CREATE OR REPLACE TYPE sqls_tab IS TABLE OF sqls_tab_obj;
Die spätere SQL-Abfrage wird dann direkt auf dieser Collection sqls_tab ausgeführt,
deren zugrundeliegende Struktur genau der Zieltabelle auf dem SQL Server entsprechen
muss. Das Hauptaugenmerk liegt jedoch auf einer oder auch mehrerer Java Klassen, welche
die eigentliche Arbeit erledigen:
• Herstellen einer Verbindung zum SQL Server
• Abfrage der entsprechenden Tabelle
• Umleitung der Daten in die ORACLE Collection
Damit dies innerhalb der Datenbank funktioniert, hat ORACLE mit der Version 8i die
JavaStoredProcedure eingeführt. Dabei handelt es sich um Java Klassen, die als Alternative
zur Datenbanksprache PL/SQL innerhalb der Datenbank benutzt werden können.
Die Java Klasse muss zunächst per JDBC die Verbindungen zum SQL Server und auch
zur ORACLE-Datenbank herstellen. Die Verbindung zum SQL Server erfolgt traditionell
über JDBC:
import
import
import
import
com.microsoft.sqlserver.jdbc.SQLServerDriver;
java.sql.Connection;
java.sql.DriverManager;
java.util.HashMap;
--->
// get configurations (e.g. from config.properties)
HashMap settings = new HashMap();
ResourceBundle bundle = ResourceBundle.getBundle("config");
settings.put("SERVER",bundle.getString("db.server");
settings.put("PORT",bundle.getString("db.port");
settings.put("DBNAME",bundle.getString("db.dbname");
settings.put("USER",bundle.getString("db.user");
settings.put("PASSWORD",bundle.getString("db.password");
// register driver to SQL Server
DriverManager.registerDriver(new SQLServerDriver());
// establish connection
Connection connSQL = DriverManager.getConnection("jdbc:sqlserver://"
+ settings.get("SERVER").toString() + ":"
+ settings.get("PORT").toString()
+ ";databaseName="
+ settings.get("DBNAME").toString()
, settings.get("USER").toString(), settings.get("PASSWORD").toString()));
Auch eine Verbindung zur ORACLE-Datenbank ist zwingend erforderlich. Da sich die
Klasse jedoch in der Datenbank befindet und auch dort ausgeführt wird, lässt diese sich
einfach die Standardverbindung geben:
import oracle.jdbc.OracleDriver;
--->
Connection connORA = new OracleDriver().defaultConnection();
Der Aufruf einer Java Klasse in der Rolle als JavaStoredProcedure kann über eine
beliebige statische Methode erfolgen. Der Start über die traditionelle Methode main ist hier
jedoch nicht zulässig:
import
import
import
import
java.sql.Connection;
java.sql.PreparedStatement;
java.sql.ResultSet;
java.sql.SQLException;
import java.util.Vector;
import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;
--->
public static ARRAY readFromSQLServer(ARRAY in)
{
// initialize SQL array
ARRAY[] out = new ARRAY[1];
// fill SQL array
try {
readFromDatabase(in,out); }
catch(SQLException e)
{ System.err.println(e.getMessage()); }
// return SQL array
return out[0];
}
Die Methode readFromSQLServer erhält einen Parameter vom ORACLE Datentyp
ARRAY und gibt einen ebensolchen auch wieder zurück. Der übergebene Parameter liefert die
Metadaten der in der Datenbank angelegten ORACLE Collection, jedoch keinerlei Daten. Der
zurückgegebene Parameter enthält die aus dem SQL Server gelesenen Daten und schreibt
diese automatisch in die ORACLE Collection zurück. Man beachte, dass der erzeugte
Parameter die Dimension 1 hat. Der Datentyp ARRAY bietet zahlreiche Möglichkeiten, um
aufgrund der Zuordnung der Collection zum Parameter dessen Struktur auszulesen. Mit
Methoden wie getSQLTypeName() oder getBaseTypeName() können die Namen der
Collection sowie des zugrundeliegenden Objektes ermittelt werden. Nun ist es relativ einfach
per JDBC das ORACLE Repository auszulesen, um die Spaltennamen des Objektes zu
erfahren. Damit kann dann ein SQL-Statement gebaut werden, das die Zieltabelle auf dem
SQL Server ausliest:
private static void readFromDatabase(ARRAY in, ARRAY[] out) throws SQLException
{
// read from SQLServer
PreparedStatement stmt = connSQL.prepareStatement(
"SELECT col1, col2, col3 FROM tab1");
ResultSet rset = stmt.executeQuery();
// fill column vectors
int idx = 0;
Vector[] a = new Vector[3];
for(int i=0; i<3; i++)
a[i] = new Vector();
while(rset.next())
{
for(int i=0; i<3; i++)
a[i].add(idx,rset.getString(i+1));
idx++;
}
rset.close();
stmt.close();
// copy column vectors to multi-dimensional array
String[][] arr = new String[a[0].size()][3];
for(int i=0; i<a[0].size(); i++)
for(int j=0; j<3; j++)
arr[i][j] = (String) a[j].get(i);
// copy mult-dimensional array to SQL array
ArrayDescriptor ads = ArrayDescriptor.createDescriptor(ds.getSQLTypeName(),connORA);
out[0] = new ARRAY(ads,connInt,arr);
}
Nachdem die Daten aus dem ResultSet in ein 2-dimensionales Array kopiert wurden,
erfolgt der eigentliche Kunstgriff. Nun werden die gelesenen Daten dem Parameter vom Typ
ARRAY zugeordnet und landen damit in der ORACLE Collection innerhalb der Datenbank.
Die fertige(n) Java Klasse(n) können nun mit dem ORACLE loadjava-Befehl in die
Datenbank transportiert werden:
loadjava -u scott/tiger -v -resolve SQLServerReader.class
Die hier benutzten Schalter und ihre Bedeutungen:
• -u
• -v
• -resolve
User/Password
verbose (Ausgabe Logging)
Auflösung von Abhängigkeiten
Neben der selbst erstellten Klasse sollte keinesfalls der JDBC-Treiber des Herstellers (hier
Microsoft) vergessen werden. Die entsprechenden Bibliotheken müssen ebenfalls in die
Datenbank geladen werden. Ein Upload von jar-Archiven ist selbstverständlich möglich und
verringert den Aufwand ungemein.
Nun befindet sich die Java Klasse innerhalb der Datenbank. Um diese benutzen zu
können, ist jedoch ein PL/SQL-Wrapper erforderlich, der die Verbindung zu der jeweiligen
statischen Methode einer Klasse herstellt und die Typkonvertierungen zwischen denen der
Datenbank und Java vornimmt:
CREATE OR REPLACE FUNCTION get_sqls_data
(
p_in
IN sqls_tab
)
RETURN sqls_tab
AS
LANGUAGE JAVA NAME 'SQLServerReader.readFromSQLServer(oracle.sql.ARRAY)
return oracle.sql.ARRAY';
Zu guter Letzt muss noch die Verbindung zum SQL Server freigegeben werden:
dbms_java.grant_permission('SCOTT','SYS:java.net.SocketPermission'
,'sqls.your_domain.de:1433'
,'connect,resolve');
Jetzt sind alle Voraussetzungen erfüllt, um jede beliebige Tabelle des freigegebenen SQL
Servers abzufragen. Dazu müssen dann lediglich die Tabellenpendants in Form von ORACLE
Objects und Collections angelegt sowie ein neuer PL/SQL-Wrapper in Form einer kurzen
Funktion erzeugt werden. Eine SQL-Abfrage sieht dann wie folgt aus:
SELECT *
FROM TABLE(get_sqls_data(sqls_tab()));
Die SQL-Abfragen können beliebig komplex sein und auch mit anderen Statements
kombiniert werden (JOINS). Die SQL Server-Tabellen verhalten sich nun wie herkömmliche
ORACLE-Tabellen und werden (fast) genauso angesprochen. Wie der Leser sicherlich ahnt,
ist die Performance, bedingt durch die zusätzliche Abfrage gegen die fremde Datenbank,
geringfügig schlechter. Die Java Klasse sollte daher in dem Sinne eine Erweiterung erfahren,
dass z.B. einschränkende WHERE-Bedingungen mitgegeben und direkt gegen den fremden
Server ausgeführt werden können. Dadurch lässt sich die zu übertragende Datenmenge
deutlich reduzieren.
Die Welt ist jedoch keine Einbahnstraße, d.h. auch die umgekehrte Richtung ist möglich.
Es können aus einer ORACLE-Datenbank heraus Daten in einen SQL Server transportiert
oder vorhandene Daten manipuliert werden.
Schreibender Zugriff auf einen SQL Server
Abb. 2: Schreibender Zugriff auf einen SQL Server aus der ORACLE RDBMS heraus
Die umgekehrte Richtung, also Daten in einen SQL Server zu schreiben oder dort zu
manipulieren, funktioniert ähnlich (siehe Abb. 3). Soll dieser Zugriff jedoch direkt per SQL
erfolgen, ist ein klein wenig mehr Aufwand zu betreiben, um die Daten in die Java Klasse zu
leiten. Da die ORACLE Collection nicht analog zum Auslesen mit einem SELECT-Statement
auch wieder gefüllt werden kann, wird zusätzlich eine ORACLE-View erzeugt. Diese kann
zwar auch keine Daten aufnehmen, dafür aber mithilfe eines sogenannten Instead-Of Triggers
die empfangenen Daten weiter- bzw. umleiten. Der Trigger füllt die ORACLE Collection und
gibt diese an die Java Klasse innerhalb der Datenbank weiter. An dieser Stelle sei lediglich
der schreibende Zugriff (INSERT) erläutert (Manipulationen per UPDATE oder DELETE sind
ähnlich zu realisieren):
public int writeToSQLServer(ARRAY in)
{
try
{ writeToDatabase(in); }
catch(SQLException e)
{ System.err.println(e.getMessage());
return 1;
}
return 0;
}
Die Methode erhält wieder einen Parameter vom ORACLE Datentyp ARRAY, der dieses
Mal jedoch die zu schreibenden Daten enthält.
import oracle.sql.STRUCT;
--->
private void writeToDatabase(ARRAY in) throws SQLException
{
// prepare statement for SQL server
PreparedStatement stmt = connSQL.prepareStatement(
"INSERT INTO tab1 (col1,col2,col3) VALUES (?,?,?)");
// loop SQL array and bind parameters to statement
Object[] objs = (Object[])in.getArray();
for(int i=0; i<objs.length; i++)
{
STRUCT strct = (STRUCT)objs[i];
for(int j=0; j< strct.getAttributes().length; j++)
stmt.setObject(j+1,strct.getAttributes()[j]);
stmt.executeUpdate();
}
stmt.close();
}
Auch hier kann für einen generischen Ansatz die Struktur der Zieltabelle aus den
Metadaten des übergebenen Parameters vom Typ ARRAY und somit dem ORACLE
Repository entnommen werden. Die Daten werden dem Parameter in in Form eines Arrays
entnommen und anschließend für jede Zeile an das JDBC-Statement gebunden und
ausgeführt.
Fazit
Die hier vorgestellte Methode zeichnet einen gangbaren Weg, wenn auf die Produkte der
Hersteller aus technischen oder sonstigen nachvollziehbaren Gründen nicht zurückgegriffen
werden kann. Die Erfahrungen in der Praxis zeigen eine gute Performance und einen
vertretbaren Konfigurationsaufwand bei der Einbindung von weiteren Tabellen. Um letzteren
zu minimieren, kann ein generischer Ansatz bei der Konzipierung und Realsierung der Java
Klasse gewählt werden. Die ORACLE-Datentypen sowie das Datenbank-Repository liefern
alle notwendigen Metainformationen.
Zur Person des Autors:
Andreas Reese ist studierter Diplom-Mathematiker und seit 1998 als IT-Consultant im
ORACLE- und JEE-Umfeld (Konzeption und Entwicklung von client-, server- oder
webbasierten JEE-Applikationen) tätig. Zunächst als angestellter Berater, machte er sich
2001 selbstständig und gründete eine IT-Firma, für die er bis Ende 2007 erfolgreich als
geschäftsführender Gesellschafter tätig war. Seitdem steht er seinen Kunden als
freiberuflicher IT-Berater zur Verfügung.
Kontakt Autor:
Andreas Reese
[email protected]
www.reecon-it.de
Herunterladen