Follow my new blog

Samstag, 29. August 2009

Entspannte Persistenz – The Lounge Repository

Relationale Datenbanken sind nicht die ganze Wahrheit für die Datenspeicherung. Das merken immer mehr Entwickler. Die Zahl der “alternativen Datenbanken” nimmt zu und die RDBMS-Frustrierten formieren sich schon: NOSQL ist der Schlachtruf.

image Einige Prominenz hat bei den “alternativen Datenbanken” nun CouchDB erlangt: eine schemalose Datenbank zur Speicherung von Dokumenten. Der API ist denkbar einfach, Queries sind natürlich auch möglich – aber für viele Anwendungen sind Dokumente eine unpassende Abstraktion ihrer Daten. Ein Kunde mit seinen Adressen mag noch als Dokument durchgehen. Doch wie ist es mit einem Kunden und seinen Rechnungen oder umgekehrt einer Rechnung mit ihrem Kunden? Kunden und Rechnungen als separate Dokumente anzusehen, funktioniert, doch sie müssen ja in Beziehung gesetzt werden. Beziehungen zwischen Dokumenten sind jedoch nicht natürlich. Das macht ja gerade die “Dokumentenhaftigkeit” aus, dass in einem Dokument Daten zusammengefasst sind, die eng zueinander gehören. Ein Dokument ist etwas Abgeschlossenes, es ist self-contained.

Dennoch übt die Einfachheit der Persistenz mit CouchDB Faszination aus. StupidDB versucht z.B. das Persistenz-Paradigma von CouchDB in die .NET-Welt zu bringen und noch “einen oben drauf zu setzen”: StupidDB ist bewusst serverlos, denn “[d]urch Replikation des Filesystems z.B. per Windows-DFS ist […] eine einfache Hochverfügbarkeit und Skalierbarkeit der Datenbasis” herstellbar.

So schön einfach die Persistenz mit StupidDB jedoch auch ist, sie leidet unter demselben Problem wie CouchDB. StupidDB verwaltet Dokumente, die nur als Ganzes gespeichert werden. Objektgraphen werden en bloc in eine Datei serialisiert.

Schemalosigkeit für Geschäftsanwendungen: The Lounge Repository

Motiviert durch diese Ansätze habe ich nun versucht, die Vorteile der Schemalosigkeit auch für Geschäftsanwendungen zu erschließen. Statt stupide auf dem Sofa abzuhängen, finde ich es jedoch zeitgemäß und auch geselliger, zu “loungen”. Deshalb habe ich meinen kleinen Open Source Persistenzframework “The Lounge Repository” genannt.

Der Name ist Programm:

  • “Lounge” soll anzeigen, dass es ein denkbar einfach und intuitiv zu benutzender Framework ist. Entspannt Objektgraphen persistieren: das soll The Lounge Repository möglich machen. Sie müssen das Persistenzmedium (hier: das Dateisystem) nicht mit einem Schema strukturieren, bevor Sie darin etwas speichern können.
  • “Repository” statt des verbreiteten Suffixes “DB” soll einen Hinweis auf die Art der Daten geben, die mit dem Framework verwaltet werden.  Repository ist ein Begriff aus dem Domain Driven Design (DDD) und bezeichnet eine Dienstleistung zur Speicherung von Entities: “Definition: A Repository is a mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects.”

Entspannt Entities persistieren, darum geht es bei The Lounge Repository. Es ist als generisches Repository für alle möglichen Arten von Entities gedacht.

Was ist eine Entity? Ein zustandsbehaftetes Objekt mit einer eigenen, speicherübergreifenden Identität. Sie identifiziert es im Hauptspeicher wie im Persistenzmedium.

Kunde und Rechnung sind naheliegende Entitäten für eine Faktura-Anwendung. Eine Rechnungsposition jedoch nicht, da sie nicht unabhängig von einer Rechnung existiert. Auf Kunden wie Rechnungen möchte man sicher direkt zugreifen, sie müssen unabhängig von einander adressierbar sein; auf Rechnungspositionen kommt man hingegen nur über ihre Rechnung. Rechnungspositionen sind im DDD-Jargon sog. Value Objects.

Wenn Sie Ihre Anwendung auf der Basis von DDD modellieren, dann kommen am Ende sicherlich auch Entities und Value Objects heraus, die Sie persistieren wollen. Entities sollen einzeln geladen und gespeichert werden oder in größeren Zusammenhängen, z.B. eine Rechnung zusammen mit ihrem Kunden. Solche Zusammenhänge (cluster) nennt DDD Aggregate.

image

Wie nun die Persistenz von Éntities bewerkstelligen? Sicherlich können Sie dafür ein RDBMS Ihrer Wahl heranziehen. ADO.NET macht es dann möglich. Oder Sie machen es sich schon etwas einfacher und benutzen einen O/R-Mapper Ihres Geschmacks – von EntityFramework über Open Access und LLBLGENPRO bis zu NHibernate. Aber ich versichere Ihnen, wenn Sie nicht sattelfest mit einem dieser Persistenzframeworks sind, dann werden Sie eine rechte Mühe haben, Ihre Entities zu persistieren. Persistor.Net verspricht zwar, Ihnen einige dieser O/R-Mapping-Mühen abzunehmen, aber auch er setzt auf ein RDBMS, das es dann zu verwalten gilt.

Entity Graphen im Dateisystem speichern

