Wie speichert man Objekte

Freitag, 04.05.2018

Mirko Matytschak

In unserer täglichen Arbeit verwenden wir ein Tool, das wir bereits Anfang des neuen Jahrtausends entwickelt haben: NDO. Dieses Tool löst die Aufgabe, Objekte zu speichern und Objektsysteme von der Datenbank abzufragen. Dabei kommt eine geniale Technologie zum Einsatz.

NDO ist die Abkürzung für .NET Data Objects und ist eine sogenannte Objektrelationale Persistenzschicht. Das sind eine Menge Fremdwörter, die aber ganz leicht zu erklären sind. Bei der Gelegenheit werfen wir einen kurzen Blick unter die Haube des Systems und erklären, warum das so genial ist.

Endlich – Objekte!

Als Microsoft im Jahr 2000 das .NET-Framework und die Sprache C# als Alternative zu Java aus dem Hut zauberte, war die Begeisterung bei den Entwicklern auf der Windows-Plattform groß. C# ist eine moderne, objektorientierte Sprache. Mit ihr lassen sich Software-Komponenten entwickeln, sogenannte Assemblies, die sich sehr leicht zu großen, leistungsfähigen Softwaresystemen zusammenstecken lassen.

Das Interessante an der Objektorientierung ist, dass sie es erlaubt, kleine abgekapselte Module zu formulieren, sogenannte Klassen, die als Komponenten für Anwendungen eingesetzt werden können. Objekte sind dann Instanzen dieser Klassen, die einen bestimmten Speicherbereich in Anspruch nehmen und das Verhalten der Klasse repräsentieren. Dabei sind Objekte Black Boxes: Man sagt den Objekten, was sie zu tun haben, ohne sich darum kümmern zu müssen, wie sie dies tun und welche Daten sie genau dazu benötigen. Die Daten des Objekts, auch Status genannt, können also von der Außenwelt isoliert werden, sie bleiben privat und können nur vom Code der Klasse verändert werden.

Auf diese Weise kann man ein System, das man programmiert, als ein System von Objekten verstehen, die durch mannigfaltige Beziehungen miteinander verbunden sind und jeweils einen bestimmten Status und ein bestimmtes Verhalten kapseln.

Speichern von Objekten

Es gibt nur ein kleines Problem damit, das es zu lösen gilt: Wenn man dem Objektsystem den Strom entzieht, ist es auf Nimmerwiedersehen verschwunden. Man braucht also einen Weg, Objektsysteme auf der Festplatte zu speichern und von der Festplatte wieder zu laden. Dieses Speichern und Laden nennt man Persistenz. Ein System, das Objekte speichern und laden kann, nennt sich „Objektorientierte Persistenzschicht“. Es erweitert objektorientierte Systeme um den Aspekt der Persistenz.

Nun hat sich im Lauf der Jahre eine Technologie etabliert, mit der man Daten in Tabellen speichern kann, diese Tabellen in Beziehungen zueinander setzen kann und mit leistungsfähigen Abfragesprachen bestimmte Zeilen dieser Tabellen suchen kann. Diese Technologie nennt sich Relationale Datenbanken. Microsofts SQL Server, MySql, Oracle oder Sqlite sind Beispiele für relationale Datenbanken. Diese Datenbanken werden als Datenbank-Server ausgeliefert, sodass sie auf eigenen Maschinen oder in der Cloud für Anwendungen zur Verfügung stehen.

Wenn man nun Objektsysteme in relationalen Datenbanken speichert, dann braucht man eine Technik, mit der Objektsysteme in relationale Daten und andersherum übersetzt werden können. Ein System, das eine solche Technik realisiert, nennt sich „Objektrelationale Persistenzschicht“. NDO ist eine solche objektrelationale Persistenzschicht.

Warum schreibt man eine Persistenzschicht?

Als wir mit der Entwicklung von NDO anfingen, waren .NET und C# noch völlig neu. Wir entwickelten eine kleine Studie, wie man mit C# Objekte auf relationale Strukturen abbilden kann. Diese stellten wir damals im Rahmen einer Workshop-Serie für Software-Entwickler vor. Das passiert häufig: Um etwas zu verstehen, programmiert man es erst einmal und lernt dann die Thematik in größerer Tiefe kennen. Aus den Erkenntnissen dieser Beispielimplementierung leiteten wir Eigenschaften ab, die eine ideale Persistenzschicht nach unserer Vorstellung haben sollte. Die wichtigsten dieser Eigenschaften waren:

  1. Das Tool sollte eine vernünftige Datenbankstruktur erstellen können
  2. Lazy Loading sollte unterstützt werden
  3. Es sollte automatisch erkannt werden, wenn Objekte geändert wurden
  4. Die Persistenzschicht sollte keine Basisklasse in der Vererbungshierarchie voraussetzen
  5. Es sollten Transaktionen unterstützt werden

