Reduzieren von Repositorys auf aggregierte Roots

82

Ich habe derzeit ein Repository für nahezu jede Tabelle in der Datenbank und möchte mich weiter an DDD ausrichten, indem ich sie auf aggregierte Roots reduziere.

Nehmen wir an, ich habe die folgenden Tabellen Userund Phone. Jeder Benutzer kann ein oder mehrere Telefone haben. Ohne den Begriff der aggregierten Wurzel könnte ich so etwas tun:

//assuming I have the userId in session for example and I want to update a phone number
List<Phone> phones = PhoneRepository.GetPhoneNumberByUserId(userId);
phones[0].Number = 911”;
PhoneRepository.Update(phones[0]);

Das Konzept der aggregierten Wurzeln ist auf dem Papier leichter zu verstehen als in der Praxis. Ich werde niemals Telefonnummern haben, die keinem Benutzer gehören. Wäre es also sinnvoll, das PhoneRepository abzuschaffen und telefonbezogene Methoden in das UserRepository aufzunehmen? Angenommen, die Antwort lautet "Ja", dann schreibe ich das vorherige Codebeispiel neu.

Darf ich im UserRepository eine Methode haben, die Telefonnummern zurückgibt? Oder sollte es immer einen Verweis auf einen Benutzer zurückgeben und dann die Beziehung durch den Benutzer durchlaufen, um zu den Telefonnummern zu gelangen:

List<Phone> phones = UserRepository.GetPhoneNumbers(userId);
// Or
User user = UserRepository.GetUserWithPhoneNumbers(userId); //this method will join to Phone

Unabhängig davon, auf welche Weise ich die Telefone erwerbe, gehe ich davon aus, dass ich eines davon geändert habe. Mein begrenztes Verständnis ist, dass Objekte unter dem Stamm über den Stamm aktualisiert werden sollten, was mich zu Auswahl 1 unten führen würde. Obwohl dies mit Entity Framework sehr gut funktioniert, scheint dies äußerst unbeschreiblich zu sein, da ich beim Lesen des Codes keine Ahnung habe, was ich tatsächlich aktualisiere, obwohl Entity Framework geänderte Objekte im Diagramm im Auge behält.

UserRepository.Update(user);
// Or
UserRepository.UpdatePhone(phone);

Schließlich vorausgesetzt , ich mehrere Lookup - Tabellen haben, die nicht wirklich etwas gebunden sind, wie CountryCodes, ColorsCodes, SomethingElseCodes. Ich könnte sie verwenden, um Dropdowns zu füllen oder aus irgendeinem anderen Grund. Sind das eigenständige Repositorys? Können sie zu einer logischen Gruppierung / einem logischen Repository kombiniert werden, z CodesRepository. Oder ist das gegen Best Practices?

e36M3
quelle
2
In der Tat eine sehr gute Frage, dass ich viel mit mir selbst zu kämpfen habe. Scheint einer dieser Kompromisspunkte zu sein, an denen es keine "richtige" Lösung gibt. Obwohl die Antworten, die zum Zeitpunkt des Schreibens verfügbar sind, gut sind und die meisten Probleme abdecken, habe ich nicht das Gefühl, dass sie "endgültige" Lösungen bieten. :(
cwap
Ich höre Sie, es gibt keine Grenze, wie nahe man der "richtigen" Lösung kommen kann. Ich denke, wir müssen unser Bestes geben, bis wir einen besseren Weg lernen :)
e36M3
+1 - Ich habe auch damit zu kämpfen. Vorher hatte ich für jeden Tisch eine separate Repo- und Service-Schicht. Ich fing an, diese zu kombinieren, wo es Sinn machte, aber dann endete ich mit einer Repo- und Service-Schicht mit über 1k Codezeilen. In meinem neuesten Anwendungsbereich habe ich ein wenig gesichert, um nur eng verwandte Konzepte in dieselbe Repo- / Service-Schicht zu integrieren, selbst wenn dieses Element abhängig ist. Beispiel: Für ein Blog habe ich dem Post-Repo-Aggregat Kommentare hinzugefügt, aber jetzt habe ich sie getrennt, um das Repo / den Service für Kommentare zu trennen.
Jpshook

Antworten:

11

