Datenpunkte

Codierung für Domain-Driven Design: Tipps für Entwickler mit Datenschwerpunkt, Teil 3

Julie Lerman

Julie LermanDies ist die letzte Folge meiner Reihe zur Unterstützung von Entwicklern mit Datenschwerpunkt, die sich komplexeren Codierungskonzepten mithilfe des domänengesteuerten Entwurfs (Domain-Driven Design, DDD) widmen möchten. Als Entwicklerin von Microsoft .NET Framework, die Entity Framework (EF) verwendet, und mit langjähriger Erfahrung in der datenorientierten (und sogar datenbankorientierten) Entwicklung habe ich viele Kämpfe durchfochten, um zu verstehen, wie ich meine Fähigkeiten mit Implementierungstechniken von DDD verbinden kann. Auch wenn ich keine vollständige DDD-Implementierung (von der Clientinteraktion bis hin zum Code) in einem Projekt verwende, profitiere ich umfassend von vielen der DDD-Tools.

In dieser letzten Folge bespreche ich zwei wichtige technische Muster der DDD-Codierung sowie ihre Anwendung auf das von mir verwendete objektrelationale Zuordnungstool (Object-Relational Mapping, ORM), und zwar Entity Framework. In einer vorherigen Folge habe ich 1:1-Beziehungen erläutert. In diesem Artikel möchte ich die von DDD bevorzugten unidirektionalen Beziehungen und ihre Auswirkungen auf Ihre Anwendung thematisieren. Diese Auswahl bedingt eine schwierige Entscheidung: Sie müssen erkennen, wann es sinnvoll ist, einen Teil des wunderbaren „Beziehungszaubers“ von EF ungenutzt zu lassen. Ich werde am Rande auch darauf eingehen, wie wichtig es ist, die Aufgaben zwischen einem Aggregatstamm und einem Repository auszugleichen.

Erstellen von unidirektionalen Beziehungen vom Stamm

Seitdem ich Modelle mit EF erstelle, sind bidirektionale Beziehungen die Norm, und ich bin dieser Norm gefolgt, ohne groß darüber nachzudenken. Die Fähigkeit, in zwei Richtungen navigieren zu können, ist sinnvoll. Im Fall von Bestellungen und Kunden möchten Sie die Bestellungen für einen Kunden anzeigen können, und bei einer Bestellung ist es praktisch, auf die Kundendaten zugreifen zu können. Ohne mir darüber Gedanken zu machen, habe ich auch eine bidirektionale Beziehung zwischen Bestellungen und ihren Positionen erstellt. Eine Beziehung zwischen Bestellung und Positionen erscheint sinnvoll. Wenn Sie hier jedoch innehalten und kurz darüber nachdenken, werden Sie feststellen, dass es tatsächlich selten vorkommt, dass Sie für eine Position zurück zur Bestellung wechseln müssen. Einer der seltenen Fälle, der mir in den Sinn kommt, entsteht bei der Berichterstellung für Produkte und der Analyse, welche Produkte normalerweise zusammen bestellt werden, oder einer Analyse der Kunden- oder Versanddaten. In diesen Fällen müssen Sie möglicherweise von einem Produkt zu den entsprechenden Positionen und dann zurück zur Bestellung navigieren. Diese Situation entsteht nach meiner Einschätzung nur bei einer Berichterstellung, in der ich wahrscheinlich nicht mit DDD-orientierten Objekten arbeiten muss.

Wenn ich nur von Bestellungen zu Positionen navigieren muss, wie kann ich am effektivsten eine solche Beziehung in meinem Modell beschreiben?

Wie oben angemerkt, werden von DDD unidirektionale Beziehungen bevorzugt. Eric Evans weist darauf hin, dass es wichtig sei, Beziehungen soweit wie möglich einzuschränken, und dass das Verstehen der Domäne den natürlichen Richtungsbias offenbaren könne. Das Verwalten der Komplexitäten von Beziehungen ist, besonders bei einer Abhängigkeit von Entity Framework zum Pflegen der Zuordnungen, definitiv ein Bereich, der zu vielen Missverständnissen führen kann. Ich habe bereits viele Artikel zu Datenpunkten verfasst, die sich mit Zuordnungen in Entity Framework befassen. Jede Reduzierung von Komplexität ist sicherlich von Vorteil.