In einem Artikel, der seinerzeit in der Zeitschrift dotnetpro erschien, beschrieben wir diese Anforderungen näher und gingen darauf ein, wie eine Persistenzlösung diese Anforderungen erfüllen kann. Der Artikel ist auch heute noch lesenswert, auch wenn wir damals noch Lösungen beschrieben haben, die es heute nicht mehr gibt (Object Spaces und Poet), während wir natürlich keine Ahnung davon haben konnten, dass Microsoft sich der Herausforderung mit dem Entity Framework stellte. Die Entwicklung des Entity Frameworks über die Jahre zeigt auch, dass das Thema nicht trivial ist, weil Microsoft im Lauf der Jahre das System ein paarmal ordentlich umkrempeln musste. Unser System NDO jedoch funktioniert im Kern immer noch auf die gleiche Weise. Es basiert auf damals noch völlig neuen Ideen, die zum Teil patentwürdig gewesen wären. Aber Anfang der 2000er Jahre waren Software-Patente noch nicht so üblich, wie heute.

Was hat es nun mit den genannten Kriterien auf sich? Gehen wir sie einmal der Reihe nach durch.

Datenbankstruktur:

Anfang der 2000er Jahre gab es nebst den objekrelationalen Persistenzschichten die rein objektorientierten Lösungen, deren Daten in Objektspeichern lagen, die nur mit den selbst entwickelten Klassen bearbeitbar waren. Objektrelationale Systeme speichern wie gesagt Daten in normalen relationalen Datenbanken, die von allen Anwendungen gelesen und geschrieben werden können, die die verwendete Datenbank ansprechen können.

Beide Lösungen haben ihre Vor- und Nachteile, aber der Siegeszug der objektrelationalen Lösungen hat sicher mit der IT-Strategie in großen Unternehmen zu tun. Viele große Firmen fordern explizit die Möglichkeit, Datenbestände unabhängig von der Logik der Anwendungen, aus denen sie stammen, analysieren zu können. OLAP war (und ist) hier ein wichtiges Stichwort.

Ist dies nicht gefordert, können Objektspeicher in gewissen Szenarien vorteilhaft sein. Objektrelationales Mapping beschränkt die Ausdrucksmittel, die Sie bei der Software-Entwicklung haben. In dem oben genannten Artikel gehen wir auf diese Einschränkungen ein. In den meisten Geschäftsanwendungen aber kann man mit den Einschränkungen des Mappings sehr gut leben und dadurch die Vorteile von relationalen Datenbanken nutzen.

Hier kommt dann die Frage auf, welchen Einfluss die Persistenzlösung auf die Datenbankstruktur nimmt. Gibt es irgendwelche Einschränkungen? Ist ein Mapping der Objekte auf die üblichen relationalen Beziehungspatterns möglich?

Der Härtetest ist meist, ob eine vorhandene, sauber angelegte Datenbank von der Persistenzschicht verwendet werden kann (das sogenannte Reverse Mapping). Ein üblicher Witz war es damals, von Mapping-Tools Abstand zu nehmen, welche die pubs-Beispieldatenbank des SQL-Servers nicht in die Objektwelt abbilden können. Die Pubs-Datenbank war zwar sehr klein, aber um zu zeigen, wie flexibel der Sql Server ist, benutzte sie Konstrukte, die kein vernünftiger Mensch in einer Anwendung aus dem richtigen Leben einsetzen würde. Es ist daher nur mit einigem Aufwand möglich, die Datenbank mit den üblichen Persistenzschichten anzusprechen.

Relationen und Lazy Loading

Eine typische Anwendung soll nicht nur einzelne Objekte in Tabellen ablegen können, sondern auch die Beziehungen von Objekten zueinander.