CouchDB & Co. haben es deshalb auch leicht gehabt, bei der Usability zu punkten. Ihre APIs sind allemal “für die schnellere Persistenz zwischendurch” sehr schön einfach. Nur leider passen sie, wie schon erwähnt, nicht so gut zum Datenmodell er üblichen Geschäftsanwendungen. Objektgraphen mit vielen Entities (Aggregate) lassen sich nicht wirklich als Dokument beschreiben. Denn wenn dieselbe Entity in mehreren solchen Aggregaten vorkommt, wird sie in mehreren Dokumenten persistiert. Sie verliert damit ihre zweckstiftende Eigenschaft, ihre Identität, ihre Eindeutigkeit.

image

So sieht Ihr schöner Entity Graph aus, wenn CouchDB oder StupidDB ihn gespeichert haben. Alles ist zu einem Dokumentenganzen zusammengefasst.

The Lounge Repository macht es hingegen anders! Jede Entity wird hier separat in einer Datei gespeichert, egal wie tief eingeschachtelt sie in einem Entity Graphen ist. Das funktioniert natürlich auch mit zyklischen Referenzen:

image

The Lounge Repository erhält die logische Identität durch physische Separierung. Das unterscheidet es wesentlich von StupidDB.

Der Lounge Repository API

The Lounge Repository ist zunächst eine Fingerübung (oder eine umfangreichere Code Kata, wenn Sie so wollen). Mit dem Lounge Repository will ich also zwei Fliegen mit einer Klappe schlagen: Ich möchte ein Werkzeug haben, mit dem ich meine Gedanken zum Thema Schemalosigkeit praktisch ausprobieren kann. Und ich möchte in kontrollierter Umgebung meinen Programmiersüchten nachgehen können ;-) Denn wer würde leugnen wollen, dass die Programmierung von Infrastruktur nicht süchtig macht? Im kleinen Freizeitrahmen ist das aber genauso wenig schlimm, wie ein gelegentliches Bierchen am Abend oder eine Zigarette alle Jahre wieder auf der Wies´n. In den endlich-clean.net Entzug muss ich deshalb jedenfalls noch nicht. Allemal, weil ich mich bemüht habe, den Lounge Repository Code clean zu halten. Doch Vorsicht vor Infrastrukturprogrammierung in Ihren Projekten!

Doch jetzt weiter zu Konkretem, zum Code. Wie sieht der API des Lounge Repository aus? Ein Hello-World-Beispiel zeigt das Grundsätzliche in wenigen Zeilen. Laden Sie den Quellcode von CodePlex herunter und machen Sie mit. Das geht mit einem SVN-Client wie Tortoise ganz schnell. Die Quellen enthalten auch eine Projektmappe mit kleinen Beispielen.

Hier nun ein Beispielprojekt, wie Sie es aufsetzen können, wenn Sie die Lounge Repository Quellen mittels deren Projektmappe einmal übersetzt haben. Es stehen dann im globalen bin-Verzeichnis des Quellbaumes die Assemblies des Frameworks bereit:

image

Davon binden Sie zunächst aber nur zwei ein: LoungeRepo.Core und LoungeRepo.Contracts:

image

Mehr ist als Vorbereitung nicht nötig. The Lounge Repository kennt keine Datenbankdateien und hat (noch) keinen Serverprozess. Es ist eine experimentelle “embedded database”.

Und jetzt der Hello-World-Code:

    1 using System;

    2 using LoungeRepo.Contracts.Core;

    3 using LoungeRepo.Core;

    4 

    5 namespace BlogSample

    6 {

    7     class Program

    8     {

    9         static void Main()

   10         {

   11             using(ILoungeRepository repo = new LoungeRepository())

   12             {

   13                 repo.Store("hello, world!", "1");

   14 

   15                 string greeting = repo.Load<string>("1");

   16                 Console.WriteLine(greeting);

   17             }

   18         }

   19     }

   20 }

Mit Store() speichern Sie Entities, mit Load() laden Sie sie wieder. So einfach ist das mit der Persistenz.

Eine Entity ist jedes Objekt, dem Sie dieses Privileg zugestehen möchten. Sie müssen die Klassen zu persistierender Objekte nicht mit einem bestimmten Attribut kennzeichnen oder von bestimmten Klassen ableiten. Eine Zeichenkette kann genauso gut wie ein Kunde-Objekt eine Entity sein:

    5 namespace BlogSample

    6 {

    7     class Kunde

    8     {

    9         public string Name { get; set; }   

   10     }

   11 

   12     class Program

   13     {

   14         static void Main()

   15         {

   16             using(ILoungeRepository repo = new LoungeRepository())

   17             {

   18                 Kunde k = new Kunde {Name="Peter"};

   19                 repo.Store(k, "2");

   20 

   21                 k = repo.Load<Kunde>("2");

   22                 Console.WriteLine(k.Name);

   23             }

   24         }

   25     }

   26 }

Wichtig ist, dass jede Entity auch wirklich eine Identität hat. Die ist beim Speichern und Laden wichtig. Denn aus ihr “berechnet” das Repository den Namen der Datei, in der es die Entität speichert bzw. aus der es sie lädt.

Ein string oder das obige Kundenobjekt haben keine von ihrer Hauptspeicheradresse unabhängige Identität. Also muss der Code explizit eine beim Speichern angeben.

Die Identität einer Entity besteht aus zwei Teilen: einer Id (eine Zeichenkette Ihrer Wahl) und einer optionalen Partition (ebenfalls eine Zeichenkette Ihrer Wahl).

Partitionen unterteilen den “Persistenzraum” (hier: das Dateisystem) und ermöglichen auf lange Sicht eine Lastverteilung. Entities einer bestimmten Partition könnten von dedizierten Servern verwaltet werden. Auch wenn das Zukunftsmusik ist, habe ich mir gedacht, das grundlegende Konzept der Partitionierung schon jetzt mit in die Funktionalität aufzunehmen. Das mag ein wenig YAGNI sein… aber was soll´s? ;-)

