Seit der Einführung der objektorientierten Programmierung ist es oft der Wunsch eines Programmierers, die in einer Datenbank enthaltenen Datensätze auf ein Objekt abzupictureen. Optimal wäre dieses Problem gelöst, wenn die Umwandlung weitgehend automatisch geschehen würde und die Anwendung keinen großen Programmieraufwand erfordert.
Diejenigen unter Ihnen, die sich bereits mit dem objekt-relationalen Mapping beschäftigt haben, werden früher oder später auf Castor gestoßen sein. Neben dem reinen Mapping liefert Castor noch ein Reihe von zusätzlichen Enterprise-Features.
In diesem Beitrag soll es um das freie Tool Osage gehen, das JDBC-basiertes objekt-relationales Mapping bietet und auf zusätzliche Features weitgehend verzichtet. Osage ist vollkommen in Java implementiert und fügt sich somit optimal in die Konzepte der Java-Plattform ein.
Objekt-relationales Mapping
Zunächst wollen wir an dieser Stelle einmal klären, worum es beim objekt-relationalen Mapping überhaupt geht. Ausgangssituation ist zunächst eine Datenbank, die die benötigten Datensätze liefert. Aufgabe der objekt-relationalen Abpictureung ist es nun, den Datensatz auf ein Objekt abzupictureen. Hierzu liefert Ihnen Osage die benötigte Unterstützung. Sie brauchen sich deshalb nicht mit SQL-Anweisungen und der JDBC-Programmierung auseinander zu setzen. Dies hat den Vorteil, dass keinerlei SQL-Anfragen in Ihrem Programm fest codiert sind. Stattdessen werden die Anweisungen dynamisch zur Laufzeit generiert. Hierdurch lässt sich der Quellcode zudem wesentlich reduzieren. Da Osage den Datenbankzugriff vollkommen durch JDBC umsetzt, sind Programme, die auf Osage setzen, unabhängig von der eingesetzten Datenbank.XML-Unterstützung inbegriffen
Um die Fähigkeiten von Osage abzurunden ist natürlich vollständiger XML-Support in das Projekt integriert. Hierdurch erhalten Sie die Möglichkeit, Datensätze direkt nach XML zu überführen. Auch die andere Richtung ist selbstverständlich möglich, um aus einer XML-Quelle direkt in die Datenbank schreiben zu können.Doch bevor wir nun mitten in die Anwendung von Osage einsteigen wollen, muss das Paket natürlich erst einmal installiert sein. Alle Information zur Installation finden Sie im folgenden Abschnitt.
Installation
Zu finden ist das Programmpaket auf http://osage.sourceforge.net. Der Download erfolgt als gepacktes Zip-Archiv. Entpacken Sie die Archivdatei in ein Verzeichnis Ihrer Wahl und wechseln Sie anschließend in das neu erstellte Unterverzeichnis osage-1.0pre10. Osage benötigt eine Reihe von Jar-Archiven, die jedoch alle im Verzeichnis lib mitgeliefert werden, sodass keine weiteren Installationen nötig sind. Der Übersetzungsvorgang erfolgt per Ant-Buildfile build.xml. Aufzurufen ist Ant über das Shellskript build.sh bzw. build.bat unter Windows. Da sich die Datei ant.jar bereits im lib-Verzeichnis befindet, braucht auch Ant nicht gesondert installiert werden. Nach dem Aufruf des entsprechenden build-Skriptes sollte sich die Datei osage-1.0pre10.jar im aktuellen Verzeichnis befinden. Die Installation ist somit abgeschlossen. Bevor wir den ersten Einsatz von Osage weiter vorbereiten, ist es zunächst einmal erforderlich, eine Datenbank zur Verfügung zu stellen. Für die Beispiele in diesem Beitrag verwenden wir ein Postgres-System unter Linux. Die Installation ist nachfolgend kurz beschrieben.PostgreSQL einrichten.
Die Installation eines Postgres-Systems stellt sicherlich kein großes Problem dar und dürfte vielen Lesern bereits bekannt sein. Dennoch folgt zunächst eine kurze Anleitung. Die aktuelle Version von Postgres beziehen Sie bitte von einem Mirror der Seite <font>http://www.postgresql.org.</font> Sie haben die Wahl zwischen binären RPM-Paketen und dem gepackten Sourcecode im *.tar.gz-Format.Das Installieren der RPM-Versionen dürfte keine Probleme bereiten, entscheiden Sie sich für das Übersetzen der Quellen, ist die Installation auch in wenigen Schritten erledigt.
Wechseln Sie hiezu in das Verzeichnis, in dem der Sourcecode entpackt worden ist und starten Sie den Konfigurationsvorgang mit dem Befehl:
./configure
gmakegmake install
adduser postgres
Nun muss noch das Datenverzeichnis der Datenbank angelegt werden. Gewöhnlich wird das Verzeichnis bei einer Standardinstallation unter | usr | local | pgsql | data erstellt. Dieser Schritt muss zunächst als User root erfolgen:
mkdir /usr/local/pgsql/data
chown postgres /usr/local/pgsql/datasu - postgres
/usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data/usr/local/pgsql/bin/postmaster -D /usr/local/pgsql/data &
/usr/local/pgsql/bin/createdb test
/usr/local/bin/psql test
Konfiguration der Datenbankverbindung
Mit einem lauffähigen Datenbanksystem ist die Arbeit natürlich noch nicht ganz abgeschlossen, schließlich müssen noch Tabellen angelegt und gefüllt, sowie die Verbindung zur Datenbank unter Osage konfiguriert werden. Um die Schritte an einem Beispiel erläutern zu können, verwenden wir an dieser Stelle die beigefügten Beispieldateien unter dem Verzeichnis examples. Zunächst ist die Datei create.sql interessant.Listing 1
DROP TABLE prodCREATE TABLE prod (id integer,name varchar(200),price numeric(18,2))DROP TABLE category_prodCREATE TABLE category_prod (prod_id integer,category_id integer)DROP TABLE prod_detailCREATE TABLE prod_detail (id integer,prod_id integer,name varchar(200))DROP TABLE prod_groupCREATE TABLE prod_group (id integer,name varchar(200),prod_id integer)DROP TABLE categoryCREATE TABLE category (id integer,description varchar(200))
Verwenden Sie Postgres, haben Sie die Wahl der manuellen Eingabe der SQL-Befehle über den Client psql oder das Übergeben der Datei create.sql. Innerhalb psql geben Sie dazu folgendes Kommando ein:
\i [Pfad zur Datei]/create.sql
<?xml version="1.0"?><db><database name="test" vendor="postgres"><driver class="org.postgresql.Driver" url="jdbc:postgresql://localhost/test"/><param name="user" value="postgres"/><param name="password" value="postgres"/></database></db>
Außer dem Attribut class erfolgt noch die Übergabe des Verbindungsstrings durch das Attribut url. Im Beispiel konfigurieren wir ein Postgres-System, das lokal läuft und über eine Datenbank test verfügt. Durch die folgenden param-Tags werden die beiden Parameter user und password gesetzt. Sollte die Datenbankinstanz unter einem anderen Benutzer als postgres laufen, nehmen Sie an dieser Stelle bitte die erforderlichen Anpassungen vor. Ausserdem muss natürlich das von Ihnen gewählte Passwort eingesetzt werden. Benutzername und Passwort werden jeweils durch das Attribut value übergeben.
Nachdem die Datenbankverbindung steht, müssen noch eine Reihe weiterer Einstellungen getroffen werden. Hierzu gibt es eine zentrale Konfigurationsdatei. Eine Beispielkonfiguration ist unter examples/conf.xml zu finden.
Listing 2
<?xml version="1.0"?><osage><database><!-- omit this for no debugging --><logfilename>sql.log</logfilename></database><dbmap><schema>file:database.qjml</schema><input>file:test.xml</input></dbmap><classmap><schema>file:maps.qjml</schema><input>file:prodcomp.xml</input><input>file:product.xml</input></classmap><builder><sqlinterface>generic</sqlinterface><database>test</database><output>newmap.xml</output><dest>..</dest></builder></osage>
Wie Sie erkennen können, taucht auch die Datei test.xml wieder auf, die innerhalb des Elementes dbmap zur Konfiguration der Datenbankverbindung angegeben wird. Zusätzlich tauchen zwei weitere wichtige XML-Konfigurationsdateien auf: prodcomp.xml und product.xml. Hier wird zentral das Mapping zwischen Datenbanktablen und Java-Klassen konfiguriert. Im folgenden Abschnitt werden wir die Konfiguration des Mappings anhand dieser Beispieldateien nachvollziehen.
Mapping
Das Mapping, zu deutsch Abpictureung, beschreibt die Verbindung von Datenbanktablen und Java-Klassen. Die Konfiguration erfolgt komfortabel über eine XML-Datei. Anhand der Beispieldateien prodcomp.xml und product.xml lassen sich die Einstellungen gut nachvollziehen. Im folgenden Listing sehen Sie einen kurzen Auszug zur Verdeutlichung:<?xml version="1.0"?><maps><class name="yourapp.comp.Product"><map-to database="test" xml="product" table="prod"/><key-generator type="max" field="id"/><field name="id" type="integer"><sql primary-key="True" /></field>...</class></maps>
createdb [Datenbankname]
| SQL | Java Types |
| bit | java.lang.Boolean |
| tinyint | java.lang.Byte |
| smallint | java.lang.Short |
| integer | java.lang.Integer |
| bigint | java.lang.Long |
| float, double | java.lang.Double |
| real | java.lang.Float |
| numeric, decimal | java.math.BigDecimal |
| char , varchar, longvarchar | java.lang.String |
| date | java.sql.Date |
| time | java.sql.Time |
| timestamp | java.sql.Timestamp |
| binary, varbinary, longvarbinary | byte[] |
Für die Validierung der XML-Mapping-Datei wird Quick und die darin enthaltene Schemasprache QJML eingesetzt. Da eine kurze Einführung in diese Schemasprache bereits im Java Magazin 10.2001 auf Seite 80 zu finden ist, gehen wir an dieser Stelle nicht gesondert auf diese Technik ein. Die Datei maps.qjml beinhaltet die entsprechenden Validierungsregeln.
Java-Klassen
Wir haben bisher zwei wesentliche Bestandteile der Dreierkette Datenbank-Mapping-Java gesehen. Zum Vervollständigen der Betrachtung gehen wir nun in diesem Abschnitt auf die entsprechende Java-Implementierung ein. Wie Sie bereits gesehen haben, ordnet die Mapping-Datei den verschiedenen Feldern einer Datenbanktable entsprechende Eigenschaften einer Java-Klasse zu. Die Implementierung dieser Java-Klasse erfolgt natürlich nach einem vorgegebenen, aber relativ einfachen Schema.Listing 3
package yourapp.db;import java.math.BigDecimal;import java.sql.Time;import java.sql.Timestamp;import java.util.ArrayList;import java.util.Collection;import java.util.Date;public class Product {public static String FOR_NAME = "yourapp.db.Product";public static String ID = "id";public static String GROUP_ID = "groupId";public static String NAME = "name";public static String PRICE = "price";private int id;private int groupId;private String name;private double price;public int getGroupId(){return groupId;}public int getId(){return id;}public String getName(){return name;}public double getPrice(){return price;}public void setGroupId(int groupId){this.groupId = groupId;}public void setId(int id){this.id = id;}public void setName(String name){this.name = name;}public void setPrice(double price){this.price = price;}}
public static String ID = "id";public static String GROUP_ID = "groupId";public static String NAME = "name";public static String PRICE = "price";
| Datenbanktabelle(Felder) | Java(Variablen) |
| id | id |
| name | name |
| group_id | groupId |
| price | price |
Anschließend werden die zugehörigen Variablen deklariert.
private int id;private int groupId;private String name;private double price;
Den Großteil der Klasse product machen schließlich die zugehörigen get- und set-Methoden aus. Zu jeder Klassenvariable muss jeweils eine get- und eine set-Methode vorhanden sein. Der Name der Methode setzt sich hierbei immer aus dem Teilstring get bzw. set und dem Variablennamen zusammen. Der Variablenname wird grundsätzlich durch einen Großbuchstaben eingeleitet.
Um das Beispiel zu vervollständigen sehen Sie nun noch einmal die entsprechende SQL-Tabellenstruktur abgepictureet:
create table prod {id int not null,name varchar(200) not null,price numeric(18,2) not null,group_id int not null,};
Automatisierung
Die drei Schritte, die nötig sind um das Mapping vorzubereiten haben Sie jetzt anhand eines Beispiels nachvollziehen können. Nun stellt sich natürlich die Frage, ob es nötig ist, jedes mal die Schritte von Hand auszuführen. Eine Datenbanktable anzulegen, die benötigte Mapping-Datei zu schreiben und anschließend die Klasse in Java zu implementieren, kann auf die Dauer sehr zeitintensiv werden. Besser wäre es, wenn bestimmte Schritte, die immer nach dem selben Schema durchgeführt werden, automatisiert werden könnten. Osage bietet Ihnen hierzu einige Hilfen an, die wir uns im Folgenden ansehen wollen. Die Klasse net.sourceforge.osage.util.builder.Main ist hierbei der zentrale Bestandteil. Mit Hilfe dieser Klasse erhalten Sie die folgenden Möglichkeiten:- Aus einer vorhandenen Mapping-Datei die entsprechende Java-Klasse generieren.
- Aus einer vorhandenen Datenbank eine Mapping-Datei erstellen.
- Aus vorhanden Mapping-Dateien eine SQL-Datei erstellen, die SQL-Befehle enthält, um die entsprechende Tabelle in der Datenbank anzulegen.
- "net.sourceforge.osage.util.builder.ClassBuilder" - Erstellt aus einer Mapping-Datei eine Java-Klasse
- "net.sourceforge.osage.util.builder.MapBuilder" - Erstellt aus einer Datenbanktabelle eine Mapping-Datei
- "net.sourceforge.osage.util.builder.SQLBuilder" - Erstellt aus einer Mapping-Datei die benötigten SQL-Anweisungen zum Generieren der entsprechenden Tabelle.
java net.sourceforge.osage.util.builder.Main net.sourceforge.osage.util.builder.ClassBuilder conf.xml
Im Einsatz
Bis zu dieser Stelle haben wir nun alle Grundlagen betrachtet, die für die Anwendung des Mappings nötig sind. Sie haben die Tabellen der Datenbank initialisiert, eine Mapping-Datei erstellt und schließlich die Java-Mapping-Klasse implementiert. Außerdem haben Sie im letzten Abschnitt gesehen, dass wesentliche Schritte aus diesem Prozess automatisiert werden können und Ihnen als Programmierer somit eine Menge zusätzlicher Arbeit erspart wird. Im Folgenden besprechen wir jetzt die konkrete Anwendung des Mappings in eigenen Klassen. Ziel ist es hierbei, Daten aus der Tabelle der Datenbank durch Zugriff auf die entsprechenden Eigenschaften der Java-Mapping-Klasse zu erhalten. Somit können alle SQL- bzw. JDBC-spezifischen Codeblöcke aus Ihrem Projekt entfernt werden. Der Datenaustausch erfolgt ausschließlich über ein Objekt.Als Beispiel verwenden wir weiterhin das Mapping der Tabelle product. Zunächst müssen wir eine Verbindung zu der verwendeten Datenbank herstellen. Da alle Verbindungseinstellungen bereits in der Konfigurationsdatei hinterlegt wurden, müssen keine Angaben fest im Quelltext codiert werden. Die Datenbank ist somit beliebig austauschbar.
Product product = null;Database db = persistanceManager.getDatabase();db.begin();
Um bestimmte Datensätze einer Tabelle abzufragen, kommt nun die Klasse RetrieveCritaria (Package: net.sourceforge.osage.api) zum Einsatz. Übergeben wird dem Konstruktor der Klasse das Kriterium, nach dem gefiltert werden soll.
RetrieveCritaria rc = new RetrieveCritaria(Product.FOR_NAME);
Iterator i = rc.perform(db);
while (i.hasNext()){product = (Product) i.next();}
db.commit();db.close();
RetrieveCritaria rc = new RetrieveCritaria(Product.FOR_NAME);rc.addSelectEqualTo(Product.PRICE, 10.0);Iterator i = rc.perfom(db);...
| Filterfunktion | Beschreibung |
| addSelectEqualTo(String attrName, Object value) addSelectEqualTo(String attrName, int value) addSelectEqualTo(String attrName, double value) addSelectEqualTo(String attrName, boolean value) | Funktion vergleicht den angegebenen Wert auf Gleichheit. |
| addSelectLessThan(String attrName, Object value) addSelectLessThan(String attrName, int value) addSelectLessThan(String attrName, long value) addSelectLessThan(String attrName, double value) | Der Wert des Attributes muss unter dem angegebenen Vergleichswert liegen. |
| addSelectLessOrEqual(String attrName,Object value) addSelectLessOrEqual(String attrName,int value) addSelectLessOrEqual(String attrName,long value) addSelectLessOrEqual(String attrName,double value) | Der Wert des angegebenen Attributes muss kleiner oder gleich dem Vergleichswert sein. |
| addSelectGreaterThan(String attrName, Object value) addSelectGreaterThan(String attrName, int value) addSelectGreaterThan(String attrName, long value) addSelectGreaterThan(String attrName, double value) | Der Wert des angegebenen Attributes muss größer als der Vergleichswert sein. |
| addSelectGreaterOrEqual(String attrName, Object value) addSelectGreaterOrEqual(String attrName, int value) addSelectGreaterOrEqual(String attrName, long value) addSelectGreaterOrEqual(String attrName, double value) | Der Wert des angegebenen Attributes muss größer oder gleich dem Vergleichswert sein. |
| addSelectNotEqual(String attrName, Object value) addSelectNotEqual(String attrName, int value) addSelectNotEqual(String attrName, long value) addSelectNotEqual(String attrName, double value) | Der Wert des angegebenen Attributes darf nicht mit dem Vergleichswert übereinstimmen. |
| addSelectLike(String attrName, String value) | Vergleich zwischen zwei Strings |
Es können natürlich mehrere Filter zu einem RetrieveCritaria-Objekt hinzugefügt werden, sodass Sie hiermit beliebige Filterungen durchführen können. Durch diese Vorgehensweise ersparen Sie sich das Erstellen von SQL-Abfragen.
Updateten und Löschen
Neben der Klasse RetrieveCriteria existieren außerdem noch die beiden Klassen UpdateCriteria und DeleteCriteria. Wie der Name der Klassen bereits vermuten lässt, wird UpdateCriteria eingesetzt, um Daten in der Datenbank zu aktualisieren und DeleteCriteria, um Daten zu löschen. Die Anwendung der Klassen geschieht analog zu RetrieveCriteria, es können folglich alle Filterfunktionen benutzt werden.PersistanceManager
Kommen wir zum Schluss noch einmal auf die Klasse PersistanceManager zurück. In unseren bisherigen Beispielen sind wir davon ausgegangen, dass bereits ein Objekt peristanceManager existiert. Die Klasse PersistanceManager ist unter anderem dafür verantwortlich, dass die Konfigurationsdateien geladen werden können und somit eine Verbindung zur Datenbank hergestellt werden kann. Hier ist der Code zum initialisieren eines PersistanceManager-Objektes:PersistanceManager persistanceManager = PersistanceManagerFactory.create();Configuration conf = ConfigurationBuilder.build("conf.xml");persistanceManager.setConfiguration(conf);....peristanceManager.destroy();