Angenommen, Sie programmieren ein Abrechnungssystem für Reisekosten mit der folgenden Objekthierarchie: Firma -> Mitarbeiter -> Reise -> Beleg. In Ihrem Datenbestand befindet sich eine Firma mit 50 Mitarbeitern, jeder der Mitarbeiter hat im Schnitt bereits 50 Reisen unternommen, auf denen im Schnitt 10 Belege anfallen. Sie holen sich nun das Firmenobjekt aus der Datenbank. Natürlich sollen beim Laden eines Objekts auch die Beziehungen zu anderen Objekten wiederhergestellt werden, sodass man von dem Firma-Objekt simpel die Mitarbeiter abfragen kann. Das hieße aber in unserem Beispiel, dass das Laden des Firmenobjekts das Laden von 25.000 anderen Objekten nach sich zieht. Wenn Sie nämlich die Beziehung zu den Mitarbeiter-Objekten wieder herstellen wollen, müssen Sie die Reiseobjekte laden, das aber bedeutet, dass Sie die Beleg-Objekte brauchen, etc.

Es braucht also einen Mechanismus, der diese Kette an einem wohl definierten Punkt unterbricht. Dazu bedient man sich der so genannten Hollow Objects. Wenn Sie das Firmen-Objekt laden, dann werden statt der echten Mitarbeiter-Objekte Platzhalter in die Liste der Mitarbeiter-Objekte eingefügt. Erst wenn ein Zugriff auf eines dieser Mitarbeiter-Objekte erfolgt, wird dieses Objekt aus der Datenbank nachgeladen. Eine Persistenzlösung muss einen solchen Mechanismus aufweisen – sonst ist sie nicht zu gebrauchen. Im verlinkten Artikel beschreiben wir näher, wie das Lazy Loading verwirklicht wird.

Es sei darauf hingewiesen, dass an dieser Hürde des transparenten Nachladens von Kindobjekten bereits ein Großteil der Persistenz-Lösungen scheitert.

Und es gibt auch das umgekehrte Problem. Wir laden das Firmenobjekt in den Speicher, aber wir wissen, dass wir gleich sämtliche Mitarbeiter-Objekte durchsuchen müssen. Dann würde das Lazy Loading den Ablauf ziemlich verlangsamen. Hier wollen wir also, dass die Mitarbeiter-Objekte gleich mitgeladen werden. Das nennt sich dann Prefetch. Man sagt der Persistenzschicht bei der Abfrage des Firmenobjekts quasi: "Wenn Du schon dabei bist, dann nimm die Mitarbeiter auch gleich mit."

Dirty-Status-Verwaltung

Angenommen, Sie haben 25.000 Objekte geladen. Im Lauf der Zeit wurden zwei davon geändert. Nun drückt der Benutzer auf die Speichern-Taste. Werden nun zwei oder 25.000 Objekte gespeichert? Das hängt davon ab, ob die Persistenzlösung eine Status-Verwaltung hat, die feststellen kann, ob sich ein Objekt geändert hat.

Komfortable Systeme können das automatisch – das ist allerdings alles andere als trivial zu implementieren. Einfachere Systeme erfordern es, dass der Benutzer die Objekte beim Persistenz-System als „Dirty“ anzeigt – das ist unelegant und fehlerträchtig. Alle 25.000 Objekte zu speichern wäre intolerabel. Wie wir im verlinkten Artikel darlegen werden, gibt es einen folgenschweren Zusammenhang zwischen der Statusverwaltung und der Fähigkeit, jedes beliebige Objekt speichern zu können. Eine saubere, vollautomatische Statusverwaltung bekommen Sie nur für Klassen, die Sie selbst geschrieben haben. Die Klasse muss in irgendeiner Weise mithelfen, dem Persistenz-System zu melden: "Ich bin verändert worden".

NDO setzt hier mit einer ganz besonderen Technologie an: Der Benutzer schreibt seine Klassen, ohne sich um die Dirty-State-Verwaltung zu kümmern. Aber er markiert die Klasse mit einem Attribut als persistent. Nach der Kompilierung der Klasse läuft ein Postprozessor, der den Bytecode der DLLs, die beim Kompilieren entstanden ist, nachträglich ändert. Das geht, weil der Bytecode durch einen von Microsoft eingereichten ECMA-Standard definiert ist. Es gibt daher Werkzeuge, die solche DLLs analysieren können und daraus neue DLLs erzeugen können, welche in ihrem Funktionen erweitert sind. Daher nennt sich solch ein Postprozessor auch "Enhancer". Wir waren weltweit eine der ersten Firmen, die einen solchen Enhancer in einem allgemein verfügbaren Produkt eingesetzt haben.