Wenn Sie das einfache Verkaufsmodell betrachten, das ich für diese Reihe zu DDD herangezogen habe, offenbart sich ein Bias von der Bestellung in Richtung der Positionen. Ich kann mir nicht vorstellen, eine Position zu erstellen, zu löschen oder zu bearbeiten, ohne bei der Bestellung zu starten.

In dem früher in dieser Reihe erstellten Order-Aggregat kontrolliert die Bestellung die Positionen. Sie müssen beispielsweise die CreateLineItem-Methode der Order-Klasse verwenden, um eine neue Position hinzuzufügen:

public void CreateLineItem(Product product, int quantity)
{
  var item = new LineItem
  {
    OrderQty = quantity,
    ProductId = product.ProductId,
    UnitPrice = product.ListPrice,
    UnitPriceDiscount = CustomerDiscount + PromoDiscount
  };
  LineItems.Add(item);
}

Der LineItem-Typ besitzt eine OrderId-Eigenschaft, aber keine Order-Eigenschaft. Folglich ist es zwar möglich, den Wert von „OrderId“ festzulegen, aber nicht von „LineItem“ zu einer tatsächlichen Order-Instanz zu navigieren.

In diesem Fall habe ich – in Evans Worten – „eine Traversierungsrichtung auferlegt“. Das heißt, ich habe sichergestellt, dass ich die Richtung von „Order“ zu „LineItem“ durchlaufen kann, aber nicht umgekehrt.

Diese Herangehensweise geht von gewissen Voraussetzungen nicht nur im Modell, sondern auch in der Datenschicht voraus. Ich verwende Entity Framework als mein ORM-Tool, und es erfasst diese Beziehung von der LineItems-Eigenschaft zur Order-Klasse ausreichend. Da ich die EF-Konventionen befolge, erkennt das Tool, dass „LineItem.OrderId“ meine Fremdschlüsseleigenschaft zurück zur Order-Klasse ist. Die Verwendung eines anderen Namens für „OrderId“ würde den Ablauf für Entity Framework komplizierter machen.

In diesem Szenario kann ich jedoch einer bestehenden „order“ (Bestellung) ein neues „LineItem“ folgendermaßen hinzufügen:

order.CreateLineItem(aProductInstance, 2);
var repo = new SimpleOrderRepository();
repo.AddAndUpdateLineItemsForExistingOrder(order);
repo.Save();

Die Bestellungsvariable stellt jetzt ein Diagramm mit einer bereits vorhandenen „order“ und einem einzelnen neuen „LineItem“ dar. Die bereits vorhandene „order“ stammt aus der Datenbank und besitzt in „OrderId“ bereits einen Wert. Das neue „LineItem“ hat jedoch für die OrderId-Eigenschaft nur den Standardwert, und der lautet „0“.

In meiner Repositorymethode wird dem EF-Kontext das order-Diagramm hinzugefügt, und dann wird der korrekte Status angewendet (siehe Abbildung 1).

Abbildung 1: Statusanwendung auf ein Order-Diagramm

public void AddAndUpdateLineItemsForExistingOrder(Order order)
{
_context.Orders.Add(order);
_context.Entry(order).State = EntityState.Unchanged;
foreach (var item in order.LineItems)
{
  // Existing items from database have an Id & are being modified, not added
  if (item.LineItemId > 0)
  {
    _context.Entry(item).State = EntityState.Modified;
  }
}
}