Sie dürfen jede gewünschte Methode in Ihrem Repository haben :) In beiden von Ihnen genannten Fällen ist es sinnvoll, den Benutzer mit der ausgefüllten Telefonliste zurückzugeben. Normalerweise wird das Benutzerobjekt nicht vollständig mit allen Unterinformationen (z. B. allen Adressen, Telefonnummern) gefüllt, und wir haben möglicherweise unterschiedliche Methoden, um das Benutzerobjekt mit verschiedenen Arten von Informationen zu füllen. Dies wird als verzögertes Laden bezeichnet.

User GetUserDetailsWithPhones()
{
    // Populate User along with Phones
}

Zum Aktualisieren wird in diesem Fall der Benutzer aktualisiert, nicht die Telefonnummer selbst. Das Speichermodell speichert die Telefone möglicherweise in einer anderen Tabelle. Auf diese Weise denken Sie möglicherweise, dass nur die Telefone aktualisiert werden. Dies ist jedoch aus DDD-Sicht nicht der Fall. Soweit Lesbarkeit betroffen ist, während die Linie

UserRepository.Update(user)

allein vermittelt nicht, was aktualisiert wird, der Code darüber würde deutlich machen, was aktualisiert wird. Außerdem wäre es höchstwahrscheinlich Teil eines Front-End-Methodenaufrufs, der möglicherweise anzeigt, was aktualisiert wird.

Für die Nachschlagetabellen und sogar sonst ist es nützlich, GenericRepository zu haben und dieses zu verwenden. Das benutzerdefinierte Repository kann vom GenericRepository erben.

public class UserRepository : GenericRepository<User>
{
    IEnumerable<User> GetUserByCustomCriteria()
    {
    }

    User GetUserDetailsWithPhones()
    {
        // Populate User along with Phones
    }

    User GetUserDetailsWithAllSubInfo()
    {
        // Populate User along with all sub information e.g. phones, addresses etc.
    }
}

Suchen Sie nach Generic Repository Entity Framework und Sie würden viele nette Implementierung gut. Verwenden Sie eine davon oder schreiben Sie Ihre eigene.

amit_g
quelle
@amit_g, danke für die Info. Ich verwende bereits ein generisches / Basis-Repository, von dem alle anderen erben. Meine Idee für eine logische Gruppierung von "Nachschlagetabellen" in einem Repository war einfach, Zeit zu sparen und die Anzahl der Repositorys zu reduzieren. Anstatt ColorCodeRepository und AnotherCodeRepository zu erstellen, würde ich einfach CodesRepository.GetColorCodes () und CodesRepository.GetAnotherCodes () erstellen. Ich bin mir jedoch nicht sicher, ob eine logische Gruppierung nicht verwandter Entitäten in einem Repository eine schlechte Praxis ist.
e36M3
Außerdem bestätigen Sie dann, dass nach DDD-Regeln die Methoden in einem Repository, die dem Stamm entsprechen, den Stamm und nicht die zugrunde liegenden Entitäten im Diagramm zurückgeben sollten. In meinem Beispiel konnte also jede Methode im UserRepository nur den Benutzertyp zurückgeben, unabhängig davon, wie der Rest des Diagramms aussieht (oder der Teil des Diagramms, an dem ich wirklich interessiert bin, z. B. Adressen oder Telefone).
e36M3
CodesRepository ist in Ordnung, aber es wäre schwierig, konsequent zu pflegen, was dazu gehört. Das gleiche kann einfach durch GenericRepository <ColorCodes> GetAll () erfüllt werden. Da GenericRepository nur sehr generische Methoden (GetAll, GetByID usw.) haben würde, würde es für die Lookup-Tabellen gut funktionieren.
amit_g
2
Leider ist diese Antwort falsch. Das Repository sollte als Sammlung von In-Memory-Objekten behandelt werden, und Sie sollten ein verzögertes Laden vermeiden. Hier ist ein schöner Artikel darüber besnikgeek.blogspot.com/2010/07/…
Rafał Łużyński
9

Ihr Beispiel für das Aggregate Root-Repository ist vollkommen in Ordnung, dh jede Entität, die ohne Abhängigkeit von einer anderen nicht vernünftigerweise existieren kann, sollte kein eigenes Repository haben (in Ihrem Fall Phone). Ohne diese Überlegung können Sie schnell eine Explosion von Repositorys in einer 1-1-Zuordnung zu DB-Tabellen feststellen.