Veränderung der Klassenhierarchie

Wer Persistenz-Lösungen einsetzt, will unbeschwert vom Aspekt der Persistenz seine fachlichen Anforderungen in Klassen gießen. Daher ist es keine gute Praxis, wenn eine Persistenzlösung es erfordert, dass persistente Klassen von einer bestimmten Basisklasse abgeleitet werden müssen. Wir erklären im Artikel, warum dennoch einige Hersteller diesen Weg gehen. Unter anderem erleichtert eine Basisklasse die Dirty-State-Verwaltung. Für den Aufbau Ihrer Klassenhierarchien ist es jedoch eine Einschränkung, die unwillkürlich zu Problemen führt. Der Königsweg führt hier über die Implementierung von Interfaces. So haben wir es auch mit NDO gemacht. Sie schreiben eine persistente Klasse und diese wird durch den Enhancer so verändert, dass sie das Interface IPersistenceCapable implementiert.

Transaktionen

Objekte kommen selten alleine daher. Fast immer stehen sie in Beziehung zu anderen Objekten. Gibt es in diesen Beziehungen eine Änderung, dann müssen meist mehrere Datensätze geändert und in die Datenbank zurück geschrieben werden. Diese Operationen müssen nach außen hin „atomar“ erscheinen, es darf also keine Änderung oder Fehlerbedingung dazwischen kommen. Mit dem Konzept der Transaktion können solche zusammengesetzten Operationen atomar durchgeführt werden. Während eine Transaktion stattfindet, können die beteiligten Tabellen und Datensätze von keiner anderen Transaktion angefasst werden; tritt ein Fehler bei einer Teiloperation auf, wird die gesamte Transaktion rückgängig gemacht. Persistenz-Layer sollten Transaktionen unterstützen. Aber nicht nur das: Sie haben im Speicher eine Menge an Objekten vorliegen, die an einer Transaktion teilnehmen. Wenn Sie eine Transaktion abbrechen, sollten diese Objekte und ihre Beziehungen zueinander auf den gleichen Stand zurückgebracht werden können, wie zuvor. Die Entwicklung dieses Features kostet die Hersteller von Persistenz-Lösungen einiges an Hirnschmalz. Aber es zeigt sich, dass die ernst zu nehmenden Kandidaten am Markt das Problem erkannt und gelöst haben. So ist es auch bei NDO geschehen.

Welche Datenbanken werden unterstützt?

Eine nicht ganz unwichtige Anforderung ließe sich noch anfügen: Die Unterstützung mehrerer Datenbanken (SQL Server, Oracle, MySql, etc.) kann ein Hinweis dafür sein, dass eine Persistenzlösung eine saubere Architektur aufweist.

Die Frage ist hier: Können Adapter für weitere Datenbanken selbst geschrieben werden? Für freiberufliche Entwickler ist es essentiell, wenn sie für verschiedene Kunden unterschiedliche Datenbanken ansprechen können. Es wäre doch traurig, wenn sie dabei nicht ein und dieselbe Persistenzlösung verwenden könnten. NDO unterstützt Sql Server, Access (nur .mdb), SQLite, SqlCe, Postgre, Mysql, Oracle und Firebird. Weitere Provider lassen sich leicht aus den Sourcen vorhandener Provider schreiben.

Neugierig auf NDO?

Wenn Sie nun noch mehr über NDO wissen wollen, können Sie einen Blick auf die NDO-Website werfen. Dort gibt es auch ein kleines Tutorial. NDO ist frei und Open Source. Sie finden die Sourcen auf Github und können es als Package von Nuget herunterladen. Der Sql Server ist im NDO-Standard-Package enthalten. Die Provider für andere Datenbanken können Sie ebenfalls von Nuget laden. Suchen Sie dazu nach dem Stichwort "NDO" unter Nuget. Für Visual Studio gibt es ein kleines Plug-in, das die Arbeit mit NDO zusätzlich erleichtert.

Einen Kommentar verfassen

Anleitung zur Textformatierung

Zum Formatieren des Textes verwenden Sie [b][/b] und [i][/i]. Verwenden Sie [url=http://ihre-site]Text[/url] für Links.

* Pflichtfelder