In Java war die Objektpersistenz bisher nicht standardisiert und deshalb nur uneinheitlich zu realisieren. Java Data Objects (JDO) ist eine neue Spezifikation, um die persistente Speicherung von Java-Objekten unabhängig von der Datenbanksicht zumachen.
Als die Sprache Java spezifiziert wurde, wurde auch an die Persistenz von Objekten gedacht – leider aber nur in Form des reservierten Schlüsselworts “transient”. Die Ergänzung Java Data Objects soll die Lücke zwischen Vision und Realität schließen.
Java Data Objects oder JDO, wie man die Spezifikation mit einem der beliebten Three-Letter-Akronyme auch nennt, wird gegenwärtig in einem Java Community Process gemeinsam von Experten entwickelt, die bei verschiedenen Firmen beschäftigt sind (siehe[1]). Inzwischen steht die Spezifikation kurz vor der 1.0-Release, sollte also im Wesentlichen bereits stabil sein.
Bei JDO geht es darum, den Anwendungsentwicklern ein Implementations- unabhängiges Framework für die persistente Speicherung von Objekten an die Hand zu geben. Vorhandene Lösungen wie das im Linux-Magazin 7/01 vorgestellte Goods[2] oder die Speicherung mittels JDBC[3] sind zwar recht einfach, werden aber mit der Abhängigkeit von spezifischen Datenbankprodukten oder zumindest von speziellen Datenbankstrukturen erkauft.
Weil JDO ein Java-Standard ist, müssen sich die Hersteller von Datenbanken eine Schnittstelle gemäß der Spezifikation einfallen lassen, so dass es den Entwicklern ermöglicht wird, ohne Kenntnis der Datenbank ihre Objekte persistent zu machen.
Die JDO-Spezifikation ist insbesondere für Hersteller von Middleware interessant. Ein einheitliches Framework würde es einem Anwender erlauben, mit geringem Aufwand etwa einen EJB-Server für eine Datenbank zu konfigurieren. Der Vergleich von relationalen und objektorientierten Datenbanken kann in diesem Zusammenhang zu bemerkenswerten Ergebnissen führen, denn meist sind EJB-Server nur auf relationale Datenbankmanagement-Systeme ausgerichtet.
Der Manager im Zentrum
Dreh- und Angelpunkt der JDO-Architektur ist der »javax.jdo.PersistenceManager«. Das ist, wie bei den meisten Java-Paketen, ein Interface, das von einem Provider implementiert werden muss. Ein Datenbankhersteller, der sein Produkt für JDO verfügbar machen will, muss eine Datenbankklasse (für die Konfiguration der Datenquelle) sowie eine Implementation des Interfaces »javax.jdo.PersistenceManagerFactory« zur Verfügung stellen. Die Datenbankklasse könnte dieses Interface auch selbst implementieren.
Ist erst mal ein Persistence Manager erzeugt, wird nur über dessen Methoden auf die Datenbank zugegriffen. Die genaue Implementation – ob relational, objekt-relational oder objektorientiert – spielt keine Rolle mehr. In Listing 1, das weiter unten erläutert wird, ist ein kleines Beispiel zu sehen, wie damit Objekte gespeichert werden können.
Eintrittskarte zur Ewigkeit
Nicht jedes Objekt einer Anwendung soll gespeichert werden. Die Eintrittskarte zur Unvergänglichkeit ist die Implementation des zweiten wichtigen Interfaces der JDO-Spezifikation: »javax .jdo.PersistenceCapable«. So genannte JDO-Instanzen implementieren dieses Interface und repräsentieren Daten in einem Datenspeicher. Zudem implementieren sie natürlich auch die applikationsspezifische Logik. Ein sauberes Design wird hier aber versuchen, durch Vererbung oder Aggregation die Datenschicht zu kapseln.
Da das Persistence-Capable-Interface recht komplex ist, gibt es Enhancer, das sind Programme von JDO-Providern, die normale Anwendungsklassen so umwandeln, dass daraus Persistence-Capable-Klassen werden. Die Spezifikation sieht dabei vor, dass die erzeugten Klassen unabhängig vom JDO-Provider sind, also kompatibel zum JDO-Standard.
Der Weg zu einer persistenten Klasse ist also ein zweistufiger Prozess. Im ersten Schritt wird ganz normal eine Klasse implementiert, im zweiten Schritt wird diese umgewandelt. Diesen Ansatz verwendet auch das erwähnte Goods.
Da die Standardklassen von Java das Persistence-Capable-Interface nicht implementieren, sind diese auch nicht persistent. Benutzerdefinierte Klassen können es aber sein, wobei jedoch nur Felder gespeichert werden, die entweder selbst wieder das PC-Interface implementieren, primitive Typen sind oder Immutable Object Classes. Das sind Objekte, die sich nicht mehr verändern lassen, wenn sie einmal erzeugt sind. Von den wichtigen Klassen sind dies etwa String, Integer oder Float.
Zusätzlich zu diesen Klassen müssen auch Date und Hashset unterstützt werden. Optional sind die Klassen Arraylist, Hashmap, Hashtable, Linkedlist, Treemap, Treeset und Vector. Die Liste ist lang genug, um auch komplizierte Datenstrukturen abzubilden. Eine Konsequenz ist aber, dass Persistenz keine vererbare Eigenschaft ist. Subklassen von nicht-persistenten Klassen können also auch persistent sein, wenn deren Basisklassen es nicht sind, und umgekehrt.

