Aus Linux-Magazin 07/2001

Coffee-Shop

Objektorientierte Datenbanken sind immer noch Ausnahmen von der relationalen Regel. Sie versprechen aber eine bessere Integration in Programme, die mit einer objektorientierten Sprache erstellt wurden. Die Datenbank Goods aus Russland – ausgestattet mit einer BSD-ähnlichen Lizenz – misst sich in diesem Coffee-Shop an diesem Anspruch.

Einen Überblick über verschiedene Datenbanksysteme unter Linux gab es in der April-Ausgabe des Linux-Magazins. Auf fast alle dieser Datenbanken ist der Zugriff über JDBC möglich, das Java-Standard-API für den DB-Zugriff. Das API ist einfach und lässt sich weitgehend sogar datenbankunabhängig verwenden – trotzdem ist es ein Strukturbruch. Die Beziehungen zwischen Objekten lassen sich nicht immer in relationale Tabellen zwingen. Und das Schreiben und Lesen von Daten oder Objekten ist stets explizit zu implementieren. Damit werden die Applikations- und die Persistenzschicht innerhalb des Codes gemischt.

Ist eine Datenbank schon vorhanden, die eventuell sogar schon von alten Anwendungen genutzt wird, gibt es zu JDBC im Grunde keine Alternative. Bei neuen Anwendungen allerdings ist der Verzicht auf eine klassische Datenbank immer eine Überlegung wert. Das gilt umso mehr, je stärker die zu speichernden Objekte miteinander durch Referenzen verknüpft sind. Denn gerade jene Objekte, die nicht Value-Objects sind, also nur einfache Attribute besitzen, lassen sich schwer in relationale Datenbanken abspeichern. Als Beispiel sei ein Stammbaum genannt: Hier gibt es eine Fülle vielfältiger Objektbeziehungen.

Um aber nicht zu sehr in die Theorie abzugleiten, geht es gleich ins Eingemachte: Die Datenbank Goods wird zunächst heruntergeladen, kompiliert und installiert.

Download und Installation

Die Quelle von Goods (Generic Object Oriented Database System) ist [1]. Der Download des 604 KByte großen Pakets geht flott vonstatten. Zusätzlich sind zwei nützliche Postscript-Dateien verfügbar: Eine ausführliche Readme-Datei (31 Seiten) und eine Beschreibung des Zugriffs über Java (13 Seiten). Wer also längere Dokumentationen lieber in Papierform liest, ist hier sehr gut bedient. Allerdings scheinen mir die in der Distribution vorhandenen HTML-Versionen etwas aktueller zu sein. Nach dem Entpacken wird zuerst das Skript Config aufgerufen, das im Wesentlichen ein paar Makefile-Templates umkopiert. Danach langt die schlichte Anweisung

> make -k all

um die Client- und Serverbibliotheken, die administrativen Tools, die Beispiele und die Java-Bibliotheken zu erzeugen. Die Option -k war in der eingesetzten Version 2.37 notwendig, weil das Linken eines Beispiels nicht funktioniert hat. Das beeinträchtigt aber nicht die Funktionsweise des Serverprogramms oder der Java-Schnittstelle.

Nach dem Kompilieren befinden sich im lib-Verzeichnis die Bibliotheken libclient.a, libserver.a sowie die beiden Jar-Archive goodsjpi.jar und goodslib.jar. Im bin-Verzeichnis sind dagegen die Tools und der eigentliche Server goodsrv gelandet.

Güter-Konzepte

Goods ist grundsätzlich sprachneutral, aktuell stehen C++- und Java-Schnittstellen bereit. Das wird möglich, weil die gesamte Applikationslogik beim Client liegt, während der Server für das Schreiben und Lesen von Objekten, Transaktionen, Objekt-Locks, Backups, Garbage Collection und so weiter verantwortlich ist.