Innerhalb einer Parition muss dann die Id eindeutig sein. Zusammen ergeben sie die Identität, die im “Persistenzraum” eindeutig ist. Ist keine Partition definiert, nimmt das Lounge Repository eine default Partition an.

Wenn Sie nicht wissen, was Sie als Partition angeben sollen, dann lassen Sie sie aus – oder wählen Sie z.B. den Klassennamen einer Entity als Partitionsnamen:

   12 class Rechnung

   13 {

   14     public string Rechnungsnummer;

   15     public Kunde Empfänger;

   16 }

   17 

   18 class Program

   19 {

   20     static void Main()

   21     {

   22         using(ILoungeRepository repo = new LoungeRepository())

   23         {

   24             Kunde k = new Kunde {Name="Maria"};

   25             Rechnung r = new Rechnung

   26                             {

   27                                 Rechnungsnummer = "090829-1",

   28                                 Empfänger = k

   29                             };

   30             repo.Store(r, "3", "Rechnung");

   31 

   32             r = repo.Load<Rechnung>("3", "Rechnung");

   33             Console.WriteLine("#{0} für {1}",

   34                         r.Rechnungsnummer,

   35                         r.Empfänger.Name);

   36         }

   37     }

   38 }

Aber nicht nur die Partition “Rechnung” für die Rechnung-Entity ist bemerkenswert an diesem Stück Code. Bitte beachten Sie auch folgendes:

  • Die Kundin Maria wurde natürlich zusammen mit der Rechnung persistiert.
  • Beim Laden der Rechnung wurde die Empfängerin natürlich auch wieder mit geladen.
  • In diesem Beispiel gibt es nur eine Entity: die Rechnung. Das Kunde-Objekt hat von sich aus keine Identität und wurde nicht ausdrücklich als Entity gespeichert.

Das Repository sieht jetzt so aus:

image 

Die Entities aus den vorangehenden Beispielen hatten keine eigene Partition und wurden daher in der default Partition gespeichert. Entity “3”, die Rechnung, steht in der explizit angegebenen Partition “Rechnung”.

Und wo ist die Kundin Maria? Sie steckt in der Rechnung-Entity mit Namen 3.entity, weil ihr Kunde-Objekt ja nicht als Entity gespeichert wurde. Das ist verständlich, oder? Schön ist es aber nicht. Sie wollen ja nicht alle Entity-Objekte explizit speichern müssen. Außerdem würde das nichts nützen, denn selbst wenn das Objekt zu Kundin Maria als Entity gespeichert worden wäre, würde das Repository das nicht merken während der Speicherung der Rechnung. Die “Entitätshaftigkeit” ist einem Kunde-Objekt ja nicht anzusehen. Bisher.

Um Entities nicht mit expliziter Identitätsangabe speichern zu müssen und auch in Entity Graphen erkennbar zu machen, können Sie sie das Interface ILoungeRepoEntityIdentity implementieren lassen. Das definiert nur zwei Properties: Id und Partition. Es trägt also nicht dick auf Ihre Domänenobjekte auf. Damit wird dann das kleine Szenario wirklich intuitiv:

    7 class Kunde : ILoungeRepoEntityIdentity

    8 {

    9     #region Implementation of ILoungeRepoEntityIdentity

   10     public string Id { get; set; }

   11     public string Partition { get { return "Kunden"; } }

   12     #endregion

   13 

   14     public string Name { get; set; }

   15 }

   16 

   17 

   18 class Rechnung : ILoungeRepoEntityIdentity

   19 {

   20     #region Implementation of ILoungeRepoEntityIdentity

   21     public string Id { get { return this.Rechnungsnummer; } }

   22     public string Partition { get { return "Rechnungen"; } }

   23     #endregion

   24 

   25     public string Rechnungsnummer;

   26     public Kunde Empfänger;

   27 }

   28 

   29 

   30 class Program

   31 {

   32     static void Main()

   33     {

   34         using(ILoungeRepository repo = new LoungeRepository())

   35         {

   36             Kunde k = new Kunde {Id="4", Name="Maria"};

   37             Rechnung r = new Rechnung

   38                             {

   39                                 Rechnungsnummer = "090829-1",

   40                                 Empfänger = k

   41                             };

   42             repo.Store(r);

   43 

   44             r = repo.Load<Rechnung>("090829-1", "Rechnungen");

   45             Console.WriteLine("#{0} für {1}",

   46                         r.Rechnungsnummer,

   47                         r.Empfänger.Name);

   48 

   49             k = repo.Load<Kunde>("4", "Kunden");

   50             Console.WriteLine(k.Name);

   51         }

   52     }

   53 }

Kunde und Rechnung implementieren nun das ILoungeRepoEntityIdentity Interface und liefern dem Repository darüber ihre Identitäten. Den Kunden habe ich zur Demonstration so ausgelegt, dass ihm die Id bei Erzeugung zugewiesen werden muss, die Rechnung entnimmt sie ihrer Rechnungsnummer. Beide enthalten jedoch eine fest verdrahtete Partition.

Die Rechnung wird wie erwartet auch jetzt wieder mit ihrem Kunden geladen, darüber hinaus kann der Code jedoch auf den Kunden auch direkt zugreifen, wie Zeile 49 zeigt.

Das ist im Grunde alles, was es zum Laden und Speichern zu sagen gibt. Sie müssen keine Vorbereitungen treffen, aber Entities sollten gekennzeichnet sein. Alle nicht gekennzeichneten Objekte sind für das Repository Value Objects.

