User Tools

Site Tools


technology:domainmodel:secondaryindex

This is an old revision of the document!


Sekundärindex - Secondary Index

In DDD heißt es, es gäbe keine secondary indices. Ich behaupte, dass die explizite Verwendung von Zweitindizes eine pragmatische Abweichung von der reinen Anwendung von DDD ist. Durch Zweitindizes kann man den komplizierten Einsatz von Domain Services, SAGAs, Reservation Patterns und Compensating Actions umgehen. Man sollte damit natürlich vorsichtig sein, wobei der übermäßige Einsatz auch nicht schadet, weil ein Zweitindex immer relativ einfach eben durch einen anderen Prozess ersetzt werden kann, der Eventual Consistency verwendet, bzw. sofortige Konsistenz mit dem Reservation Pattern.

Wenn man sich relativ sicher sein kann, dass ein Aggregte immer gemeinsam mit dem Zweitindex in derselben Partition liegen wird, dann sollte der Einsatz eines Zweit- oder auch Drittindexes auch eine langfristige Lösung darstellen. Die Umsetzung ist relativ einfach - ein paar technische Voraussetzungen müssen eingebaut werden. Dann können Zweitindizes sogar so gebaut werden, dass sie sehr gut in DDD passen, denn dann sind sie nichts anderes als weitere Zweit- und Dritt-Aggregates, die die Ereignisse von anderen Aggregates verwenden.

Beispiel Zeitdatensätze

Angenommen die Regeln1) sind folgende

  • Zeitdatensätze dürfen sich nicht überschneiden
  • Wenn der Status des Zeitdatensatzes nicht editierbar ist, darf der Zeitdatensatz nicht verändert werden.

Die erste Regel umfasst mehrere Zeitdatensätze, die Zweite einen einzelnen. Wenn ein Zeitdatensatz noch mehr Verhalten hat als lediglich terminiert zu werden, möchte man ihn auch als Aggregate haben. Für die erste Regel braucht man aber ein Kalender Aggregate, ansonsten könnte man sie nicht einhalten. Also liegt folgende Lösung nahe:

Umsetzung als 2 Aggregate Typen

Es gibt den Zeitdatensatz und man kann ihn im Kalender eintragen/terminieren. Der Zeitdatensatz kann seine eigenen Regeln einhalten, der Kalender die Regel, dass es keine Zeitüberschneidungen geben darf. Aber irgendwie scheint diese Trennung recht künstlich. Wenn der Anwender einen Zeitdatensatz anlegt, und dann explizit im Kalender einträgt, wäre die Trennung auch für den Anwender verständlich. Trotzdem gibt es mehrere Probleme, u.a.

  • Die zweite Regel kann nicht eingehalten werden. Der Status gehört zum Zeitdatensatz. Ein Zeitdatensatz, der gesperrt wurde, darf nicht neu terminiert werden. Das kann nicht konsistent im Kalender geprüft werden. Man könnte den Zeitdatensatz2) zwar vor einer Neuterminierung fragen, ob das in Ordnung ist, das ist aber keine konsistente Prüfung. Man kann sie künstlich konsistent machen indem man in Aggregates einbaut, dass sich andere an sie anhängen können, eine connect oder eine lock Methode, und die Änderungen transaktional speichern, z.B. über einen Domain Service.
  • Der Zeitdatensatz ist für den Anwender nur schließlich konsistent. Da die Ereignisse aus unterschiedlichen Aggregates nicht zwingend in der Reihenfolge behandelt werden, könnte ein Datensatz in einem gesperrten Status mit einer alten Terminierung angezeigt werden. Das wäre irreführend.
  • Wenn der Projektleiter einen Zeitdatensatz genehmigen möchte, tut er das für den gesamten Datensatz, inklusive aller Eigenschaften und der Terminierung. Man könnte das hinter den Kulissen lösen, indem man auf beide Aggregates dieselbe Statusänderung anwendet - aber auch das scheint wieder mal eine rein künstliche und zu komplizierte Lösung für ein einfaches Problem3).
  • Zusammengefasst gehört die Terminierung fest zum Zeitdatensatz, sie ist ein Hauptbestandteil des Datensatzes. Also wäre die Trennung nicht die korrekte Lösung.

Umsetzung als ein großes Aggregate