Die Kommunikation zwischen Client und Server verwendet ein Request/ Response-Messagemodell auf normalen Netzwerkverbindungen. Das Protokoll ist ausführlich dokumentiert und kommt zurzeit mit 14 Requesttypen und zehn Reply-Typen aus. Deren Details sind aber für einen normalen Benutzer irrelevant, eher für Programmierer, die beispielsweise eine Schnittstelle zu Python implementieren wollen.

Goods-Datenbanken bestehen aus einem oder mehreren Storages, die über das gesamte Netzwerk verteilt sein können. Jedes Storage wird von einem Storage Server ( goodsrv) verwaltet. Ein Objekt wird jeweils nur in einem storage gespeichert. Ist es einmal dort gebunden, kann es nicht mehr verschoben werden. Mehrere Server dienen also nicht der Replikation, sondern der Partitionierung.

Normalerweise muss sich ein Client nicht darum kümmern, wo das Objekt gespeichert wird, es ist aber möglich, dies Client-seitig zu definieren. Die Anzahl der Server pro Datenbank sollte nicht zu groß sein, da sonst ein zu hoher Synchronisationsaufwand notwendig ist, etwa bei serverübergreifenden Transaktionen.

Die Architektur des Servers ist modular. So gibt es einen Storage Memory Manager, einen Transaction Manager, einen Page Pool Manager, einen Object Access Manager und einen Class Information Manager. Durch die modulare Struktur sind diese Komponenten austauschbar, es lassen sich also auch mehrere Implementationsstragegien untersuchen.

Das Metaobject Protocol

Ein Herausstellungsmerkmal objektorientierter Datenbanken ist der transparente Umgang mit persistenten Objekten. Der Umgang mit Objekten, die gespeichert werden, ist identisch zu dem von transienten Objekten, insbesondere also davon, wie die Objekte erzeugt wurden. Man spricht dann von orthogonaler Persistenz. Das erreicht man durch das expliziete Speichern so genannter Wurzelobjekte. Alle von diesem Objekt referenzierte Objekte werden dann ebenfalls gespeichert.

In Goods wird allerdings aus Performancegründen von der strengen Form der othogonalen Persistenz abgewichen. Nur Objekte, die von speziellen Basisklassen abstammen, können gespeichert werden (da Java keine Mehrfachvererbung erlaubt, ist das eine ernst zu nehmende Einschränkung). Die Basisklassen müssen ein Objekt mit dem Namen metaobject einer Subklasse der Klasse goodsjpi.Metaobject enthalten. Für Goods-Anwendungen wird dafür die Klasse goodsjpi.Persistent zur Verfügung gestellt.

Implementiert wird die Persistenz dadurch, dass der kompilierte Bytecode nachbearbeitet und jede Methode, die Objekte verändert, mit entsprechenden Datenbankaufrufen ergänzt wird. Diese Veränderung erfolgt automatisch durch das Tool Javamop. Das Tool macht dabei eine Reihe von Annahmen. In einigen wenigen Fällen, zum Beispiel bei Arrays, ist eine Modifikation von Objekten jedoch nicht automatisch feststellbar und muss deshalb explizit durch einen Aufruf von metaobject.modify() signalisiert werden.

Persistente Klassen sollten neben den primitiven Java-Typen nur Referenzen auf ebenfalls persistente Klassen enthalten. Das schließt Java-Arrays aus. Genau genommen ist ein Objekt variabler Länge erlaubt, es kann ein Array oder ein String sein. Bei Java-Strings ist zu beachten, dass dadurch die gespeicherten Objekte nicht mehr von C++ aus ansprechbar sind. Wer also seine Goods-Datenbank auch von C++-Clients aus ansprechen will, sollte auf später beschriebenen Alternativen ausweichen.

Programmieren für Goods

Nach so viel Theorie nun auch ein praktisches Beispiel, das die Konzepte etwas verständlicher macht. In Listing 1 sind Ausschnitte eines einfachen Programms zu sehen, das verallgemeinerte binäre Bäume erzeugt und das Ergebnis in einer Goods-Datenbank speichert.