Falls Sie das EF-Verhalten nicht kennen, sollten Sie wissen, dass die Add-Methode den Kontext veranlasst, alles im Diagramm nachzuverfolgen (die Bestellung und die einzelne Position). Gleichzeitig wird jedes Objekt im Diagramm mit dem Status „Added“ (Hinzugefügt) gekennzeichnet. Da bei dieser Methode die Verwendung einer vorhandenen Bestellung im Mittelpunkt steht, weiß ich, dass „Order“ nicht neu ist. Aus diesem Grund legt die Methode den Status der Order-Instanz fest, indem er auf „Unchanged“ (Unverändert) gesetzt wird. Es wird zudem überprüft, ob „LineItems“ vorhanden sind, und ihr Status wird auf „Modified“ (Geändert) gesetzt, damit sie in der Datenbank aktualisiert und nicht als neu eingefügt werden. In einer komplexeren Anwendung würde ich ein Muster verwenden, um den Status für jedes Objekt genauer zu ermitteln. Aus Gründen der Übersichtlichkeit soll dies in diesem Beispiel nicht durchgeführt werden. (Eine frühe Version dieses Musters finden Sie im Blog von Rowan Miller unter bit.ly/1cLoo14. Ein aktualisiertes Beispiel befindet sich in dem von uns mitverfassten Buch „Programming Entity Framework: DbContext“ [O’Reilly Media, 2012].)

Da diese Aktionen ausgeführt werden, während vom Kontext die Objekte nachverfolgt werden, fixiert Entity Framework „wie von Zauberhand“ auch den Wert von „OrderId“ in meiner neuen LineItem-Instanz. Aus diesem Grund erkennt „LineItem“ beim Aufrufen von „Save“ (Speichern), dass der Wert für „OrderId“ „1“ ist.

Aufgeben des EF-Beziehungsverwaltungszaubers – für Updates

Glücklicherweise folgt mein LineItem-Typ der EF-Konvention mit dem Fremdschlüsselnamen. Wenn Sie etwas anderes als „OrderId“ benennen, wie „OrderFK“, müssten Sie an Ihrem Typ ein paar Änderungen vornehmen (zum Beispiel die nicht gewünschte Navigationseigenschaft „Order“ einführen) und dann EF-Zuordnungen angeben. Dies ist nicht erstrebenswert, weil Sie nur Komplexität hinzufügen würden, um ORM zu erfüllen. Gelegentlich kann dies erforderlich sein. Wenn nicht, vermeide ich es lieber.

Einfacher wäre es, jede Abhängigkeit von dem EF-Beziehungszauber aufzugeben und die Einstellung des Fremdschlüssels im Code zu steuern.

Im ersten Schritt wird EF angewiesen, diese Beziehung zu ignorieren, andernfalls sucht EF weiterhin nach dem Fremdschlüssel.

Ich verwende den folgenden Code in der DbContext.OnModelBuilder-Methodenaußerkraftsetzung, damit EF die Beziehung ignoriert:

modelBuilder.Entity<Order>().Ignore(o => o.LineItems);

Jetzt steuere ich die Beziehung. Das bedeutet Umgestaltung. Folglich füge ich dem „LineItem“, das „OrderId“ und andere Werte benötigt, einen Konstruktor hinzu, sodass „LineItem“ eher einer DDD-Entität gleicht. Damit bin ich zufrieden. Zudem muss ich die CreateLineItem-Methode in „Order“ ändern, um den Konstruktor anstatt eines Objektinitialisierers zu verwenden.

Abbildung 2 zeigt eine aktualisierte Version der Repositorymethode.

Abbildung 2: Die Repositorymethode

public void UpdateLineItemsForExistingOrder(Order order)
{
  foreach (var item in order.LineItems)
  {
    if (item.LineItemId > 0)
    {
      _context.Entry(item).State = EntityState.Modified;
    }
    else
    {
      _context.Entry(item).State = EntityState.Added;
      item.SetOrderIdentity(order.OrderId);
    }
  }
}

