Blog

Domänenorientiertes Design Teil 3 - Modelle lesen

Aktualisiert Oktober 22, 2025
7 Minuten

Ich vermute, einige Leser werden sagen : "Hey, Lesemodelle sind nicht Teil von DDD!". Sie werden nur teilweise Recht haben. Eric Evans erwähnt tatsächlich die Verwendung von Abfragen, die eine Zusammenfassung einiger Berechnungen zurückgeben.

Teil 1. Teil 2.

Lesen Sie Models in the Wild

Das Problem bei der üblichen Herangehensweise an Lesemodelle (die manchmal auch anders genannt werden) ist, dass sie in der Regel dasselbe physische und oft auch konzeptionelle Modell wie die eigentliche Domäne haben. In einem stark partitionierten System mit vielen Arten von Aggregatwurzeln, die möglicherweise getrennt in mehreren Bounded Contexts leben, kann dies ernsthafte Kopfschmerzen verursachen.

In einem typischen SQL-gestützten Datenmodell führt der Königsweg dazu, viele Navigationseigenschaften für Entitäten zu erstellen, mit denen sie für Abfragen miteinander verbunden werden. Dies ist in C# mit sprachintegrierten Abfragen (LINQ) besonders einfach. Haben Sie schon einmal ähnlichen Code gesehen?

var ticketsLastMonth = from projekt in Context.Projekte
  wo Projekt.IstAktiv
  from ticket in Projekt.Tickets
  wo ticket.StartDatum  >= startOfMonth
  gruppieren Sie ticket nach ticket.ZugewiesenePerson in g
  wählen Sie ein neues TicketViewModel {  
  Titel = ticket.Titel,
  ZugewiesenePerson = ticket.ZugewiesenePerson.Name + " " + ticket.ZugewiesenePerson.Nachname,
  Manager = ticket.AssignedPerson.Manager.Name + " " + ticket.AssignedPerson.Manager.LastName,
  Status = Context.StatusTranslations.FirstOrDefault(t =>  t.Id == ticket.Status).Übersetzung
  };

Diese Abfrage erzeugt mehrere Verknüpfungen und/oder Unterabfragen, und sie ist auch nicht besonders komplex. Komplexe Bildschirme erfordern oft bis zu 10 verbundene Tabellen. Noch schlimmer wird es, wenn anstelle eines spezialisierten Typs wie der obigen TicketViewModel tatsächliche Entitäten zurückgegeben werden (nun, sie können trotzdem innerhalb eines Ansichtsmodells zurückgegeben werden). Kombinieren Sie das mit aktiviertem Lazy Loading und Sie haben gerade erst begonnen, einen Weg einzuschlagen, der zu ernsthaften Leistungsproblemen führt.

Außerdem ist es trivial einfach, solche Abfragen über mehrere Lesemodelle hinweg wiederzuverwenden. Das ist der Punkt, an dem dieser Ansatz zu einem Alptraum bei der Wartung werden kann - wenn Sie ein Lesemodell ändern, können mehrere andere kaputt gehen. Und selbst wenn sie nicht kaputt gehen, verhalten sie sich möglicherweise anders als erwartet.

Geben Sie es zu, wir haben alle schon solchen Code geschrieben. Aber wir können es besser!

Halten Sie sie getrennt - CQRS als Retter in der Not

Ein erster Schritt besteht darin, das Domänenmodell und das Lesemodell auf konzeptioneller Ebene zu trennen. Damit meine ich, dass eine Abfrage nicht dasselbe Datenmodell verwenden sollte, das für die Speicherung der Entitäten verwendet wird. Der einfachste Ansatz besteht darin, die obige LINQ-Abfrage durch eine spezielle SQL-Abfrage zu ersetzen, die möglicherweise als Ansicht gekapselt ist. Eine solche Abfrage oder Ansicht wäre über ein separates Datenbankobjekt zugänglich - / von Entity Framework, von NHibernate oder ähnlich. Auf diese Weise ist es nicht möglich, dass Domänenentitäten aus der Abfrage durchsickern.

Ich persönlich schlage vor, Lesemodelle auch in ihren eigenen Bibliotheken getrennt zu halten. Auf diese Weise wird es nicht möglich sein, Abfragen zwischen Lesemodellen wiederzuverwenden. Es sei darauf hingewiesen, dass diese Vorschläge weder exklusiv noch inklusiv sind. Der Mittelweg besteht darin, das Datenbankmodell zwischen den Domänen- und Lesemodellen gemeinsam zu nutzen, es aber nicht direkt als Domänenentität zu verwenden. Werfen Sie einen Blick auf diesen Beitrag mit dem Titel Domänenmodelle vs. Persistenzmodelle, um eine ausführlichere Diskussion zu führen.

Leider hat der Ansatz, reines SQL über ein gemeinsam genutztes relationales Modell auszuführen, einen großen Nachteil: Er lässt die Feinheiten von LINQ weg. Änderungen an der Datenbank sind nur zur Laufzeit erkennbar und es kann für einige Entwickler schwieriger sein, SQL-Code anstelle von C# zu schreiben.

Last but not Least: Event Sourcing

Domänenereignisse können nicht nur verwendet werden, um den Zustand von Entitäten zu speichern, sondern auch, um die Lesemodelle inkrementell zu erstellen. Sie werden übrigens oft als Projektionen bezeichnet. Diese Verwendung von Domänenereignissen hat große Auswirkungen. Zunächst einmal kann jede Abfrage eine einfache select aus nur einer Tabelle sein. Projektionen sind in der Regel hoch spezialisiert und denormalisiert, was zu blitzschnellen Lesevorgängen beiträgt. Da sie sich in völlig separaten Tabellen/Datenbanken befinden, können solche Lesemodelle außerdem selektiv dupliziert und die Last ausgeglichen werden.