Das Beispiel diente schon dazu, die objektorientierte Datenbank Poet, siehe [2] und [3], zu testen. Die Klasse Node wird von goodsjpi.Persistent abgeleitet und stellt einen (persistenten) Knoten im Baum dar. Als so genanntes Root-Objekt dient ein Objekt der Klasse Woods, das einfach ein Array der Toplevel Nodes enthält. Dies Objekt ist lediglich deshalb notwendig, weil unter Goods alle Objekte innerhalb eines Storage nur über ein einziges Wurzelobjekt ansprechbar sind. Ist die Ableitung aller persistenten Objekte von einer Basisklasse Persistent noch vertretbar, ist diese Einschränkung sehr hinderlich. Doch warum soll es in einer Datenbank nicht viele völlig unabhängige Objekte geben?

Die sicherlich interessantesten Zeilen sind in der Methode writeDB() nachzulesen (siehe Listing 1, Zeilen 104 bis 115). Hier wird zunächst die Datenbank geöffnet, dann das Wood-Objekt mit den erzeugten Nodes als Wurzelobjekt definiert und die Datenbank wieder geschlossen. Alle Objekte, die über Referenzen vom Wood-Objekt ebenfalls erreichbar sind, werden ebenfalls gespeichert.

In diesen wenigen Zeilen zeigen sich die Vorzüge und Möglichkeiten der objektorientierten Datenbanken: Es ist kein kompliziertes Zusammenbauen von Insert- und Update-Statements mehr notwendig und Integritätsprobleme, wenn etwa Objekte auf mehrere Tabellen verteilt sind, treten auch nicht auf.

Ein Objekt der Klasse Node verwendet ein goodslib.ArrayOfObject-Objekt für seine Blätter (siehe Listing 1, Zeile 20). Goods stellt diese Klasse (wie auch noch weitere Klassen) zur Verfügung, um Collections speichern zu können. Auch eine Alternative zu Strings ist vorhanden, wodurch sich Kompatibilitätsprobleme mit C++ umgehen lassen.

Die Datenbank wird geöffnet über eine Konfigurationsdatei (Zeile 108). Diese Datei ist sehr einfach aufgebaut (siehe Listing 2) und enthält die Anzahl der Storages (erste Zeile) und die Angabe, wo sie sich im Netzwerk befinden (Zeilen 2-n). Die gleiche Datei (aber ohne die Extension .cfg) muss den Serverprogrammen beim Start übergeben werden.

Listing 1: Node.java

001: import java.io.*;
002: import goodsjpi.*;
003: import goodslib.*;
004:
012: public class Node extends Persistent {
013:
014:   private static final transient String DB_CFG   = "node.cfg";
015:   private        transient int    iNodes;
016:   private static transient int    iGlobalNr = 0;
017:
018:   private int           iDepth, iLeafNr, iNumber;
019:   private String        iRootName;
020:   private ArrayOfObject iLeafs;
021:
055:   public void createTree() {
056:     if (iDepth == 0)
057:       return;
058:     else {
059:       iLeafs = new ArrayOfObject(iNodes);
060:       for (int i=0;i<iNodes;++i) {
061:         Node n = new Node(iDepth-1,iNodes,i,iRootName);
062:         iLeafs.putAt(i,n);
063:         n.createTree();
064:       }
065:     }
066:   }
067:

104:   public static void writeDB(Wood w) {
105:     Database db = new Database();
106:     long start, end;
107:     System.out.println("Opening database");
108:     db.open(DB_CFG);
109:     System.out.print("Saving nodes in db: ");
110:     start = System.currentTimeMillis();
111:     db.setRoot(w);
112:     end = System.currentTimeMillis();
113:     System.out.println(Long.toString(end-start) + " msec");
114:     db.close();
115:   }
116:
120:   public static void main(String[] args) {
121:     if (args.length<3) {
122:      System.out.println("usage: java Node depth nodes/leaf n_trees");
123:      System.exit(1);
124:     }
125:
126:     long start, stop;
127:
128:     int    depth        = Integer.parseInt(args[0]);
129:     int    nodesPerLeaf = Integer.parseInt(args[1]);
130:     int    nTrees       = Integer.parseInt(args[2]);
131:
132:     System.out.println("Creating "+nTrees+" trees with depth "+depth+" and "+
133:       nodesPerLeaf+" nodes per leaf");
134:     start = System.currentTimeMillis();
138:     Wood w = new Wood(nTrees);
139:     w.create(depth,nodesPerLeaf);
140:     System.out.println("Number of nodes: " + count());
141:     writeDB(w);
142:     stop = System.currentTimeMillis();
143:     System.out.println("Total time: " + (stop-start) + " msec");
144:   }
145: }
146:
147: class Wood extends Persistent {
148:   private Node[] iTop;
149:
150:   public Wood(int nTrees) {
151:     iTop = new Node[nTrees];
152:   }
153:
160:   public void create(int depth, int nodesPerLeaf) {
161:     long start, end;
162:     System.out.print("Creating trees: ");
163:     start = System.currentTimeMillis();
164:     for (int i=0;i<iTop.length;++i) {
165:       iTop[i] = new Node(depth,nodesPerLeaf,0,"Tree " + Integer.toString(i));
166:       iTop[i].createTree();
167:     }
168:     end = System.currentTimeMillis();
169:     System.out.println(Long.toString(end-start) + " msec");
170:   }
171: }