Objektgraphen, d.h. Objekthierarchien und –netzwerke – auch solche mit Zyklen – werden korrekt gespeichert/geladen, d.h. Entitäten wandern in je eigene Dateien. Wie Sie Objektverweise aufbauen, ist Ihnen überlassen. Sie können einzelne Referenzen halten wie die Rechnung auf ihren Empfänger. Oder Sie benutzen Arrays oder Collections, um mehrere Referenzen zu verwalten.

The Lounge Repository persistiert alle Felder der Objekte, die ihm zur Speicherung übergeben werden. Immer. Es findet (derzeit) kein change tracking statt. Wollen Sie ein Feld ausschließen, dann setzen Sie darüber das [NonSerialized] Attribut, das Sie von der .NET-Serialisierung kennen.

Dass Sie Entitäten auch löschen können, ist selbstverständlich. Rufen Sie Delete() auf dem LoungeRepository unter Angabe der Identität auf.

Zum Schluss bleibt nur noch eine Frage: Kann man eigentlich auch Entitäten durch Queries ermitteln? Ja, man kann. Das Lounge Repository sammelt alle Entitäten eines Typs in einem sog. Extent. Das ist nichts weiter als eine lange Liste von Objekten, die das Repository als IEnumerable<T> anbietet. Deshalb können Sie darauf mit Linq in gewohnter Weise zugreifen:

   33 static void Main()

   34 {

   35     using(ILoungeRepository repo = new LoungeRepository())

   36     {

   37         Kunde k = new Kunde {Id="4", Name="Maria"};

   38         Rechnung r = new Rechnung

   39                             {

   40                                 Rechnungsnummer = "090829-1",

   41                                 Empfänger = k

   42                             };

   43         repo.Store(r);

   44 

   45         r = new Rechnung

   46                     {

   47                         Rechnungsnummer = "090715-2",

   48                         Empfänger = k

   49                     };

   50         repo.Store(r);

   51 

   52         k = new Kunde { Id = "5", Name = "Dennis" };

   53         r = new Rechnung

   54                     {

   55                         Rechnungsnummer = "090803-3",

   56                         Empfänger = k

   57                     };

   58         repo.Store(r);

   59 

   60 

   61         var mariasRechnungen =

   62             from rg in repo.GetExtent<Rechnung>()

   63                 where rg.Empfänger.Name == "Maria"

   64                 select rg;

   65 

   66 

   67         foreach (Rechnung mariasRg in mariasRechnungen)

   68             Console.WriteLine("#{0} für {1}",

   69                 mariasRg.Rechnungsnummer,

   70                 mariasRg.Empfänger.Name);

   71     }

   72 }

Die Zeilen 37 bis 58 bauen eine kleine Datenbasis an persistenten Entities auf. Und die Zeilen 61 bis 64 fragen sie mit einer Linq-Query ab. repo.GetExtent<T>() liefert dafür die Grundlage in Form einer Liste aller Rechnung-Entities, die geladen oder gespeichert wurden.

Hier liegt z.Z. noch eine Begrenzung des Lounge Repository: Extents enthalten zunächst nur Objekte, die das Repository “gesehen” hat. Objekte, die auf der Platte im Repository liegen, aber vom Repository weder direkt oder indirekt geladen wurden, sind (noch) nicht in dessen Cache enthalten und tauchen daher nicht im Extent auf.

Das können Sie jedoch ausbügeln, indem Sie zu Beginn einer Sitzung den internen Cache mit allen persistenten Entities populieren. Ja, so laden Sie zwar die ganze Datenbank in den Hauptspeicher, aber das macht nichts. The Lounge Repository ist (zunächst) genau für solche Szenarien gedacht, in denen Sie eben nicht Gigabytes an Daten verwalten. Selbst einige Hundert Megabytes in den Hauptspeicher zu laden auf einem 4 GB Laptop sollte allerdings den Kohl nicht fett machen.