Beachten Sie, dass ich nicht mehr das order-Diagramm hinzufüge und dann den Status von „order“ auf „Unchanged“ festlege. Da EF die Beziehung nicht kennt, würde es beim Aufruf von „context.Orders.Add(order)“ die Order-Instanz, aber nicht die verwandten Positionen wie zuvor hinzufügen.

Stattdessen durchlaufe ich die Positionen des Diagramms und setze nicht nur den Status vorhandener Positionen auf „Modified“, sondern auch den Status neuer Positionen auf „Added“. Die von mir verwendete DbContext.Entry-Syntax führt zwei Aufgaben aus. Bevor sie den Status festlegt, überprüft sie, ob der Kontext bereits die bestimmte Entität kennt (oder „nachverfolgt“). Falls nicht, wird die Entität intern angehängt. Nun kann sie auf die Tatsache reagieren, dass der Code die Statuseigenschaft festlegt. An diese einzelne Codezeile hänge ich den festgelegten Status von „LineItem“ an.

Mein Code stimmt jetzt mit einer anderen sinnvollen Anweisung zum Verwenden von EF mit DDD überein: Verlassen Sie sich beim Verwalten von Beziehungen nicht auf EF. EF leistet Großartiges und ist in vielen Szenarien ein wahrer Schatz. Davon profitiere ich schon seit Jahren. Sie möchten für DDD-Aggregate natürlich unbedingt die Beziehungen in Ihrem Modell verwalten und sich nicht darauf verlassen, dass die Datenschicht erforderliche Aktionen für Sie ausführt.

Da ich zurzeit ganze Zahlen für meine Schlüssel verwenden muss (zum Beispiel „Order.OrderId“) und darauf angewiesen bin, dass mir meine Datenbank die Werte für die Schlüssel zur Verfügung stellt, muss ich das Repository für neue Aggregate zusätzlich bearbeiten, beispielsweise eine neue Bestellung mit Positionen. Ich brauche eine enge Persistenzkontrolle, damit ich das altmodische Muster des Einfügens von Diagrammen verwenden kann: Bestellung einfügen, neuen, von der Datenbank generierten OrderId-Wert abrufen, ihn auf die neuen Positionen anwenden und diese in der Datenbank speichern. Dies ist erforderlich, weil ich die Beziehung, die normalerweise von EF zum Ausführen dieses Vorgangs verwendet wird, getrennt habe. Im heruntergeladenen Beispiel können Sie meine Implementierung im Repository erkennen.

Nach all den Jahren bin ich endlich bereit, meine Abhängigkeit davon zu kündigen, dass mein Bezeichner von der Datenbank erstellt wird. Nun kann ich anfangen, GUIDs für meine Schlüsselwerte zu verwenden, die ich in meiner App generieren und zuweisen kann. Dadurch kann ich meine Domäne noch weiter von der Datenbank trennen.

Beibehalten des EF-Beziehungsverwaltungszaubers – für Abfragen

Mein Modell ohne EF-Beziehungen war im vorherigen Szenario zum Ausführen von Updates äußerst hilfreich. Ich möchte jedoch nicht alle Beziehungsfeatures von EF verlieren. Zum Beispiel möchte ich nicht das Laden verwandter Daten beim Abfragen von der Datenbank aufgeben. Ob beim Eager Loading, Lazy Loading oder expliziten Laden – ich finde die Fähigkeit von EF, verwandte Daten zu laden, ohne zusätzliche Abfragen ausdrücken und ausführen zu müssen, äußerst nützlich.

An dieser Stelle ist müssen wir unsere Sichtweise des „Separation of Concerns“-Konzepts erweitern. Beim Befolgen von DDD-Grundsätzen für den Entwurf kann es durchaus zu unterschiedlichen Repräsentationen ähnlicher Klassen kommen. Beispielsweise können Sie dies bei einer Customer-Klasse anwenden, die für die Verwendung im Kontext der Kundenverwaltung entworfen wurde – im Gegensatz zu einer Customer-Klasse, die einfach zum Auffüllen einer Auswahlliste entworfen wurde und die nur den Namen und Bezeichner des Kunden benötigt.