In diesem Fall ist der Kalender das Aggregate, und die Zeitdatensätze sind Entitäten innerhalb des Kalenders. Das löst alle logischen Probleme:

  • Alles, was zum Zeitdatensatz gehört, steckt im Zeitdatensatz drin,
  • Eine Sicht auf alle Zeitdatensätze enthält niemals eine Überschneidung,
  • Die Regeln können problemlos eingehalten werden.

Es gibt aber technische Probleme:

  • Der Kalender umschließt alle Zeitdatensätze des Mitarbeiters, vom ersten bis zum letzten Tag seiner Beschäftigung. Das führt dazu, dass
    • jeder Methodenaufruf in einem einzelnen Zeitdatensatz immer das Laden des gesamten Kalenders benötigt, was unnötiger Rechenaufwand ist und nach einiger Zeit langsam werden kann,
    • die Methodenaufrufe in zwei getrennten Zeitdatensätzen sich in die Quere kommen können, auch wenn sie gar nichts miteinander zu tun haben, weil sie die Terminierung gar nicht betreffen. Der Projektleiter erhält dann u.U. eine ConcurrencyException wenn er einen Zeitdatensatz von letzter Woche genehmigen möchte, weil der Mitarbeiter gerade einen neuen Zeitdatensatz erfasst hat.

Diese Lösung löst also alle logischen Probleme, führt aber andere Probleme ein, die das Arbeiten mit dem System zu restriktiv machen4). Es muss also eine andere Lösung her.

Lösung als einzelnes Aggregate mit Sekundärindex

Nochmal zusammengefasst, was wir bisher haben:

Es wurde beschrieben, wie man einen Domain Service verwenden kann, um sicher zu stellen, dass sich Zeitdatensätze nicht überschneiden. Der Terminkalender wird als zweites Aggregate umgesetzt und direkt vom Client5) verwendet. Ich meine aber, dass der Client nur mit dem Zeitdatensatz arbeiten möchte. Der Zeitdatensatz wird neu angelegt, im Kalender verschoben, usw. Dafür den Kalender explizit einzuführen ist zwar eine sinnvolle Lösung, der Client muss dabei aber wissen, dass man einen Termin im Kalender erst verschieben kann, nachdem der Termin darauf geprüft wurde, ob er editierbar ist. D.h. der Terminkalender ist nichts anderes als ein Mittel, um Zeitüberschneidungen zu verhindern, die restliche Geschäftslogik ist vollständig im Zeitdatensatz verankert. Bildlich sieht das aus wie folgt:

Die pragmatische Lösung

Um das Programmiermodell für den Client zu vereinfachen, könnte man den Terminkalender hinter dem Zeitdatensatz verstecken. Der Client arbeitet nur mit Zeitdatensätzen, und diese verwenden den Terminkalender um auf Zeitüberschneidungen zu prüfen - transparent, und somit einfach, für den Client. Der Zeitdatensatz könnte wie folgt aussehen:

public class ServiceAction: AggregateRoot
{
  private ISecondaryIndexRepository<EmployeeCalendar> EmployeeCalendarRepository;
 
  public ServiceAction(Guid id, Guid employeeId, DateTime start, DateTime end) // Constructor for adding new ServiceAction
  {
    // vorher vielleicht noch ein paar prüfungen
    var change = ServiceActionAdded(id, employeeId, start, end);    
    var calendar = EmployeeCalendarRepository.GetOrCreate(employeeId);
    calendar.Validate(change); // hier unternimmt der calender = secondary index, eigene prüfungen
    ApplyChange(change), new List<IndexVersion>() { calendar.GetVersion() });
  }
}

Das ist ein bisschen erklärungsbedürftig. Es gibt bekanntermaßen den EventStore:

EventStore
  * ID: Int32
  * AggregateID: Guid (besser string wegen natürlichen IDs - für Später)
  * Version: Int32
  * Data: nvarchar(max)

Neu dazu gekommen ist der SecondaryIndexStore:

SecondaryIndexStore
  * ID: Int32
  * IndexID: string
  * Version: Int32
  * EventStoreID: Int32 (FK to EventStore)