Für einen solchen sog. “prefetch” binden Sie einfach die Assembly LoungeRepo.Core.Extensions ein und importieren Sie den gleichnamigen Namensraum. Dann sind alle Entities über ihre Extents zu erreichen:

    5 using LoungeRepo.Core.Extensions;

    6 

    7 namespace BlogSample

    8 {

    9     …

   32     class Program

   33     {

   34         static void Main()

   35         {

   36             using(ILoungeRepository repo = new LoungeRepository())

   37             {

   38                 repo.PrefetchAllEntities();

   39                 …

   63                 var mariasRechnungen =

   64                     from rg in repo.GetExtent<Rechnung>()

   65                         where rg.Empfänger.Name == "Maria"

   66                         select rg;

Ausblick

The Lounge Repository ist “a work in progress”. Ich habe mir damit eine Spielwiese angelegt, auf der ich Ideen zum schemalosen Umgang mit Daten und anderem ausprobieren kann. Das Projekt ist Open Source und Sie finden es bei CodePlex in seiner vollen “clean beauty”: http://loungerepo.codeplex.com/

Laden Sie den Quellcode runter und spielen Sie damit. Wenn Sie Fragen oder Einfälle haben, lassen Sie uns bei CodePlex darüber diskutieren. In der Projektmappe finden Sie auch eine kleine Aufgabenliste, die ich führe. Da sehen Sie, dass noch einiges zu tun ist am Lounge Repository. Und auch darüber hinaus habe ich schon Ideen, z.B. wie ein solche Repository verteilt und asynchron betrieben werden kann.

Einstweilen mag The Lounge Repository Datenbanken wie SQL Server oder selbst CouchDB nicht ersetzen. Aber ich würde mich freuen, wenn in ihm ein Keim läge, der es in einigen Szenarien zu einer Alternative zum default RDBMS machte. Prototypen, kleine Anwendungen… dort, wo Sie Entitäten identifizieren und “schnell mal persistieren wollen” ohne Schemaaltlasten, hat The Lounge Repository (bzw. eine der anderen “alternativen Datenbanken”) es sicher verdient, berücksichtigt zu werden.

Ich bin gespannt auf Ihr Feedback.

19 Kommentare:

Anonym hat gesagt…

Das klingt sehr interessant. Beim Lesen des Artikels habe ich mich nur die ganze Zeit gefragt, wo genau die Daten auf der Platte gespeichert werden. Kann man das konfigurieren?

Ralf Westphal - One Man Think Tank hat gesagt…

@Anonym: Die Daten liegen per default im Verzeichnis der Anwendung. Wenn sie woanders liegen sollen, kann man einen Pfad zu einem anderen Verzeichnis beim Instanzieren von LoungeRepository mitgebene.

-Ralf

Anonym hat gesagt…

Ich finde das auch sehr interessant, habe dazu aber noch ein paar Fragen:

(1) Wie reagiert das System bei Änderungen in den zu persistierenden Klassen? Ich kann mir vorstellen, dass nicht mehr benötigte Felder und Änderungen in der Reihenfolge der Felder bei einigen Serializern Probleme bereiten kann. Ich bin mir nicht sicher, glaube mich aber noch an eine Situation erinnern zu können wo der XmlSerializer Objekte nicht mehr deserialisieren konnte, weil ich die Reihenfolge der Properties geändert hatte. Ich glaube das war aber noch .NET 1.0, ob das mittlerweile behoben wurde kann ich aber nicht sagen.

Auch umbennenen von Klassen etc. könnte Probleme bereiten, hier gibt es aber zumindest die ILoungeRepoEntityIdentity Schnittstelle.

(2) Gibt es die Möglichkeit Felder zu indizieren? z.B. über ein Attribut mit dessen Hilfe dann eine seperate Datei erstellt wird in der alle Indizes stehen? Ich kann mir gut vorstellen, dass dies deutliche Vorteile bringt, wenn ich über viele Daten suchen möchte. Allerdings auch ein deutlicher Overhead. Über einen eigenen Linq Provider wird man dann wohl nicht mehr drum herum kommen.

(3) Ist schon ein Service in Planung? Mit einer REST Schnittstelle und einer entsprechenden .NET API wäre das eine tolle Sache für kleinere Anwendungen oder Prototypen.

Ralf Westphal - One Man Think Tank hat gesagt…

@Anonym: Das sind interessante und wichtige Fragen. Mein derzeitiger Gedankenstand dazu:

1) Aktuell ist intern eine binäre Serialisierung im Einsatz. Aber die macht kein Problem, weil sie die Entities nicht direkt serialisiert, sondern sog. normalisierte Entities (NEntities).

Das Repo wandelt eine Entity in eine Hierarchie von normalisierten Objekten (NObjekts) um. Alles, was kein primitiver Typ ist (oder ein Array davon) wird in ein NObjekt gewandelt. NEntities sind nur ein bisschen besondere NObjekte, weil sie noch id/partition haben.

NObjekte enthalten eine Liste von Name-Wert-Paare für jedes Feld ihres Ursprungsobjekt. Und eine Info über den Typ des Ursprungsobjektes.

Dieser Normalisierungsschritt ist nötig, weil ich sonst nicht wissen kann, welche Entities in einem Entity Graphen stecken.

Entity-Referenzen werden in den NObjects durch spezielle Referenzobjekte ersetzt.

Das erkläre ich hier, damit klar ist, wo der Ansatzpunkt für dein Problem ist: in der Normalisierung und nicht in der Serialisierung.

Wenn du den Normalisierer austauschst (intelligenter machst), dann kann er auch Schemaänderungen bis zu einem gewissen Grad bewältigen. Umbenennungen von Feldern lassen ihn heute noch husten; verschobene Felder machen ihm aber nichts aus. Gelöschte Felder sind auch ok. Neue Felder lassen ihn wieder husten.

2) Bisher habe ich an Optimierungen wie das Indizieren von Felder nicht gedacht. Es geht um das Ausprobieren des grundlegenden Programmiermodells. Wenn es dabei mal etwas länger dauert bei einer komplexen Query, dann find ich das ok. Wie gesagt: The Lounge Repo soll grad nicht SQL Server für die Verwaltung von 10 Millionen Kundendaten ersetzen ;-) Im Augenblick ist das Repo auch 100% Caching ausgerichtet. Das ist die Prämisse.

Da also alle Daten im Hauptspeicher sind, werden Queries auch nicht so lange dauern, würd ich mal sagen.

Indizierung ist also eine Optimierung, die irgendwann mal kommen kann - aber grad eben nicht oben auf meinem Zettel steht. Ich würd sagen: Zeig mir dein Szenario, wo das Fehlen einer Indizierung ein Problem ist. Dann reden wir weiter :-)

3) The Lounge Repo als echten Server aufzusetzen ist hingegen durchaus schon in der Planung. Erst will ich intern Asynchronizität einführen. Dann verteilen. Denn ohne intern bessere Skalierbarkeit macht Verteilung nicht so recht Sinn. Der Sinn von Verteilung ist ja, eine höhere Last zu schultern.

An welcher Stelle ich das Repo dafür auseinander schneide, weiß ich aber noch nicht. Ich glaube aber, dass es wohl erstmal zwischen Serialisierung und Persistenz sein wird. Da ists am einfachsten. Das Protokoll ist in jedem Fall ganz einfach: PUT=Store, GET=Load, DELETE=Delete. Den Extent würd ich erstmal wohl nur lokal lassen.