Es ist auch sinnvoll, unterschiedliche DbContext-Definitionen zu haben. In Szenarien, in denen Sie Daten abrufen, benötigen Sie möglicherweise einen Kontext, der die Beziehung zwischen „Order“ und „LineItems“ kennt, damit Sie mittels Eager Load eine Bestellung mit ihren Positionen aus der Datenbank laden können. Beim Ausführen von Updates, wie ich es oben beschrieben habe, benötigen Sie dagegen möglicherweise einen Kontext, der die Beziehung explizit ignoriert, sodass Sie eine genauere Kontrolle über Ihre Domäne haben.

Ein CQRS-Muster (Command Query Responsibility Segregation, Zuständigkeitstrennung bei Befehlsabfragen) bietet diesbezüglich eine extreme Ansicht für eine bestimmte Teilmenge komplexer Probleme, die Sie möglicherweise mit Software lösen. Nach der Vorstellung von CQRS handelt es sich beim Datenabruf („reads“) und bei der Datenspeicherung („writes“) um separate Systeme, die möglicherweise unterschiedliche Modelle und Architekturen benötigen. In meinem kleinen Beispiel erhalten Sie eine Vorstellung von den Möglichkeiten von CQRS. Es hebt den Vorteil unterschiedlicher Konzepte von Beziehungen für Datenabrufoperationen als für Datenspeicherungsoperationen hervor. Weitere Informationen über CQRS erhalten Sie in der sehr hilfreichen Ressource „Die CQRS-Reise“, die Sie unter msdn.microsoft.com/library/jj554200 herunterladen können.

Der Datenzugriff findet im Repository, nicht im Aggregatstamm statt

Zum Abschluss möchte ich eine letzte Frage erörtern, die mich quälte, als ich begann, mich auf unidirektionale Beziehungen zu konzentrieren. (Das bedeutet nicht, dass ich keine weiteren Fragen zu DDD habe, ich möchte damit nur sagen, dass es sich um das letzte in dieser Reihe besprochene Thema handelt.) Uns „datenorientierten“ Denkern stellt sich häufig die folgende Frage zu unidirektionalen Beziehungen: Wo genau findet (bei DDD) der Datenzugriff statt?

Bei der ersten Veröffentlichung konnte EF nur mit einer Datenbank funktionieren, wenn ein Reverse-Engineering einer vorhandenen Datenbank durchgeführt wurde. Daher gewöhnte ich mich daran, wie oben beschrieben, dass jede Beziehung bidirektional ist. Wenn die Customer- und Order-Tabellen in der Datenbank eine Primärschlüssel-/Fremdschlüsseleinschränkung hätten, in der eine 1:n-Beziehung beschrieben wäre, so sah ich genau diese 1:n-Beziehung in dem Modell. „Customer“ besaß eine Navigationseigenschaft für eine Sammlung von „orders“. „Order“ besaß eine Navigationseigenschaft für eine Instanz von „Customer“.

Während der Weiterentwicklung zu Model- und Code-First, wo das Modell beschrieben und eine Datenbank generiert werden kann, folgte ich weiterhin dem Muster und definierte an beiden Enden einer Beziehung Navigationseigenschaften. EF war zufrieden, die Zuordnungen waren einfacher, und die Codierung war natürlicher.

So ärgerte es mich, wenn bei DDD ein Order-Aggregatstamm eine „CustomerId“ oder vielleicht sogar einen vollständigen Customer-Typ kannte, ich aber von „Order“ nicht zurück zu „Customer“ navigieren konnte. Als Erstes fragte ich mich: „Was passiert, wenn ich alle Bestellungen für einen Kunden finden muss?“ Ich bin immer davon ausgegangen, dass ich dazu in der Lage sein musste, und war es gewohnt, in beiden Richtungen Zugriff auf die Navigation zu haben.