Sie sollten sich die Verwendung des Arbeitseinheitsmusters für Datenänderungen und nicht für die Repositorys selbst ansehen, da diese Ihrer Meinung nach zu Verwirrung hinsichtlich der Absicht führen, wenn Änderungen an der Datenbank beibehalten werden. In einer EF-Lösung ist die Arbeitseinheit im Wesentlichen ein Schnittstellen-Wrapper um Ihren EF-Kontext.

In Bezug auf Ihr Repository für Suchdaten erstellen wir einfach ein ReferenceDataRepository, das für Daten verantwortlich wird, die nicht speziell zu einer Domänenentität gehören (Länder, Farben usw.).

Darren Lewis
quelle
1
Danke. Ich bin nicht sicher, wie Unit of Work das Repository ersetzt. Ich setze UOW bereits in dem Sinne ein, dass am Ende jeder Geschäftstransaktion (Ende der HTTP-Anforderung) ein einzelner SaveChanges () -Aufruf an den Entity Framework-Kontext erfolgt. Ich gehe jedoch immer noch durch Repositories (die den EF-Kontext enthalten) für den Datenzugriff. Wie UserRepository.Delete (Benutzer) und UserRepository.Add (Benutzer).
e36M3
5

Wenn das Telefon ohne Benutzer keinen Sinn ergibt, handelt es sich um eine Entität (wenn Sie sich für die Identität interessieren) oder ein Wertobjekt und sollte immer über den Benutzer geändert und gemeinsam abgerufen / aktualisiert werden.

Stellen Sie sich aggregierte Wurzeln als Kontextdefinierer vor - sie zeichnen lokale Kontexte, befinden sich jedoch selbst im globalen Kontext (Ihrer Anwendung).

Wenn Sie dem domänengesteuerten Design folgen, sollten Repositorys 1: 1 pro Aggregatwurzel sein.
Keine Ausreden.

Ich wette, das sind Probleme, mit denen Sie konfrontiert sind:

  • technische Schwierigkeiten - Fehlanpassung der Objektbeziehungsimpedanz. Sie haben Probleme damit, ganze Objektdiagramme mit Leichtigkeit beizubehalten, und die Art des Entity-Frameworks hilft nicht weiter.
  • Das Domänenmodell ist datenzentriert (im Gegensatz zum verhaltenszentrierten Modell). Aus diesem Grund verlieren Sie das Wissen über die Objekthierarchie (zuvor erwähnte Kontexte) und auf magische Weise wird alles zu einer aggregierten Wurzel.

Ich bin nicht sicher, wie ich das erste Problem beheben soll, aber ich habe festgestellt, dass das Beheben des zweiten Problems das erste Problem gut genug behebt. Probieren Sie dieses Papier aus , um zu verstehen, was ich mit verhaltensorientiert meine .

Ps Das Reduzieren des Repositorys auf das Aggregieren von Root macht keinen Sinn.
Pps vermeiden "CodeRepositories". Das führt zu datenzentriertem -> prozeduralem Code.
Ppps Arbeitseinheitsmuster vermeiden. Aggregierte Roots sollten Transaktionsgrenzen definieren.

Arnis Lapsa
quelle
1
Da der Link zum Artikel
JwJosefy
3

Dies ist eine alte Frage, aber es lohnt sich, eine einfache Lösung zu veröffentlichen.

  1. EF Context bietet Ihnen bereits sowohl Arbeitseinheit (Änderungen verfolgen) als auch Repositorys (speicherinterne Referenz auf Daten aus der Datenbank). Eine weitere Abstraktion ist nicht zwingend erforderlich.
  2. Entfernen Sie das DBSet aus Ihrer Kontextklasse, da Phone kein aggregierter Stamm ist.
  3. Verwenden Sie stattdessen die Navigationseigenschaft "Telefone" für Benutzer.

statische Leere updateNumber (int userId, Zeichenfolge oldNumber, Zeichenfolge newNumber)

static void updateNumber(int userId, string oldNumber, string newNumber)
    {
        using (MyContext uow = new MyContext()) // Unit of Work
        {
            DbSet<User> repo = uow.Users; // Repository
            User user = repo.Find(userId); 
            Phone oldPhone = user.Phones.Where(x => x.Number.Trim() == oldNumber).SingleOrDefault();
            oldPhone.Number = newNumber;
            uow.SaveChanges();
        }

    }