Der SecondaryIndexStore hat einen Primärschlüssel der sich aus der IndexID und der Version zusammen setzt. Wie im EventStore muss die Version zu einer IndexID bei 1 anfangen, in einer Schritten aufsteigen sein, und dabei lückenlos sein6). Man kann jetzt zu jedem Ereignis beliebig viele Sekundärindizes prüfen. Ein Sekundärindex kann z.B. die IndexID 'Terminkalender-rtavassoli@gmail.com' haben. Alle Aggregates, die diesen Index prüfen müssen, können den Domänenereignissen einfach Index Ereignisse mit geben. Es können sogar Aggregates unterschiedlichster Art denselben Index verwenden.

Der Methode ApplyChange() im Aggregate kann somit ein SecondaryIndex mit gegeben werden7). Der SecondaryIndex enthält nach einem Methodenaufruf einfach eine ID und eine Version, aber keine Daten. Die Idee eines Sekundärindizes ist die, dass die Daten aus den Ereignissen der Aggregates verwendet werden8). Wenn der Sekundärindex eigene Daten hätte, wäre es ein vollwertiges Aggregate. Pro Domain Event darf maximal ein SecondaryIndex (ID|Version) gesetzt werden, anders herum kann aber ein SecondaryIndex (ID|Version) mehrere Domain Events umfassen, z.B. wenn der Index nach mehrerern Änderungen geprüft wird9).

Das Repository erkennt beim Speichern, ob an einem Domain Event weitere Ereignisse eines Sekundärindizes hängen. Wenn ja, werden die Sekundärindex Ereignisse zu dem Domain Event gespeichert. So wird 100% sicher gestellt10), dass der Sekundärindex eingehalten wird, denn wenn es eine concurrency violation gibt, dann haben mehrere Aggregates gleichzeitig Änderungen vorgenommen, die den Index betreffen. Wie bei concurrency violations für Aggregates kann die Aktion in dem Fall einfach wiederholt werden.

Das Repository eines Sekundärindizes lädt die Ereignisse, die es für den Index braucht, ganz einfach über

SELECT EventStore.DATA, SecondaryIndexStore.Version FROM EventStore INNER JOIN SecondaryIndexStore ON EventStore.ID = SecondaryIndexStore.EventStoreID WHERE SecondaryIndexStore.IndexID = @IndexID ORDER BY SecondaryIndexStore.Version ASC

Die Aggregates, die den Sekundärindex verwenden, sind dafür verantwortlich, alle Ereignisse, die den Index betreffen, mit SecondaryIndexEvents zu kennzeichnen, ansonsten würden sie nicht mit einbezogen werden. Das heißt

Der Sekundärindex gehört logisch zu den Aggregates, die ihn verwenden. Die Entwicklung der Aggregates und der Indizes muss somit von einem Team und in einem C# Projekt11) passieren. Die Speicherung der Aggregates muss transaktional möglich sein - wenn Aggregate-Typ A transaktional mit dem Index gespeichert wird, und ebenso Aggregate-Typ B, dann auch Aggregate-Typ A zusammen mit Typ B. Alle Aggregates, die den Index verwenden, müssen also immer in ein und derselben Partition liegen.

Der Client kann jetzt einfach einen Zeitdatensatz erstellen12) und über das Repository<ServiceAction> speichern. Der vereinfachte Code für die Neuanlage eines Zeitdatensatzes gegenüber dem Code eines Domain Service sieht dann so aus:

  public void Handle(AddServiceAction command)
  {
    var action_repo = new Repository<ServiceAction>();
    var action = new ServiceAction.Create(command.ID, command.EmployeeId, command.Start, command.End);
    action_repo.Save(action);
  }

Das ist wesentlich einfacher als mit einem Domain Service, und auch erwartungsgemäß. Ich will einen Zeitdatensatz erstellen, welcher für mich eine Entätit ist, mit der ich direkt arbeiten kann. Kein Terminkalender, kein Domain Service, und vor allem keine komplexen Prozesse, die verteilte Entitäten koordinieren müssen.

Auch eine Umterminierung des Zeitdatensatzen ergibt jetzt deutlich mehr Sinn. Die Änderung ist am Zeitdatensatz, also wird die Änderung auch durch den Zeitdatensatz geschützt13). Mit einem Terminkalender Aggregate würde die Änderung nur den Terminkalender betreffen, nicht aber den Zeitdatensatz - und Prüfungen wie z.B. auf den Status des Zeitdatensatzes müssten künstlich mit irgendwelchen Ereignissen wie ServiceActionStatusValidated festgehalten werden.