Wenn die Logik bei meinem Order-Aggregatstamm beginnt, wie kann ich diese Frage jemals beantworten? Ich dachte am Anfang fälschlicherweise auch, dass alle Aufgaben über den Aggregatstamm durchgeführt werden, was nicht gerade half.

Die Lösung traf mich hart, und ich fühlte mich etwas lächerlich. Für den Fall, dass noch andere auf das gleiche Problem stoßen sollten, möchte ich meine Gedanken mit Ihnen teilen. Weder der Aggregatstamm noch „Order“ müssen mir bei der Beantwortung der Frage helfen. In einem Order-orientierten Repository, womit ich meine Abfragen und Persistenz ausführe, gibt es jedoch keinen Grund dafür, dass ich zum Beantworten meiner Frage auf keine Methode zurückgreifen kann.

public List<Order>GetOrdersForCustomer(Customer customer)
  {
    return _context.Orders.
      Where(o => o.CustomerId == customer.Id)
      .ToList();
  }

Die Methode gibt eine Liste von Order-Aggregatstämmen zurück. Wenn die Erstellung im Rahmen von DDD erfolgt, würde ich die Methode nur in mein Repository aufnehmen, wenn sie auf jeden Fall in dem bestimmten Kontext gebraucht wird, nicht einfach „für alle Fälle“. Möglicherweise brauche ich sie jedoch in einer App zur Berichterstellung oder etwas Ähnlichem, jedoch nicht unbedingt in einem Kontext zum Erstellen von Bestellungen.

Nur der Anfang der Suche

Ich habe in den letzten Jahren viel über DDD dazu gelernt. Und bei den Themen, die ich in dieser Reihe besprochen habe, handelt es sich genau um diejenigen, mit denen ich die meisten Probleme hatte. Dabei ging es entweder um das Verstehen oder das Ermitteln der Vorgehensweise beim Implementieren, wenn Entity Framework Teil meiner Datenschicht ist. Ein Teil meiner Frustration war darauf zurückzuführen, dass ich jahrelang meine Software aus der Perspektive der Funktionsweise in meiner Datenbank betrachtete. Nachdem ich mich von dieser Sichtweise befreit hatte, konnte ich mich auf das wirkliche Problem konzentrieren: das Domänenproblem, für das ich Software entwerfe. Gleichzeitig muss ich ein gesundes Gleichgewicht finden, weil ich auf Datenschichtprobleme stoßen kann, wenn es an der Zeit ist, dies in meine Lösung aufzunehmen.

Während ich mich darauf konzentriert habe, was passieren könnte, wenn ich meine Klassen direkt zur Datenbank mit Entity Framework zurück zuordne, darf ich nicht vergessen, dass es zwischen der Domänenlogik und der Datenbank eine (oder mehrere) zusätzliche Schichten geben kann. Beispielsweise besitzen Sie möglicherweise einen Dienst, mit dem Ihre Domänenlogik interagiert. An dieser Stelle hat die Datenschicht wenige (oder keine) Auswirkungen auf das Zuordnen von Ihrer Domänenlogik. Das Problem liegt nun beim Dienst.

Für Ihre Softwarelösungen stehen mehrere Herangehensweisen zur Verfügung. Selbst wenn ich keinen vollständigen End-to-End-DDD-Ansatz implementiere (dies erfordert besondere Kenntnisse), profitiert mein gesamter Prozess weiterhin von den Lektionen und Techniken, die ich aus DDD gelernt habe.

Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin und lebt in den Bergen von Vermont. Sie hält weltweit in Benutzergruppen und bei Konferenzen Vorträge zum Thema „Datenzugriff“ und zu anderen Microsoft .NET Framework-Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von „Programming Entity Framework“ (2010) sowie der Ausgaben „Code First“ (2011) und „DbContext“ (2012). Alle Ausgaben sind im Verlag O’Reilly Media erschienen. Folgen Sie ihr auf Twitter unter twitter.com/julielerman, und besuchen Sie ihre Pluralsight-Kurse unter juliel.me/PS-Videos.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Stephen Bohlen (Microsoft)