Listing 2: Node.cfg

1: 2
2: 0: sirius:6100
3: 1: canopus:6100

Objektsuche

Eine Datenbank dient nicht nur der Speicherung, sondern auch dem Lesen von Daten. Objekte werden innerhalb von Goods entweder über Referenzen – ausgehend vom Wurzelobjekt – gefunden oder über Queries. Dafür gibt es in Goods die Klasse goodslib.Query. Über eine an SQL angelehnte Syntax lassen sich damit Objekte herausfiltern. Das Konzept funktioniert zwar, hat aber zwei entscheidende Nachteile. Erstens ist es nicht kompatibel zur Object Query Language (OQL), einer ebenfalls an SQL angelehnten Spezifikation der Object Data Management Group (ODMG; siehe [4]). Andererseits – und entscheidender – erfolgt die Filterung erst lokal auf dem Client.

Das ist eine Konsequenz aus der Architektur von Goods. Wie oben beschrieben liegt die gesammte Applikationslogik auf dem Client. Damit ist zwar der Server relativ einfach sprachneutral zu implementieren, jedoch sind alle Daten erst über das Netzwerk an den Client zu schicken, bevor die Query-Engine sie filtert. Bei größeren Datenmengen verspricht das keine akzeptable Netzwerk- und Anwendungsperformance.

Kompilieren und Starten

Das Programm wird normal kompiliert, anschließend werden die Klassen durch das oben erwähnte Javamop-Tool umgewandelt. Danach ist alles bereit, um die Anwendung zu starten. Die nötigen Schritte sind aus Listing 3, dem Ausschnitt aus einem Makefile, ersichtlich.

Listing 3: Makefile

62: GOODS_HOME := /home/Bablokb/src/goods
63:
64: Node.class: Node.java
65:    javac -classpath 
66:      $(GOODS_HOME)/lib/goodsjpi.jar:$(GOODS_HOME)/lib/goodslib.jar 
67:         -d . Node.java
68:    $(GOODS_HOME)/bin/javamop -package goodsjpi *.class 
69:        $(GOODS_HOME)/lib/goodsjpi.jar $(GOODS_HOME)/lib/goodslib.jar
70:
71: run: Node.class
72:    -$(GOODS_HOME)/bin/goodsrv node &
73:    $(JRE) -cp 
74:      $(GOODS_HOME)/lib/goodsjpi.jar:$(GOODS_HOME)/lib/goodslib.jar:. 
75:        Node 5 2 5
76:
77: dbclean:
78:    -killall goodsrv
79:    -rm *.class node.his node.idx node.log node.map node.odb

Der Betrieb