Kreidig
quelle
Abstraktion ist nicht obligatorisch, wird aber empfohlen. Entity Framework ist immer noch nur ein Anbieter und Teil der Infrastruktur. Es geht nicht einmal nur darum, was passieren sollte, wenn der Anbieter wechselt, sondern in größeren Systemen gibt es möglicherweise mehrere Arten von Anbietern, die unterschiedliche Domänenkonzepte für unterschiedliche Persistenzmedien beibehalten. Dies ist die Art von Abstraktion, die sehr einfach zu erstellen ist, aber schmerzhaft ist, wenn man sie über genügend Zeit und Komplexität umgestaltet.
Joseph Ferris
1
Ich habe festgestellt, dass es sehr schwierig ist, die Vorteile von EF'S ORM (z. B. verzögertes Laden, abfragbare Dateien) beizubehalten, wenn ich versuche, auf eine Repository-Schnittstelle zu abstrahieren.
Chalky
Es ist natürlich eine interessante Diskussion. Da das verzögerte Laden sehr spezifisch für die Implementierung ist, ist sein Wert meines Erachtens auf die Infrastruktur beschränkt (Domänenobjekt in und aus mit schichtengebundener Übersetzung). Viele der Implementierungen, die ich gesehen habe, stoßen auf Probleme, wenn eine generische Abstraktion versucht wird. Ich tendiere dazu, eine explizite Implementierung zu wählen, da die generischen Methoden nur einen sehr geringen Domänenwert haben. EF macht Querableables zwar sehr benutzerfreundlich, aber das Problem wird zur Rolle des Repositorys - dh Repositorys, die von Controllern verwendet werden, verpassen die Vorteile der Abstraktion.
Joseph Ferris
0

Wenn eine Telefonentität nur zusammen mit einem aggregierten Stammbenutzer sinnvoll ist, ist es meiner Meinung nach auch sinnvoll, dass der Vorgang zum Hinzufügen eines neuen Telefondatensatzes in der Verantwortung des Benutzerdomänenobjekts durch eine bestimmte Methode (DDD-Verhalten) liegt Dies ist aus mehreren Gründen durchaus sinnvoll. Der unmittelbare Grund ist, dass wir überprüfen sollten, ob das Benutzerobjekt vorhanden ist, da die Telefonentität von ihrer Existenz abhängt, und möglicherweise eine Transaktionssperre beibehalten, während weitere Validierungsprüfungen durchgeführt werden, um sicherzustellen, dass kein anderer Prozess das Stammaggregat zuvor gelöscht hat Wir sind mit der Validierung der Operation fertig. In anderen Fällen möchten Sie bei anderen Arten von Root-Aggregaten möglicherweise einen Wert aggregieren oder berechnen und ihn in den Spalteneigenschaften des Root-Aggregats beibehalten, um später eine effizientere Verarbeitung durch andere Vorgänge zu ermöglichen.

Wenn Sie Methoden verwenden möchten, mit denen alle Telefone abgerufen werden, unabhängig davon, welche Benutzer sie besitzen, können Sie dennoch über das Benutzer-Repository nur eine Methode verwenden, die alle Benutzer als IQueryable zurückgibt. Anschließend können Sie sie zuordnen, um alle Benutzer-Telefone abzurufen und eine Verfeinerung durchzuführen frage damit ab. In diesem Fall benötigen Sie also nicht einmal ein PhoneRepository. Außerdem würde ich für IQueryable lieber eine Klasse mit Erweiterungsmethode verwenden, die ich überall verwenden kann, nicht nur aus einer Repository-Klasse, wenn ich Abfragen hinter Methoden abstrahieren möchte.

Nur eine Einschränkung, um Telefonentitäten nur mithilfe des Domänenobjekts und nicht eines Telefonrepositorys löschen zu können. Sie müssen sicherstellen, dass die Benutzer-ID Teil des Telefonprimärschlüssels ist. Mit anderen Worten, der Primärschlüssel eines Telefondatensatzes ist ein zusammengesetzter Schlüssel besteht aus UserId und einer anderen Eigenschaft (ich schlage eine automatisch generierte Identität vor) in der Phone-Entität. Dies ist intuitiv sinnvoll, da der Telefondatensatz dem Benutzerdatensatz "gehört" und das Entfernen aus der Benutzer-Navigationssammlung dem vollständigen Entfernen aus der Datenbank entspricht.

Kaveh Hadjari
quelle