Abbildung 1: Java-Entwickler können auf der Sun-Website ihre JDO-Kenntnisse in einem Quiz unter Beweis stellen.
Der Lebenszyklus eines Objekts
Ohne zu tief in die Details zu gehen, die in der Spezifikation gut dokumentiert sind, sei noch ein Aspekt von persistenten Objekten erwähnt: Für sie gibt es einen Lebenszyklus während des Ablaufs einer Anwendung. Definiert sind insgesamt zehn Zustände, etwa: »persistent-dirty« gibt an, dass das Objekt in der aktuellen Transaktion verändert wurde und nicht dem Datenspeicher-Zustand entspricht. Weitere Zustände sind »persistent-clean« oder »persistent-deleted«.
JDO-Instanzen können entweder persistent, also beständig, oder transient (flüchtig) sein. Ebenso können beide Typen bei Bedarf an Transaktionen gebunden werden. Für persistente Objekte sollte das außer in Embedded-Anwendungen der Normalfall sein. Transiente Objekte unter Transaktionskontrolle verwenden ist auch dann sinnvoll, wenn etwa ein solches Objekt den internen Zustand einer Anwendung wiederspiegelt und bei einem Rollback auch automatisch zurückgesetzt werden soll.
Was bin ich: Objekt-Identitäten
Ein beliebter Anfängerfehler in Java ist der Vergleich von zwei Objekten mit
if (obj1 == obj2) {
...
}
Hier vergleicht man die Objektidentität, stellt also fest, ob beide Variablen auf dasselbe Objekt verweisen. Meist ist aber der logische Vergleich
if (obj1.equals(obj2)) {
...
}
gemeint, bei dem man die Inhalte vergleicht. Objektidentität wird ausschließlich von der Virtual Machine (VM) gesteuert, während der Vergleich der Daten zweier Objekte eine Eigenschaft der Klasse ist. Die Methode »equals()« sollte deshalb bei allen datenzentrierten Klassen überschrieben werden.
Mit persistenten Objekten ist die Sache noch komplizierter, da eine Referenz nur innerhalb des Adressraums der virtuellen Maschine relevant ist, die Objekte aber auch in der Datenbank stecken. Wird also ein Objekt aus der Datenbank in die Anwendung geladen, existieren zwei Inkarnationen des Objekts. Die Datenbank hat ihre eigene Vorstellung von Identität, die mit der Referenz-orientierten Vorstellung der VM nichts zu tun hat. Trotzdem muss der Persistence Manager die Objekte synchronisieren.
JDO löst das Problem durch die Einführung einer JDO-Identität für die JDO-Instanzen. Hier gibt es drei Typen: die Key-basierte Identität, bei der Felder des Objekts wie ein Primary Key wirken. Wichtig ist, dass die »equals()«-Methode dann dieselbe Idee von Gleichheit hat wie die Datenbank. Neben dieser JDO-Identität, die eng mit der logischen, Applikations- bezogenen Identität verbunden ist, gibt es noch die Möglichkeit, die JDO-Identität durch den Datenspeicher oder die JDO-Implementation sicherzustellen.
Die Identität wird über eine Object-ID gesteuert. Dies ist eine Klasse, die von der Applikation bereitgestellt oder über ein JDO-Tool erzeugt wird. Falls nicht die Key-bezogene Identität verwendet wird, wird die Klasse der Object-ID durch die JDO-Implementation zur Verfügung gestellt.
Ein Beispiel sagt mehr als tausend Worte
Genug der Theorie! Für das folgende Beispiel muss die Referenz-Implementation von[4] heruntergeladen und installiert werden. Die Installation ist einfach und schnell erledigt, da es sich nur um eine Zip-Datei handelt, die zu entpacken ist. Sinnvoll ist es darüber hinaus noch, das Execute-Bit für die Verzeichnisse zu setzen:
# cd /usr/local
# md jdo
# cd jdo
# unzip ~/jdori-1_0-pre-ea-src-07_jun_2001.zip
# find . -type d -exec chmod +x {} ;
Die Referenz-Implementation enthält als Datenbank einen File Object Store. Außerdem ist die komplette Spezifikation und unter »src/test« zusätzlich der Test Enhancer dabei, mit dessen Hilfe sich eine normale Klasse in eine PC-Klasse umwandeln lässt. Für die Kompilation ist noch das Ant-Tool[5] von Apache notwendig.
Für die praktische Arbeit bietet es sich an, den Test Enhancer nach der erfolgreichen Kompilation in einem eigenen Jar-File zu verpacken:
# cd /usr/local/jdo/src/test # jar cvf ../../TestEnhancer.jar TestEnhancer*.class EnhancementNotNeededException.class
Keep it simple
Um die Sache einfach zu halten, wird als Datenobjekt die Klasse »Person« mit den drei Feldern Vorname, Nachname und Alter verwendet. In Listing 1 ist zu sehen, wie Objekte dieser Klasse geschrieben und gelesen werden. Dabei kommt nicht die Original-Klasse zum Einsatz, sondern »PCPerson«, die vom Test Enhancer erzeugt wird. Diese abgeleitete Klasse ist Persistent Capable, ansonsten verhält sie sich wie das Original. Eine Einschränkung des Test Enhancers ist allerdings, dass er nur »public«-Felder verarbeiten kann.
Der Aufbau des Programms ist simpel. Im Constructor (nicht gezeigt) wird ein neues »FOStorePMF«-Objekt (File Object Store) erzeugt. Diese Persistence Manager Factory wird in der »configure()«-Methode konfiguriert (der Ausschnitt zeigt den wichtigsten Schritt, nämlich die Angabe der URL). In den »writeData()«- und »readData()«-Methoden wird dann jeweils über diese Factory ein Persistence-Manager-Objekt erzeugt, das zum Lesen und Schreiben von Objekten verwendet wird.
Der komplette Quellcode, einschließlich Makefiles ist wieder über meine Homepage (siehe[6]) verfügbar. In der Referenz-Implementation gibt es auch verschiedene Testprogramme, die die Anwendung der verschiedenen Aspekte von Objektpersistenz zeigen (für meine Begriffe zwar etwas unübersichtlich, zum gegenwärtigen Zeitpunkt ist das aber verständlich).
Die vier Wege zum persistenten Objekt
Zum gespeicherten Objekt gelangt man auf vier Wegen. Der einfachste führt über Referenzen innerhalb der Objekte. Das erfolgt transparent für den Benutzer, setzt aber ein persistentes Objekt voraus. Ist es nicht vorhanden, muss man es über den Persistence Manager finden. Dafür gibt es drei Möglichkeiten, die kurz beschrieben werden.
Eine Möglichkeit ist die Verwendung der Methode »PersistenceManager.getObjectById()«, die aber voraussetzt, dass die Anwendung tatsächlich eine Object-Id konstruieren kann. Ein weiterer Weg führt über die so genannten Extents, das sind Mengen von Objekten einer Klasse, die über die Datenbank gemanagd werden. In diesem Fall heißt die Methode »PersistenceManager.getExtent()«, die alle Objekte einer spezifizierten Klasse zurückliefert. In Listing 1 wird in der »read-Data()«-Methode genau diese Option verwendet. Die letzte Möglichkeit zu den Objekten führt über Queries, die im Gegensatz zur Abfrage des kompletten Extents eine Filterung der zurückgelieferten Objekte erlauben.
JDO definiert eine eigene, Java-zentrierte Abfragesprache, die JDOQL. Diese Sprache muss vom Provider auf die Query-Sprache der Datenbank, also beispielsweise SQL, abgebildet werden. Für den Entwickler ist es völlig transparent, ob die Filterung von Objekten innerhalb der Datenbank geschieht oder erst im Persistence Manager (Ersteres ist natürlich wünschenswert).
Die Query-Sprache wird wieder über ein Query-Interface implementiert. Der Persistence Manager wirkt dabei als Factory für entsprechende Query-Objekte. Die müssen dann noch für die eigentliche Query konfiguriert werden: abgefragte Klassen, Ziel-Collection, Werte von Attributen, Sortierreihenfolge und so weiter. Über die »compile()«- und »execute()«-Methoden werden die Queries dann ausgeführt. Details sind wieder der Spezifikation zu entnehmen.
Noch eine Query-Sprache – ist das wirklich notwendig? Da es hier um einen streng Java-zentrierten Ansatz geht, ist genau das gerechtfertigt. Die Sprache fügt sich in das “Denken in Objekten”-Paradigma gut ein und ist somit für Java-Entwickler leicht zu lernen.
finally{}
JDO ist eine interessante Technologie, die momentan zwar noch ein paar Ecken und Kanten hat, sich aber sicherlich durchsetzen wird. Insbesondere wer keine großen Datenbankanwendungen schreibt, sondern nur einige Objekte auf einfache Weise speichern muss, findet hier eine sinnvolle Lösung.
In Zukunft sind sowohl von den Datenbankherstellern als auch von dritter Seite JDO-Implementationen zu erwarten. Gerade Lösungen, die auf SQL aufsetzen, sollten portabel für verschiedene Datenbanken konfigurierbar sein. Ein erstes Produkt in diese Richtung ist Kodo JDO, eine kommerzielle JDO-Implementation (Download über[7]).
Castor JDO von Exolab[8] ist ein weiteres Projekt mit dem Kürzel JDO im Namen, es bietet zwar ein objekt-relationales Mapping, verfolgt aber einen anderen Ansatz. Hier muss das Mapping explizit definiert werden, ein deutlich aufwändigerer, wenn auch eventuell performanterer Weg.
Mit diesem Coffee-Shop schließe ich die kleine Reihe über Datenbanken und Persistenz vorerst ab. Die nächsten Folgen widmen sich mehr den unterschätzten und teilweise vernachlässigten echten Java- (Client-)Programmen. (uwo)
Listing 1: JdoTest.java |
021: package test;
022:
023: import java.io.*;
024: import java.util.*;
025:
026: import javax.jdo.*;
027:
028: import com.sun.jdori.*;
029: import com.sun.jdori.common.*;
030: import com.sun.jdori.fostore.*;
031:
032: import de.bablokb.jdo.*;
033:
041: public class JdoTest {
042:
043: private static final String DBNAME = "TestDB";
044: private static final String DBNAME_EXT[] = {"btd", "btx"};
045: private static final String DBURL = "file://" +
046: new File(DBNAME).getAbsolutePath();
047: private FOStorePMF iPersManFactory;
048:
091: private void readData() throws Exception {
092: configure(false);
093: System.out.println("reading data");
094:
095: PersistenceManager pm = iPersManFactory.getPersistenceManager();
096: Transaction tx = pm.currentTransaction();
097:
098: tx.begin();
099: Extent ext = pm.getExtent (PCPerson.class,false); // no subclasses
100: Iterator it = ext.iterator();
101: while (it.hasNext()) {
102: PCPerson p = (PCPerson) it.next();
103: System.out.println(p.toString());
104: }
105: ext.close(it);
106: tx.commit();
107:
108: pm.close();
109: }
110:
117: private void writeData() throws Exception {
118: configure(true);
119: System.out.println("writing data");
120:
121: PersistenceManager pm = iPersManFactory.getPersistenceManager();
122: Transaction tx = pm.currentTransaction();
123:
124: tx.begin();
125: PCPerson pc = new PCPerson("Tux","Penguin",10);
126: pm.makePersistent(pc);
129: tx.commit();
130:
131: pm.close();
132: }
133:
140: private void configure(boolean createDB) throws Exception {
157: iPersManFactory.setConnectionURL(DBURL);
158: }
177: }
|
Der Autor |
|
Bernhard Bablok arbeitet bei der AGIS mbH (Allianz Gesellschaft für Informatik Service mbH) als Systemprogrammierer im Systems-Management-Bereich. Wenn er nicht Musik hört, mit dem Fahrrad oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Objektorientierung. Er ist unter [coffee-shop@bablokb.de] zu erreichen. |
Infos |
|
[1] JDO-Community-Page: [http://jcp.org/jsr/detail/012.jsp] [2] Coffee-Shop, “Russische Güter – Die Datenbank GOODS aus Russland”, Linux-Magazin 7/01, S. 118ff. [3] Coffee-Shop, “Artikel-Verwaltung mit JDBC und MySQL”, Linux-Magazin 5/99, S. 98ff. (auch online verfügbar) [4] Download der Referenz-Implementation: [http://access1.sun.com/jdo/] [5] Ant – ein plattformübergreifendes Make: [http://jakarta.apache.org/ant] [6] Download des kompletten Beispiels: [http://www.bablokb.de/jdo/] [7] Kodo JDO: [http://www.techtraders.com/products/jdo.html] [8] Castor JDO: [http://castor.exolab.org] |