Bei einem solchen Schnitt wären serverseitige Queries allerdings nicht möglich. Der Server würde nicht mal normalisierte Objekte kennen. Erst die haben ja erkennbare Felder. Vielleicht also auch nach der Normalisierung ein Schnitt? Würde das Prokoll kaum ändern, glaub ich. Aber serverseitige Queries müssten dann "von Hand" programmiert werden, weil die Objekte auf dem Server nicht "im Original" vorliegen. Das möchte ich nämlich vermeiden, sonst musst du nämlich die Assemblies mit den Typen der Objekte auch auf dem Server haben.

Mit normalisierten Objekten wären aber Queries auch möglich. Ist dann nur mehr Aufwand. Wenn ich also mal viel Zeit habe... :-)

Vorher deshalb die einfachere Verteilung, denke ich.

-Ralf

Stefan Koelle hat gesagt…

Hallo, Ralf,

es freut mich, dass du unsere StupidDB auch angesehen hast. Deine Implementierung sieht sehr interessant aus, loest aber nicht die Probleme, die wir mit StupidDB zu loesen versucht haben. Hierzu vielleicht ein paar Gedankengaenge zum weiterdiskutieren:

1. Intelligente Aufteilung der Daten

Idee hinter StupidDB waren die Speicherung von Userdaten. Pro User eine Partition. Wenn ich bei LoungeRepository mal tausende Partitionen habe, dann wird es im Filesystem schnell recht voll bei dir. Du legst ja pro Partition immer wieder ein neues Directory an. Daher unser Hashing der Partitions und die Angabe der erwarteten Anzahl von Partition.


2. Langfristig Aufteilung auf verschiedene Speicherserver

Gleich mit dem Hashing der Partitions hat man langfristig folgene Moeglichkeit. Ueber eine spezielle Verteilung koennte man Partitions ueber verschiedene physikalische Speicherorte verteilen, also Shards bilden. Somit ist man auch skalierbar falls die Performance des Filesystems mal nachlassen sollte.


Diese beiden Punkte waeren jedoch leicht auch bei LoungeRepository moeglich zu implementieren.


3. List von Partitions

Wie gesagt, Userdatenspeicherung war unser Kerngedanke. Somit moechte ich auch dem User alle hochgeladenen Daten anzeigen. Ist bei LoungeRepository nur ueber den Cache derzeit moeglich. Da sieht man wohl auch die unterschiedliche Intention. StupidDB soll selten benutzte Daten einfach und simpel speichern, daher auch kein Cache, aber das in grosser Menge. LoungeRepository soll wenig Daten speichern, dies aber effizient und schoen speichern.


4. Revisions

In StupidDB kann man relativ einfach Revisionen von Objekten anlegen.


Gruss
Stefan Koelle

Ralf Westphal - One Man Think Tank hat gesagt…

@Stefan: Danke für deine Klärungen und Einwände. Der Ausgangspunkt von StupidDB und Lounge Repository ist sicher ein anderer. Insofern ist das LoungeRepo auch keine Kritik an StupidDB, sondern ein Weiterdribbeln nach Aufnahme der Vorlage... ;-)

Zu deinen Punkten:

1) Wieviele Partitionen eine Anwendung anlegt, ist ihr überlassen. Wer nur eine Partition pro User anlegen will, der kann das gern tun.

Das StupidDB wg des Partitionshandlings aber auch große Datenmengen ausgelegt sein soll, verstehe ich nicht. Gerade wenn "nur" User eine Partition anlegen, sind es ja vergleichsweise wenige Partitionen. Die meisten Anwendungen haben höchstens nur hunderte Benutzer. Oder geht es um Web-Anwendungen mit Millionen?

Aber davon abgesehen: Die Zahl der Partitionen halte ich immer für viel kleiner als die der Entities. Insofern sehe ich kein Problem bei einer mehr oder weniger großen Zahl von Partitionsverzeichnissen. Es kommt eher auf die Frage der Datendateien an. Und da unterscheidet sich StupidDB nicht vom LoungeRepo: es wird eben zunächst mal pro Objektgraph 1 Datei angelegt.

The Lounge Repository enthält vom Konzept her also StupidDB: Wenn ich nur die Wurzeln als Entities auslege, wird jeder Objektgraph wirklich nur als 1 Datei persistiert. StupidDB kann es nicht anders. The Lounge Repository will es aber anders können. Das ist der Unterschied.

Die Zahl der Partitionen vorhersehen zu müssen, halte ich für eine unnötige Einschränkung. Sozusagen premature optimization. Selbst hunderte Verzeichnisse machen ja kein Problem. Wenn man sie schachtelt noch weniger. Da setze ich mal auf die Effizienz der Dateisysteme.

2) Bisher habe ich mich tatsächlich auf einen Verzeichnisast auf der Platte beschränkt. Aber ich habe schon ein Feature in Planung, um Partitionen unterschiedlichen Verzeichnissen bzw. Shares zuweisen zu können.

Und wenn ich dann noch Zeit und Lust habe, soll man mit Partitionen auch unterschiedliche Stores verbinden können, z.B. Partition A auf Fileserver SA, Partition B bei S3, Partition C bei Amazon mit SimpleDB, Partition D mit FTP auf Server SD usw. Dafür müssten nur Store-Adapter geschrieben werden. Das sind heute jeweils 3 Methoden.

Weiter bei Teil II der Antwort...

Ralf Westphal - One Man Think Tank hat gesagt…

Teil II:

3) Den Cache habe ich nur für die Extents und damit für Queries eingebaut. Da hab ich zwei Fliegen mit einer Klappe geschlagen. Zugriff über inhaltliche Abfrage sehe ich als ein Muss an.