Eine Datenbank muss nicht nur den Anforderungen bei der Anwendungsentwicklung entsprechen, sondern sich auch in den Betrieb reibungslos einbinden lassen. Bei Goods sieht die Sache nicht optimal aus. Der Server liest und schreibt standardmäßig von der Konsole – für den normalen Betrieb ist das nicht tragbar. Die Konfigurationsdatei goodsrv.cfg, die beim Start ausgewertet wird, erlaubt aber die Angabe eines Ports für den Tel net-Zugriff:

server.admin_telnet_port="localhost:8023"

Damit kann der Server im Hintergrund gestartet werden. Eine Telnet-Session zum angegebenen Port ermöglicht dann die Durchführung einfacher Sysadmin-Aufgaben (siehe Listing 4). Über die Telnet-Session lässt sich der Server sauber stoppen. Automatisch über ein Skript ist das leider nicht möglich (zumindest schweigt sich die Dokumentation darüber aus).

Die Datenbanken selbst sind Dateien, die denselben Basename wie die Konfigurationsdatei haben, aber andere Extensions. Heißt die Konfigurationsdatei node.cfg, werden die Dateien node.his, node.idx, node.log, node .map und node.odb erzeugt. Neben dem Online-Backup im laufenden Betrieb über den Telnet-Port können diese Dateien selbstverständlich auch nach dem Herunterfahren des Servers normal gesichert werden.

Listing 4: Management des Servers

> telnet localhost 8023
>help
Commands:
help [COMMAND] print information about command(s)
open open database
close close database
logout terminate administrative session
exit terminate server
show [CATEGORIES] show current server state
monitor PERIOD [CATEGORIES] periodical monitoring of server state
backup FILE_NAME [TIME [LOG_SIZE]] schedule online backup process
stop backup stop backup process
restore BACKUP_FILE_NAME restore database from the backup
trace [TRACE_MESSAGE_CLASS] select classes of output messages
log [LOG_FILE_NAME|"-"] set log file name
set PARAMETER = INTEGER_VALUE set server parameters
rename OLD_CLASS_NAME NEW_CLASS_NAME rename class in the storage
rename CLASS COMPONENT_PATH NEW_NAME rename component of the class
>logout

finally{}

Als Fazit zu Goods bleibt ein gemischtes Gefühl. Die Schwächen auf der Betriebsseite sind nicht architekturbedingt und ließen sich innerhalb kürzester Zeit beheben. Schwerer wiegen die Einschränkungen, die durch die Architektur der Datenbank gegeben sind. Nur Subklassen von Persistent zu speichern ist zwar lästig, aber bei einem sauberen Anwendungsdesign macht es sowieso Sinn, nur spezielle, schlanke Datenobjekte persistent zu machen.

Eine ganz massive Einschränkung für das Programmdesign ist es jedoch, alle Objekte an einem einzigen Wurzelobjekt aufzuhängen zu müssen. Und die fehlende servergestützte Query-Verarbeitung macht Goods zudem nur für kleinere Anwendungen sinnvoll einsetzbar.

Auf der Habenseite ist Goods eine sehr schlanke und durchsichtige Implementation, die auch den gemischten Betrieb von Java- und C++-Clients erlaubt. Letztlich ist also anhand der konkreten Anforderungen von Fall zu Fall abzuwägen, ob Goods eingesetzt werden kann. Da Goods unter einer sehr liberalen, BSD-ähnlichen Lizenz steht, hat zudem jeder die Möglichkeit, Verbesserungen einzubringen. ( uwo)

Infos

[1] Goods-Homepage: http://www.ispras.ru/~knizhnik

[2] Poet-Homepage: http://www.poet.de

[3] Coffee-Shop: Objekte im Kasten, Linux-Magazin 2/2000, S. 144ff. (auch online verfügbar)

[4] ODMG-Homepage: http://www.odmg.org

Der Autor

Bernhard Bablok arbeitet bei der AGIS mbH (Allianz Gesellschaft für Informatik Service mbH) als Systemprogrammierer im Systemmanagement-Bereich. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Objektorientierung. Er ist zu erreichen unter: coffee-shop@bablokb.de

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben