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

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 Client1) 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: 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
  {
    // 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(ServiceActionAdded(id, employeeId, start, end), 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 sein2). Man kann jetzt zu jedem Ereignis beliebig viele Secondärindizes prüfen. Ein Sekundärindex kann z.B. die IndexID 'Terminkalender-rtavassoli@gmail.com' habren. 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 werden3). 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 werden4). 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 wird5).

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 gestellt6), 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.

Der Client kann jetzt einfach einen Zeitdatensatz erstellen7) 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ützt8). 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.

1) Domain Service
2) einer Schritte sind ja per Definition lückenlos
3) im Fall von oben ist das der calendar
4) Wie das funktioniert, sehen wir gleich
5) i.d.R. wird es aber zu jedem Ereignis, das den Index betrifft, genau eine Index-Version geben
6) Mit optimistic locking
7) oder umterminieren, dass würde intern ähnlich ablaufen
8) consistency boundary
technology/domainmodel/secondaryindex.1359378919.txt.gz · Last modified: 2013/01/28 14:15 by rtavassoli