Für eine inhaltliche Abfrage muss ich aber auch den Typ kennen. Also bietet The Lounge Repository Extents (d.h. nach Typen zusammengefasste Daten) und nicht Partitionen als Basis für Listen.

Die Einschränkung der Extents nach Partitionen wäre aber auch kein Problem. Das liegt recht nahe, z.B. repo.GetExtent< Person >("FilialeB").Where(...)

Umgekehrt ist auch die Listung von Entities nach Partitionen kein Problem, würde jedoch nicht die Typgleichheit garantieren. Darüber wären nur schwer Abfragen zu fahren. Deshalb hab ich das erstmal gelassen.

4) Revisions habe ich ganz bewusst ausgelassen. Eine Möglichkeit dafür bietet eine transparente Replikation z.B. mit Dropbox. Dropbox verwaltet Revisionen - allerdings kann ich darauf nicht von Lounge Repo zugreifen.

Wenn ich Revisionen jedoch "richtig" einbauen will, dann habe ich beim Lounge Repository ein Problem. StupidDB legt alles in einer Datei ab. Die kann leicht komplett versioniert werden. Aber The Lounge Repository zerlegt die Daten in mehrere Dateien. Wenn ich Entity A speichere, die auf Entity B verweist, und A hat sich geändert, soll ich dann für A und B neue Versionen anlegen? Oder nur für A?

Im Augenblick - und das scheint mir die größere Frage zu sein - schaut The Lounge Repository ja auch noch gar nicht darauf, ob sich eine Entity überhaupt verändert hat. Ein Objektgraph wird immer komplett gespeichert. Ist das wirklich gut? Nein, auf die Dauer darf das so nicht bleiben. Ein change tracking muss her oder zumindest eine Info, ob eine Entity irgendwie "dirty" ist.

Revisionen "ganz einfach", sind natürlich auch ganz einfach. Für eine Entity sich ein "Backup" zu wünschen, ist schnell eingebaut. Aber ist es das, was man für Entities will? Hm... da möchte ich nicht so tief in die Glaskugel schauen. YAGNI lauert überall ;-)

Bottom line: Klar, es ist zu sehen, dass StupidDB einen spezielleren Ausgangspunkt hatte. Ihr habt für ein konkretes Problem eine simple Lösung gesucht.

Das hat The Lounge Repository aus einem anderen Blickwinkel aufgenommen. Deshalb ist manches gleich (und damit anders als bei CounchDB) und manches eben auch anders. StupidDB gibt es ja schon; ich wollte sie ja nicht nochmal entwickeln :-) Mein Problem ist allerdings ein allgemeineres - das ich ebenfalls und inspiriert durch CouchDB und StupidDB versuche, ganz einfach anzugehen.

Herausgenommen habe ich mir dabei ganz bewusst vor allem nur eines: die Annahme, dass die Daten in den Hauptspeicher passen. Das wird auch erstmal so bleiben, glaub ich. Für das Laden/Speichern/Löschen einzelner Entities mit ihren drunterhängenden Bäumen ist das nicht wichtig. Aber für Queries. Denn für die möchte ich nicht in absehbarer Zeit eine Queryengine schreiben, die auf persistenten Daten läuft. Um das Programmiermodell für Entities mit einem schemalosen Store auszuprobieren, wäre das Overkill, glaube ich.

Soviel mal als Antwort und Hintergrundinfo. Vielleicht sehen wir uns ja auf den .NET Open Space in Leipzig. Dann plaudern wir dort ausführlicher oder machen gar eine Session zum Thema schemalose Datenbanken ;-)

-Ralf

Stefan Koelle hat gesagt…

Hallo, Ralf,

ich sehe dein LoungeRepo auch sicher nicht als Kritik, sondern eine Basis zum weiterdiskutieren.

Ich freu mich schon auf Leipzig, da koennen wir ja die Diskussion von hier ja evtl. fortfuehren.

Kurz aber noch zu den Partitionen. Mit User sind WebBenutzer in grosser Zahl gemeint, bei uns > 0,5 Mio. Pro User legen wir eine Partition an und dort Informationen, Objekte und Files zum User. Das verschachteln des Dateisystems berechnen wir durch die Zahl der Partitionen. Die Spruenge sind da gewaltig, daher muss man nicht zuviel ueberlegen. Die Spruenge sind 256 Partitionen, 65536 Partitionen, 16 Mio Partitionen, usw.

Ich finde es jedenfalls spannend mal eine alternative Implementierung in .NET zu sehen.

Gruesse
Stefan

Ralf Westphal - One Man Think Tank hat gesagt…

@Stefan: Aha, so ist das mit den Partitionen gedacht. Da verstehen wir schon Unterschiedliches drunter. Dann hat StupidDB einen große Zahl von Partitionen, die durch die Nutzerzahl bestimmt ist. Beim Lounge Repository bestimmt sie eher die Problemdomäne - oder irgendein beliebiger anderer Grund. Partitionen können hier auch nach Gusto geschachtelt werden.

Dann warten wir mal Leipzig ab...

Cheerio!

Ralf

Thomy hat gesagt…

