Freitag, 3. September 2010


Artikel

Mai 2006 | Artikel

Renaissance der Objektdatenbanken?

(Link zum Artikel: http://www.entwickler.de/dotnet//000824)

Die kostenlose db4o-Objektdatenbank fasziniert Entwickler

Text: Stefan Edlich
Mit db4o Version 5.0 steht eine neue Version einer Objektdatenbank zur Verfügung, die in vielen Foren wegen einiger neuer Features − wie native Abfragen alias Native Queries (NQ) − für Furore gesorgt hat. Grund genug also, diese Version mit ihren überraschend einfachen Persistenzmechanismen einmal genauer unter die Lupe zu nehmen.

Seit einer langen Zeit liegt der Begriff Impedance Mismatch oft wie ein schwerer Fluch über dem Wunsch der Anwender, der Persistenz von Objekten etwas eleganter Herr zu werden. So werden zwar die Mapping-Lösungen immer ausgefeilter, aber grundlegende Konzepte wie Vererbung, Polymorphie, komplexe Referenzen etc. sträuben sich recht erfolgreich, sich dem OO-relationalen Mapping hinzugeben. Nicht triviale Objekte direkt zu speichern erscheint daher als der sinnvollste Weg. Er hat sich jedoch bisher in Ermangelung vernünftiger Standards und einfacher bzw. leistungsfähiger Tools leider noch immer nicht als erfolgreicher Weg herausgestellt. Dem begegnet db4o, indem die Datenbank zusätzlich ein weiteres Persistenzproblem angeht: Eine Abfragesprache weniger anzubieten. db4o wird unter einem „Dual Licence Model“ vertrieben – die Verwendung ist so lange frei, wie db4o nicht in einem Non-GPL-Produkt eingebettet und vertrieben wird.

Von 0 auf 100 in wenigen Sekunden
Bevor die brandneuen Features von db4o vorgestellt werden, soll dem Leser die Möglichkeit gegeben werden, sich innerhalb weniger Minuten selbst ein Bild von der Objektdatenbank mit ihren gerade für erfahrenen Entwickler neuartigen Konzepten zu machen. Sie kann von db4o.com schnell in der passenden .NET- oder Java-Version heruntergeladen werden. Nach der Installation des MSI-Archives liegt eine Assembly mit dem Namen db4o.dll vor, auf der zu jedem Projekt ein Verweis eingefügt wird. Jetzt kann jede beliebige Klasse mit nur zwei Zeilen gespeichert werden. Schon hier könnten manche Leser stutzig werden: Keine besonderen Ableitungen, keine Interfaces, unter modernen Plattformen auch keine besonderen Konstruktoren? Das klingt verdächtig, aber es funktioniert. Jetzt also eine Objektinstanz speichern:
  1. ObjectContainer db = Db4o.OpenFile("C:/tmp/myDB.yap");
  2. db.Set(new Person("John", 42));
  3. db.Commit() ;
  4. db.Close();
Das sollte auf so ziemlich jedem Rechner sofort lauffähig sein. Schon hier fällt einer der vielen Vorteile auf: Es ist keine Schemainformation nötig. Die Klasse selbst ist das Schema. db4o liest das Schema via Reflection aus dem Objekt selbst. Wie die Klasse Person aussieht, ist daher völlig egal. Dennoch sollten natürlich verschiedene Anwendungen, die auf die gleiche Datenbasis zugreifen, Kenntnis derselben Objektdefinitionen haben. Jetzt fehlt eigentlich nur noch: Objekte finden, ändern, löschen und das grundlegende Handwerkszeug ist vorhanden. Listing 1 zeigt diese Operationen im Schnelldurchlauf.
  1. Listing 1
  2. ------------------------------------------------------------------
  3. ObjectContainer db = Db4o.OpenFile("C:/tmp/myDB.yap");
  4. Person tmp = new Person();
  5. tmp.name = "John";
  6. ObjectSet res = db.Get(tmp);
  7. p1 = (Person) res.Next(); // Print p2
  8. p1.Age = 43 ;
  9. db.Set(p1); // update the object
  10. db.Delete(p1); //delete the object
  11. db.Commit();
  12. db.Close();
Als erster Schritt wird die Datenbank (in Gestalt der Datei myDB.yap) wieder geöffnet. Die vorher gespeicherte Person soll nun gesucht werden. db4o kennt drei verschiedene Abfragemechanismen. Der erste ist auch der einfachste und besonders gut für Trivialsuchen geeignet: Query by Example (QBE). Hier wird ein Beispielobjekt übergeben und alle Objekte in einem ObjectSet zurückgegeben, die den gesetzten Feldern entsprechen. Danach ist das gefundene Objekt dem Objekt-Container db bekannt, kann geändert und mit set() gespeichert werden. Ebenso trivial ist das Löschen von Objekten mit delete(). Bliebe noch zu erwähnen, dass das Ganze mit commit() und rollback() transaktionssicher ist (ACID), es einen Client/Server-Modus gibt, Replikation recht einfach ist, es einen In-Memory-Modus gibt und die ganze Datenbank – besonders bei tiefen Objekten – den Benchmarks nach [1] extrem schnell ist. Natürlich gibt es noch viele weitere Features, wie die Unempfindlichkeit gegen die meisten Schema/Klassenänderungen, aber die folgenden Betrachtungen konzentrieren sich auf die Persistenz selbst und die Native Queries.
Eine Abfragesprache weniger?
Grundsätzlich sind alle Abfragesprachen (Query Languages) wie SQL, OQL etc. immer noch mehr oder weniger String- basiert. Und in vielen Fällen ist eine String-basierte Abfragesprache durchaus von Vorteil. Viele Entwickler beherrschen SQL, es gibt „Legacy Code“ und SQL (als populäre Datenbanksprache) enthält viele nützliche Konstrukte. Aber es kann durchaus sehr viele Fälle geben, in denen SQL oder andere String-basierte Abfragesprachen problematisch sind. db4o ist daher die erste Datenbank, die einem Forschungsansatz von William Cook folgt [2][3] (Abbildung 1), bei dem einfach die eigene Programmiersprache als Abfragesprache vorgeschlagen wird. Die Idee besteht darin, die Mächtigkeit von C# oder Java auszunutzen, um quasi ein einfaches Abfrageobjekt zu erstellen, das das Ergebnis der Abfrage in einer typischen Liste zurückliefert. Diese Abfragen in der hauseigenen Programmiersprache werden hier Native Queries (NQ) genannt. Ein einfaches Beispiel:
  1. IList <Pilot> pilots = db.Query <Pilot> (delegate(Person person) {
  2. return person.Age > 50 && person.Name == "John";});
Wo liegen die Vorteile dieses Ansatzes?
  • Diese Abfragen sind typsicher. Der gesamte Code in dieser Abfrage wird in der IDE zur Entwicklungszeit geprüft. Fehler, die sonst in SQL-Strings verborgen wären und dort erst spät gefunden würden, werden in der Entwicklungsumgebung (Visual Studio) sofort entdeckt.
  • Der Code ist refaktorierbar. Das Schlagwort Refactoring – quasi Code- oder Architekturanpassungen, Änderungen oder Verbesserungen – ist in aller Munde und auch in alle guten Entwicklungsumgebungen integriert. Selbst einfache Refactorings wie das Umbenennen von Feldern kann bei RDBMS/Mapping-Lösungen zu unangenehmen Fehlern führen, da sie in den Mapping-XML- Dateien oder im SQL-Code nicht direkt abhängig sind. Das Refactoring eines Feldes einer C#-Klasse allerdings bewirkt, dass die Abfrage selbst auch sofort korrekt geändert wird.
  • Der Entwickler braucht keine weite re Abfragesprache mehr, außer seiner (Lieblings-) Sprache selbst. Er kann beliebige AND-, OR- oder NOT-Sprachelemente von C# nutzen.
In dem zuvor gezeigten „Delegaten“ kann der Entwickler innerhalb der geschweiften Klammer jeden beliebigen Code oder beliebige Methoden aufrufen. In der Praxis ist die Anwendung von Native Queries einfach und macht zudem noch Spaß. In jeder Entwicklungsumgebung kann trivial ein Makro definiert werden, welches den Rumpf des kompletten Delegaten auf Knopfdruck in den Quelltext einfügt. Dann muss nur noch die eigentliche Abfrage über die Verknüpfung der Felder ausgefüllt werden. Die Implementierung dieses Mechanismus ist recht interessant, weil die Abfrage intern in die zweite db4o-Abfragesprache SODA übersetzt wird (dabei werden Bedingungen = Constraints als Objektknoten in Abfrageobjekten definiert). Das bedeutet, dass in C# der vorliegende Delegat oder in Java die entsprechende match-Methode nie ausgeführt wird. Spannend werden diese Abfragen besonders dann, wenn (mittels Zuschalten von weiteren Bibliotheken) eine Abfrageoptimierung – also die Erstellung eines effizienteren SODA-Baumes – durchgeführt wird. Das Ziel ist natürlich, möglichst wenige oder gar keine Objekte für die Abfrage instanziieren zu müssen. Hier leistet die db4o-Optimization-Engine bereits in der aktuellen Version sehr gute Arbeit und wird in Zukunft weiter verbessert werden.
Komplexität beherrschen
Objektdatenbanken spielen ihren Vorteil – auch in Bezug auf die Performance – besonders bei komplexen Objektstrukturen aus. So gibt es durchaus viele Anwendungen, deren Objektstruktur tief ist, was häufiger bei baum- oder netzartigen Strukturen der Fall ist. In der Industrie sind hier nicht selten Objekttiefen von 10 bis 20 anzutreffen (Firma referenziert Angestellte, Angestellte referenziert Personen, Personen referenziert Adressen, usw.).
Aber auch dies soll an einem einfachen Beispiel in Abbildung 2 illustriert werden: Gegeben sei eine Klasse Part, die ein beliebiges Bauteil referenziert. Diese wird um eine umfassende Klasse PartAssembly erweitert (Listing 2), die eine ArrayListe der Bauteile enthält. Das Ergebnis kann zum Bespiel ein Auto oder ein Mainboard repräsentieren.
  1. Listing 2
  2. ------------------------------------------------------
  3. public class Part {
  4. private string category;
  5. private string name;
  6. private string manufacturer;
  7. private string manufacturerPartNumber;
  8. . . .
  9. }
  10. public class PartAssembly : Part {
  11. private System.Collections.ArrayList components;
  12. . . .
  13. }
Schon bei dieser trivialen Hierarchie und nur einer Collection gibt es zwei Möglichkeiten, dies unter Relationalen Systemen (RDBMS) in Datenbanken zu gießen: Entweder eine Lösung mit drei oder zwei Tabellen, je nachdem ob man components eine eigene Tabelle zugesteht oder nicht. Bei drei Tabellen würde man Part in einer Tabelle, PartAssembly in einer zweiten Tabelle und den Inhalt der ArrayList in einer dritten („horizontales Mapping“) darstellen. Die components-Tabelle würde zwei Spalten enthalten: Eine für den Part und eine Referenz für PartAssembly, den Identifier. Die zweite Spalte wäre der Fremdschlüssel. Sie stellt die Verbindung zwischen den Komponenten und PartAssembly her. Bei einer Lösung mit zwei Tabellen („filtered Mapping“) besäßen Part und PartAssembly jeweils eine Tabelle. In diesem Fall müsste eine weitere Spalte – classID – ergänzt werden, welche jene Zeilen unterscheidet, die Part-Objekte von ihren PartAssembly-Objekten trennen. Man stelle sich nun einmal vor, die Entwickler würden diese Klassen mit vielen weiteren Klassen und Collections spicken. Wie wäre es aber, wenn all die Diskussionen um Mapping-Strategien völlig überflüssig wären? Unter db4o ist dem so.

db4o speichert jedes beliebige Objekt als ein solches über db.set(anyObject) ab. Das Suchen und Laden dieser Objekte ist ebenso trivial, dank der Abfragesprachen QBE, SODA und den neuen NQs. Ein Zusammensetzen von komplexen Objekten aus einer Vielzahl von relationalen Tabellen kann daher völlig entfallen. Listing 3 und 4 zeigen das Speichern eines Bauteiles und (QBE) Suchen nach Bauteilen, die einem konkreten Hersteller (Manufacturer) entsprechen.

  1. Listing 3
  2. -------------------------------------------------------------
  3. ObjectContainer Database = Db4o.OpenFile(fileName);
  4. // Container öffnen
  5. // Erstelle ein neues Part Objekt
  6. Part mb = new Part ( "Motherboard",
  7. "Desktop Board D945Gnt",
  8. "Intel",
  9. "BOXD945GNT");
  10. Database.Set(mb); // Speichere das Bauteil in der Datenbank
  11. Database.Commit();// Schliesse die Transaktion ab
  12. Database.Close(); // Schliesse die Datenbank
  1. Listing 4
  2. ------------------------------------------------------------------------------------
  3. Part PartTemplate = new Part(// erstelle ein Template-Objekt zum Suchen
  4. null,
  5. "Desktop Board D945GNT",
  6. null, null);
  7. ObjectSet result = Database.Get(PartTemp);// Suche nach Objekten die passen
  8. if(Result.HasNext()) { // Auslesen der Objekte
  9. Part mb = (Part) result.Next();
  10. // Etwas mit dem Objekt machen
  11. . . .
  12. }
Natürlich werden die Mapper und dazugehörigen Tools immer leistungsfähiger und die Frage nach dem konkreten Mapping wird einem in der Regel abgenommen. Dies kann aber im Datenbankmodell oder in der Performance von Nachteil sein. Und wenn die Objekte wirklich komplex werden, stoßen auch viele Mapper schnell an ihre Grenzen. Zwei Dinge sind weiterhin interessant: db4o erlaubt es, beim Suchen und dem dazugehörigen Laden, die Tiefe zu steuern, mit der Objekte aktiviert werden. Unter Umständen kann es von Vorteil oder viel schneller sein, nicht das gesamte 20 Stufen tiefe Objekt zu laden, sondern nur die ersten ein bis zwei Stufen. Dies kann mit dem ActivationDepth-Parameter gesteuert werden:
  1. Db4o.configure().activationDepth(5);
  2. // Hier z.B. 5 Stufen tief
Das Gleiche gilt für Updates, bei denen ebenfalls gesteuert werden kann, wie tief „geupdated” werden soll. In einer kommenden Version soll es einen Transparent-Modus geben. Der Entwickler muss sich, wenn er dies wünscht (eventuell auf Kosten der Performance), nicht mehr um eine stufentiefe Aktivierung kümmern. Allerdings lässt sich bereits jetzt die ActivationDepth (UpdateDepth) beliebig hoch setzen und der Entwickler bekommt (und schreibt) immer das Objekt in seiner gesamten Tiefe.
Fazit
Der Native-Query-Ansatz ist ein Leckerbissen für Entwickler, der mit Sicherheit den bisherigen Verfahren/Abfragesprachen erhebliche Marktanteile streitig machen wird. Seine Berechtigung erfahren NQs alleine schon dadurch, dass Microsoft im Rahmen seines LINQ-Projektes [4] diesen Trend erkannt hat und selbst vorantreibt. So wird die LINQ-Syntax ein Teil des kommenden C# 3.0 sein. Durch die vorgestellten Mechanismen und Vereinfachungen bei der Persistenz könnten Objektdatenbanken durchaus einen zweiten Frühling erleben. Insbesondere in Nischen, die nicht durch RDBMS-Lösungen oder Data-Warehouse-Anforderungen besetzt sind. Zum Beispiel im mobilen und Embedded-Bereich, der durch die nur 400 KB große DLL ideal für db4o erscheint. Der Leser sei daher herzlich eingeladen, die paar Minuten zu investieren, um diese Technologie einem Praxistest zu unterziehen.
Neue Features in Version 5.0
  • Native Queries
  • Unterstützung für .NET 2.0 Generic Collection
  • Strong Encryption für db4o File I/O
  • Ein Hardware-Crash-Simulator
  • Ein neues Freespace-Management-System

Features, die 2006 kommen:

  • RDBMS-Replikation mittels Hibernate
  • Transparente Aktivierung von Objekten
  • Neuer Objectmanager V2.0
  • Schnelle Collections
  • Ein Visual-Basic-Tutorial
  • Verbesserte Defragmentierung
  • J2ME-CLDC/MIDP-Unterstützung
Prof. Dr. Stefan Edlich lehrt Softwaretechnik an der TFH-Berlin und ist Autor vieler Informatikbücher. Er ist Mitgründer des Vereins OODBMS.org und seit db4o von Objektdatenbanken fasziniert.

Links & Literatur
  1. www.polepos.org
  2. William R. Cook, Siddhartha Rai: Statically Typed Objects as Remotely Executable Queries, Dept of CS, University of Texas at Austin
  3. William R. Cook, Carl Rosenberger: Native Queries for Persistent Objects A Design White Paper, August 2005
  4. msdn.microsoft.com/netframework/future/linq/

Kommentare