Wie erstellen Sie solche Lesemodelle? Es ist ganz einfach: Sie warten auf Domain Events und aktualisieren das Lesemodell entsprechend. Die Erstellung einer sehr einfachen Summe offener Tickets pro Projekt würde dadurch erfolgen, dass der Ticketzähler des Projekts immer dann erhöht wird, wenn ein hypothetisches TicketOpenedEvent eingetreten ist, und der Zähler nach TicketOpenedClosedEvent dekrementiert wird. Theoretisch ist das ganz einfach, aber es muss eine gewisse Infrastruktur vorhanden sein, um dies zu ermöglichen.

Auf diese Weise erstellte Lesemodelle können auch sicher verworfen und von Grund auf neu erstellt werden, wenn sie sich aufgrund eines Fehlers oder zur Aufnahme einer anderen Art von Domänenereignis ändern müssen. Da alle vergangenen Ereignisse für immer gespeichert werden, kann jederzeit ein neues Lesemodell erstellt werden, das rückwirkend alle Ereignisse aus der Vergangenheit enthält.

Beispiel aus der Link-Sharing-Plattform

Ein Teil der PGD.DDD-Lösung ist PGS.DDD.ReadModel, das die grundlegenden Teile zur Erstellung von Lesemodellen enthält. Es enthält die notwendigen Schnittstellen, um Lesemodelle aus Domain Events zu erstellen.

Aufbau von Lesemodellen

In unserem Projekt handelt es sich bei den Lesemodellen im Grunde um SQL Server-Datenbanktabellen, die durch die Verarbeitung von Domänenereignissen schrittweise aufgefüllt werden. Der Kern dieses Prozesses sind die IReadModelBuilder Schnittstellen.

public interface IReadModelBuilder
{
  void Clear();
  void Speichern();
}

public interface IReadModelBuilder : IReadModelBuilder where TDomainEvent : DomainEvent
{
  void ApplyEvent(TDomainEvent domainEvent);
}

Um ein Lesemodell zu erstellen, implementieren wir die Builder-Schnittstelle für jedes relevante Ereignis. Für uns besteht die Implementierung von einfach in der Konvertierung der Eingabe in eine Tabellenzeile. Dies könnte jedoch komplizierter sein, wenn das Lesemodell irgendeine Art von Aggregation durchführen würde, wie z.B. das Summieren und Gruppieren von Ereignissen.

Das Lesemodell nachbilden

Ich habe bereits erwähnt, dass der Vorteil der Verwendung von Event Sourcing zur Erstellung von Lesemodellen darin besteht, dass sie nach Belieben verworfen und von Grund auf neu erstellt werden können. Vielleicht haben Sie die Methode Clear bemerkt. Sie dient dazu, alle Inhalte eines Lesemodells zu löschen, damit relevante Ereignisse aus der Vergangenheit mit den Methoden von verarbeitet werden können. Wir haben eine einfache Methode erstellt, die alles durchläuft:

private static readonly Faulheit<ISet>  _supportedDomainEvents;
private readonly IEventStore _eventStore;
private readonly IReadModelBuilderFactory _factory;

privates Objekt Recreate()
{
  using (var readModel = _factory.Create())
  {
  readModel.Clear();

  foreach (DomainEvent domainEvent in _eventStore.GetEvents())
  {
  if (_supportedDomainEvents.Value.Contains(domainEvent.GetType()))
  {
  ((dynamic) readModel).ApplyEvent((dynamic) domainEvent);
  }
  }

  readModel.Save();
  }
}

Es ist so einfach wie die Iteration über jedes einzelne Ereignis aus dem Ereignisspeicher und die Auswahl von Ereignissen, die von dem angegebenen IReadModelBuilder Typ unterstützt werden. Mit ein wenig Reflexion und einem dynamischen Methodenaufruf sind dafür nur ein paar Zeilen Code erforderlich.

Zusammenfassung

Der hier beschriebene Ansatz ist sehr eigenwillig und hängt von der Verwendung von Event Sourcing ab. Anstelle eines Servicebusses wäre es auch möglich, regelmäßig einen ATOM-Feed mit Ereignissen abzufragen und die seit der letzten Prüfung eingetretenen Ereignisse zu übernehmen.

Andererseits bietet ein solcher Ansatz unübertroffene Möglichkeiten, Lesemodelle rückwirkend zu erstellen. Ein neues Lesemodell kann jederzeit während der Lebensdauer der Anwendung implementiert werden und vergangene Daten einbeziehen. Traditionell kann eine neue Funktion dies nicht und funktioniert erst ab dem Zeitpunkt der Bereitstellung. Es ist auch möglich, mehrere Instanzen eines kritischen Lesemodells zu erstellen, so dass ein Lastausgleich möglich ist. Dies geht mit einer möglichen Latenzzeit einher, in der die Ereignisse angewendet werden. Aber von da an ist ein solches Lesemodell einfach eine statische Ansicht Ihrer Daten, die sowohl horizontal als auch vertikal enorm skaliert werden kann. Es muss nicht einmal eine Tabelle sein. Ein Lesemodell kann einfach eine statische, vorberechnete HTML-Datei sein!

Contact

Let’s discuss how we can support your journey.