Warum Sekundärindizes wie kompensierende Aktionen für eine schließliche Konsistenz funktionieren

Es wird ein Index in einer bestimmten Reihenfolge aufgebaut, basierend auf Ereignissen von unterschiedlichen Aggregates, deren Reihenfolge unbestimmt ist. Der Index kann doch gar nicht funktionieren, was, wenn die Reihenfolge der Ereignisse von anderen Beobachtern eine andere ist? Nun, der Index

  • serialisiert Ereignisse für den Index. Die Ereignisse sind auch passiert, d.h. dass der Index wie ein weiteres Aggregate agiert,
  • Die Ereignisse können nun von jemanden anderem durchaus in einer anderen Reihenfolge beobachtet werden, da die Reihenfolge außerhalb der Domäne nur innerhalb eines Aggregates definiert ist. Außerhalb der Domäne stellt der Sekundärindex also auch lediglich eine schließliche Konsistenz sicher. Wenn Termin A von 10:00-12:00 auf 08:00-10:00 verschoben wird, danach Termin B von 12:00-14:00 auf 10:00-12:00, dann bleibt der Sekundärindex erhalten. Wenn jemand anderes aber erst die Verschiebung von Termin B beobachtet, sieht er eine Zeitüberschneidung der beiden Termine von 10:00-12:00 bis er auch die Verschiebung von Termin A sieht. D.h. aus seiner Sicht kann es kurzzeitige Zeitüberschneidungen geben.

Wenn das Ergebnis dasselbe ist wie mit Techniken für verteilte Zeitdatensätze14), die Umsetzung aber einfacher ist, wird die einfachere Variante gewählt, in dem Wissen, dass die Methoden und Techniken bekannt sind, das Ganze auf eine Verteilte Umgebung zu ändern.

Das Ergebnis ist aber stärker als bei verteilter schließlicher Konsistenz, weil in dem Beispiel die Änderung von Termin A nur noch nicht beobachtet wurde, sie hat aber bereits statt gefunden. Man kann somit mit der alten Version von Termin A direkt nichts machen, weil man in der Domäne eine ConcurrencyException erhalten würde.

Und zum Abschluss

Eine schließliche Konsistenz im verteilten Mitarbeiterkalender15) herzustellen ist nicht so ohne. Wie stelle ich sie her? E-Mail an Mitarbeiter, dass es eine Überschneidung gibt? Zeitdatensätze nach einer Neuterminierung automatisch in einen Status wird geprüft versetzen, und dann einen Event-Handler bauen, der die Prüfungen serialisiert, und ihn auf geprüft oder Konflikt setzt - nach der Regel, wer zuerst kam, wird zuerst auf geprüft gesetzt.

Alternativ erlaubt man die Möglichkeit einer Überschneidung ohne speziellen Status, und die nachgelagerten Aktionen prüfen auf Überschneidungen. Z.B. würde das Drucken von Reports, das Erstellen von Monatsabschlüssen und das Abrechnen von Zeiten verhindert werden, wenn es Konflikte gibt. Dann gibt es einfach eine Meldung, und der Mitarbeiter muss den Konflikt lösen bevor die Zeiten weiter verarbeitet werden können.

1) Invariants
2) oder das Query/Read Model
3) wobei sie gar nicht so abwegig ist - kann man sich nochmal Gedanken drüber machen
4) die Einschränkung ist nötig um die Regeln einzuhalten, kann aber zu oft dazu führen, dass der Anwender eine Konfliktmeldung erhält
5) Domain Service
6) einer Schritte sind ja per Definition lückenlos
7) im Fall von oben ist das der calendar
8) Wie das funktioniert, sehen wir gleich
9) i.d.R. wird es aber zu jedem Ereignis, das den Index betrifft, genau eine Index-Version geben
10) Mit optimistic locking
11) am besten in einem Projektordner
12) oder umterminieren, dass würde intern ähnlich ablaufen
13) consistency boundary
14) Schließliche Konsistenz bezüglich Zeitüberschneidungen
15) also ohne Sekundärindex
technology/domainmodel/secondaryindex.1359468632.txt.gz · Last modified: 2013/01/29 15:10 by rtavassoli