Nur eine Idee: Wenn man das darueberliegende Programmiermodell (ganz schicker Ansatz) von der Persistenz entkoppelt werden wuerde, koennte das ganze meines Erachtens auch auf dem Entlib Caching Block laufen und so auch auf distributed Caches wie Velocity (http://tinyurl.com/yw55tm).

Dann waeren auch (vermutete) Probleme wie concurrent Zugriff auf das Repository von mehreren Prozessen in dieser Schicht geloest.

Ralf Westphal - One Man Think Tank hat gesagt…

@Thomy: Das ist ein interessanter Gedanke! Entitätsgraphen müssten zwar immer noch traversiert werden, um die Entitäten zu finden und separat in den Cache zu hängen. Aber die Persistenz (oder auch Replikation) wäre an die Cache-Infrastruktur delegiert.

Darüber werd ich mal nachdenken... Danke für den Input!

Ronald Schlenker hat gesagt…

Sehr interessant. Wir verfolgen mit dem Object Lounge Framework einen ähnlichen Ansatz. Das Ziel ist, Domänenmodelle mit möglichst wenig Aufwand zu persistieren, d.h. Wegfall von Mappings / Schematagenerierung, etc, jedoch mit den von RDBMS gewohnten Features wie Isolation, Transaktionen, usw. Ich muss gestehen, dass ich diesen Post nur überflogen habe und werde ihn mir demnächst genauer anschauen, denn es ist bestimmt sehr interessant, die beiden Ansätze gegenüberzustellen. Jedenfalls schön zu sehen, dass andere Leute auf ähnliche Ideen kommen :)

Ronald

Ralf Westphal - One Man Think Tank hat gesagt…

@Ronald: Die ObjectLounge sieht cool aus. Das Tutorial-Beispiel ist vielleicht ein wenig groß geraten, weil alles in einem Modell dargestellt werden sollte - aber die Features sind cool. Transaktionalität kommt gut.

Über das Lounge Repository habe ich inzw. in der dotnetpro 3/2010 geschrieben. Und 4/2010 wird noch ein wenig unter die Haube schauen.

Aber ich hänge nicht am Lounge Repo. Ich freue mich über jede Initiative, die gleiche Einfachheit mit etwas mehr Leistung bringt. Und Transaktionalität ist sicher etwas, das ich nicht einbauen werde. Dafür fehlt mir dann doch die Zeit.

-Ralf

Ronald Schlenker hat gesagt…

Danke für die Kritik am Tutorial; ja, um sich einen groben Überblick zu schaffen erschlägt es einen vielleicht etwas, das muss ich mal ändern. Wie vieles andere ist Schreiben eben auch die Kunst der Kürze :)

Wären denn Transaktionen beim Lounge Repository überhaupt notwendig? Für mich sieht es so aus, als spiele sich die Interaktion in relativ isoloerten Statements ab; d.h. es wird doch ohnehin kein Entity-bezogener State über einen längeren Zeitraum gehalten, und die Entities sind ja mehr oder weniger disconnected, wenn sie einmal geladen sind. Somit fällt die Notwendigkeit, Rollbacks auf bereits instanziierten Entities machen zu müssen meines Erachtens weg, da sie im Store ja unangetastet sind und sie sich im Bedarfsfall wieder "unangetastst" holen lassen. Lediglich atomicity hinsichtlich des Store müsste gewährleistett sein. Korrigiere mich, wenn ich falsch liege...

Ralf Westphal - One Man Think Tank hat gesagt…

@Ronald: Hm... sind die Entities im Lounge Repo "disconnected"? Würd ich nicht sagen. Im Gegenteil! Die dürfen ja eben connected sein und werden trotzdem separat gespeichert.

Atomizität ist natürlich kein Konzept für das Lounge Repo, weil es keine Tx unterstützt. An einer Entity kann man in sovielen Schritten wie gewünscht herumändern. Stürzt das Programm vor einem Store() ab, dann sind die Änderungen weg. Und stürzt es beim Store() ab, dann ist Atomizität auch nicht gewährleistet. Oder doch... hm... das Store() ist am Ende ja sehr simpel. Zumindest pro Entity ist das Speichern atomisch. Aber nicht für einen Objektgraphen.

Wie gesagt: Ich kann beim Lounge Repo ohne Tx leben. Es ist eben für "die kleine Persistenz zwischendurch" gedacht. Mit noch ein Stück weniger Aufwand als bei der ObjectLounge.

So kann sich jeder seine Persistenz schneidern, wie er will: StupidDB, Lounge Repo, Object Repo, Persistor.Net, O/R Mapper...

-Ralf

Thinker hat gesagt…

Hallo Ralf,

Was passiert, wenn ich Produkte lösche, die bereits Bestellungen zugeordnet sind?

Gibts da sowas wie ein OnDelete Cascade ?

Oder muss ich dann jede Bestellung durchsuchen nach genau diesem Produkt und dies Produkt rauslöschen?

Ralf Westphal - One Man Think Tank hat gesagt…

@Thinker: Nix Event. Das Lounge Repo ist eine gaaanz einfache Persistenzbibliothek. Konsistenz musst du selbst herstellen.

-Ralf

Anonym hat gesagt…

Hallo Ralf.
Ich teste derzeit Dein Repository bei einem privaten PRojekt von mir. In diesem Zusammenhang bin ich darüber gestolpert, daß die Daten der Base Klasse meiner zu speichernden Klasse nicht serialisiert werden. Sprich wenn ich diese wieder lade, sie die Werte der Base Klasse nicht da.
Wo liegt der Fehler ?
Wenn ich meine Klasse direkt Binär serialisiere geht alles.

Grüße Gregor

Ralf Westphal - One Man Think Tank hat gesagt…

@Gregor: Derzeit ist es so, dass private Felder in Basisklassen nicht (de)normalisiert werden vom Lounge Repo.

Reflection liefert via abgeleiteter Klasse diese Felder nicht einfach zurück.

Werde mir gelegentlich mal anschauen, was ich dagegen machen kann, wie aufwändig eine Lösung ist.

-Ralf