Ich habe schon seit einiger Zeit nach einer guten Lösung für die Probleme gesucht, die durch das typische Repository-Muster entstehen (wachsende Liste von Methoden für spezielle Abfragen usw. Siehe: http://ayende.com/blog/3955/repository-) is-the-new-singleton ).
Ich mag die Idee, Befehlsabfragen zu verwenden, besonders durch die Verwendung des Spezifikationsmusters. Mein Problem mit der Spezifikation besteht jedoch darin, dass sie sich nur auf die Kriterien einfacher Auswahl bezieht (im Grunde genommen auf die where-Klausel) und sich nicht mit anderen Fragen von Abfragen befasst, wie z. B. Zusammenfügen, Gruppieren, Auswahl von Teilmengen oder Projektion usw. Grundsätzlich müssen alle zusätzlichen Rahmen, die viele Abfragen durchlaufen müssen, um den richtigen Datensatz zu erhalten.
(Hinweis: Ich verwende den Begriff "Befehl" wie im Befehlsmuster, auch als Abfrageobjekte bezeichnet. Ich spreche nicht von Befehl wie bei der Befehls- / Abfragetrennung, bei der zwischen Abfragen und Befehlen unterschieden wird (Aktualisieren, Löschen, einfügen))
Daher suche ich nach Alternativen, die die gesamte Abfrage kapseln, aber dennoch flexibel genug sind, um nicht nur Spaghetti-Repositorys gegen eine Explosion von Befehlsklassen auszutauschen.
Ich habe zum Beispiel Linqspecs verwendet, und obwohl ich einen gewissen Wert darin finde, Auswahlkriterien aussagekräftige Namen zuzuweisen, reicht dies einfach nicht aus. Vielleicht suche ich eine gemischte Lösung, die mehrere Ansätze kombiniert.
Ich suche nach Lösungen, die andere möglicherweise entwickelt haben, um entweder dieses Problem oder ein anderes Problem anzugehen, aber diese Anforderungen dennoch erfüllen. In dem verlinkten Artikel schlägt Ayende vor, den nHibernate-Kontext direkt zu verwenden, aber ich bin der Meinung, dass dies Ihre Geschäftsschicht erheblich kompliziert, da sie jetzt auch Abfrageinformationen enthalten muss.
Ich werde ein Kopfgeld dafür anbieten, sobald die Wartezeit abgelaufen ist. Bitte machen Sie Ihre Lösungen mit guten Erklärungen bounty-würdig, und ich werde die beste Lösung auswählen und die Zweitplatzierten bewerten.
HINWEIS: Ich suche etwas, das ORM-basiert ist. Muss nicht explizit EF oder nHibernate sein, aber diese sind am häufigsten und passen am besten. Wenn es leicht an andere ORMs angepasst werden kann, wäre das ein Bonus. Linq kompatibel wäre auch schön.
UPDATE: Ich bin wirklich überrascht, dass es hier nicht viele gute Vorschläge gibt. Es scheint, als wären die Leute entweder komplett CQRS oder sie befinden sich komplett im Repository-Lager. Die meisten meiner Apps sind nicht komplex genug, um CQRS zu rechtfertigen (etwas, für das die meisten CQRS-Befürworter bereitwillig sagen, dass Sie es nicht verwenden sollten).
UPDATE: Hier scheint es ein wenig Verwirrung zu geben. Ich suche keine neue Datenzugriffstechnologie, sondern eine einigermaßen gut gestaltete Schnittstelle zwischen Unternehmen und Daten.
Im Idealfall suche ich eine Art Kreuzung zwischen Abfrageobjekten, Spezifikationsmuster und Repository. Wie oben erwähnt, behandelt das Spezifikationsmuster nur den Aspekt der where-Klausel und nicht die anderen Aspekte der Abfrage, wie z. B. Verknüpfungen, Unterauswahlen usw. Repositorys behandeln die gesamte Abfrage, geraten jedoch nach einer Weile außer Kontrolle . Abfrageobjekte behandeln auch die gesamte Abfrage, aber ich möchte Repositorys nicht einfach durch Explosionen von Abfrageobjekten ersetzen.
quelle
Antworten:
Haftungsausschluss: Da es noch keine großartigen Antworten gibt, habe ich beschlossen, einen Teil eines großartigen Blogposts zu veröffentlichen, den ich vor einiger Zeit gelesen und fast wörtlich kopiert habe. Den vollständigen Blogbeitrag finden Sie hier . Hier ist es also:
Wir können die folgenden zwei Schnittstellen definieren:
Das
IQuery<TResult>
gibt eine Nachricht an, die eine bestimmte Abfrage mit den zurückgegebenen Daten unter Verwendung desTResult
generischen Typs definiert. Mit der zuvor definierten Schnittstelle können wir eine Abfragenachricht wie folgt definieren:Diese Klasse definiert eine Abfrageoperation mit zwei Parametern, die zu einem Array von
User
Objekten führt. Die Klasse, die diese Nachricht verarbeitet, kann wie folgt definiert werden:Wir können jetzt die Verbraucher von der generischen
IQueryHandler
Schnittstelle abhängig machen :Dieses Modell bietet uns sofort viel Flexibilität, da wir jetzt entscheiden können, was in das Modell injiziert werden soll
UserController
. Wir können eine völlig andere Implementierung einfügen oder eine, die die eigentliche Implementierung umschließt, ohne Änderungen an derUserController
(und allen anderen Verbrauchern dieser Schnittstelle) vornehmen zu müssen .Die
IQuery<TResult>
Schnittstelle bietet uns Unterstützung bei der Kompilierung beim Angeben oder EinfügenIQueryHandlers
unseres Codes. Wenn wir das ändernFindUsersBySearchTextQuery
zurückzukehrenUserInfo[]
statt (durch die ImplementierungIQuery<UserInfo[]>
), dasUserController
wird nicht kompilieren, da die generische Typ Einschränkung fürIQueryHandler<TQuery, TResult>
nicht in der Lage sein , kartierenFindUsersBySearchTextQuery
zuUser[]
.Das Injizieren der
IQueryHandler
Schnittstelle in einen Verbraucher weist jedoch einige weniger offensichtliche Probleme auf, die noch angegangen werden müssen. Die Anzahl der Abhängigkeiten unserer Verbraucher kann zu groß werden und zu einer Überinjektion des Konstruktors führen - wenn ein Konstruktor zu viele Argumente verwendet. Die Anzahl der Abfragen, die eine Klasse ausführt, kann sich häufig ändern, was ständige Änderungen der Anzahl der Konstruktorargumente erforderlich machen würde.Wir können das Problem beheben, dass zu viele
IQueryHandlers
mit einer zusätzlichen Abstraktionsebene injiziert werden müssen . Wir erstellen einen Mediator, der zwischen den Verbrauchern und den Abfragehandlern sitzt:Dies
IQueryProcessor
ist eine nicht generische Schnittstelle mit einer generischen Methode. Wie Sie in der Schnittstellendefinition sehen können,IQueryProcessor
hängt dies von derIQuery<TResult>
Schnittstelle ab. Dies ermöglicht uns eine Unterstützung bei der Kompilierungszeit bei unseren Verbrauchern, die von der abhängig istIQueryProcessor
. Lassen Sie uns das umschreibenUserController
, um das Neue zu verwendenIQueryProcessor
:Das
UserController
hängt jetzt von einem abIQueryProcessor
, der alle unsere Anfragen bearbeiten kann. Die MethodeUserController
'sSearchUsers
ruft dieIQueryProcessor.Process
Methode auf, die ein initialisiertes Abfrageobjekt übergibt. Da dasFindUsersBySearchTextQuery
dieIQuery<User[]>
Schnittstelle implementiert , können wir es an die generischeExecute<TResult>(IQuery<TResult> query)
Methode übergeben. Dank der C # -Typinferenz kann der Compiler den generischen Typ bestimmen, sodass wir den Typ nicht explizit angeben müssen. Der Rückgabetyp derProcess
Methode ist ebenfalls bekannt.Es liegt nun in der Verantwortung der Umsetzung des
IQueryProcessor
, das Richtige zu findenIQueryHandler
. Dies erfordert eine dynamische Typisierung und optional die Verwendung eines Dependency Injection-Frameworks und kann mit nur wenigen Codezeilen durchgeführt werden:Die
QueryProcessor
Klasse erstellt einen bestimmtenIQueryHandler<TQuery, TResult>
Typ basierend auf dem Typ der angegebenen Abfrageinstanz. Dieser Typ wird verwendet, um die angegebene Containerklasse aufzufordern, eine Instanz dieses Typs abzurufen. Leider müssen wir dieHandle
Methode mit Reflection aufrufen (in diesem Fall mit dem Schlüsselwort C # 4.0 dymamic), da es zu diesem Zeitpunkt unmöglich ist, die Handlerinstanz zu konvertieren, da das generischeTQuery
Argument zur Kompilierungszeit nicht verfügbar ist. Sofern dieHandle
Methode nicht umbenannt wird oder andere Argumente erhält, schlägt dieser Aufruf niemals fehl. Wenn Sie möchten, ist es sehr einfach, einen Komponententest für diese Klasse zu schreiben. Die Verwendung von Reflexion führt zu einem leichten Abfall, ist jedoch kein Grund zur Sorge.Um eines Ihrer Anliegen zu beantworten:
Eine Konsequenz der Verwendung dieses Entwurfs ist, dass es viele kleine Klassen im System gibt, aber es ist eine gute Sache, viele kleine / fokussierte Klassen (mit klaren Namen) zu haben. Dieser Ansatz ist eindeutig viel besser als viele Überladungen mit unterschiedlichen Parametern für dieselbe Methode in einem Repository, da Sie diese in einer Abfrageklasse gruppieren können. Sie erhalten also immer noch viel weniger Abfrageklassen als Methoden in einem Repository.
quelle
TResult
Parameter derIQuery
Schnittstelle nicht nützlich ist. In meiner aktualisierten Antwort wird derTResult
Parameter jedoch von derProcess
Methode von verwendetIQueryProcessor
, um dieIQueryHandler
zur Laufzeit aufzulösen .IQueryable
und sichergestellt, dass die Auflistung nicht aufgelistet wurde. Anschließend habeQueryHandler
ich die Abfragen aufgerufen / verkettet. Dies gab mir die Flexibilität, meine Abfragen zu testen und zu verketten. Ich habe einen Anwendungsservice zusätzlich zu meinemQueryHandler
, und mein Controller ist dafür verantwortlich, direkt mit dem Service anstelle desMein Umgang damit ist eigentlich simpel und ORM-agnostisch. Meine Ansicht für ein Endlager ist dies: Das Repository Aufgabe ist die App mit dem Modell für den Kontext erforderlich sind, so dass die App fragt nur den Repo für was es will , es aber nicht sagen , wie es zu bekommen.
Ich versorge die Repository-Methode mit einem Kriterium (Ja, DDD-Stil), das vom Repo zum Erstellen der Abfrage verwendet wird (oder was auch immer erforderlich ist - es kann sich um eine Webservice-Anforderung handeln). Joins und Gruppen imho sind Details zum Wie, nicht das Was und ein Kriterium sollten nur die Basis sein, um eine where-Klausel zu erstellen.
Modell = das endgültige Objekt oder die Datenstruktur, die von der App benötigt werden.
Wahrscheinlich können Sie die ORM-Kriterien (Nhibernate) direkt verwenden, wenn Sie dies möchten. Die Repository-Implementierung sollte wissen, wie die Kriterien mit dem zugrunde liegenden Speicher oder DAO verwendet werden.
Ich kenne Ihre Domain und die Modellanforderungen nicht, aber es wäre seltsam, wenn der beste Weg darin besteht, dass die App die Abfrage selbst erstellt. Das Modell ändert sich so sehr, dass Sie nichts Stabiles definieren können?
Diese Lösung erfordert eindeutig zusätzlichen Code, koppelt jedoch nicht den Rest an einen ORM oder was auch immer Sie für den Zugriff auf den Speicher verwenden. Das Repository fungiert als Fassade und IMO ist es sauber und der Code für die Kriterienübersetzung ist wiederverwendbar
quelle
Ich habe dies getan, dies unterstützt und dies rückgängig gemacht.
Das Hauptproblem ist folgendes: Egal wie Sie es tun, die hinzugefügte Abstraktion verschafft Ihnen keine Unabhängigkeit. Es wird per Definition lecken. Im Wesentlichen erfinden Sie eine ganze Ebene, nur um Ihren Code niedlich aussehen zu lassen ... aber es reduziert nicht die Wartung, verbessert die Lesbarkeit oder bringt Ihnen irgendeine Art von Modell-Agnostizismus.
Der lustige Teil ist, dass Sie Ihre eigene Frage als Antwort auf Oliviers Antwort beantwortet haben: "Dies dupliziert im Wesentlichen die Funktionalität von Linq ohne alle Vorteile, die Sie von Linq erhalten."
Fragen Sie sich: Wie könnte es nicht sein?
quelle
Sie können eine fließende Schnittstelle verwenden. Die Grundidee ist, dass Methoden einer Klasse die aktuelle Instanz genau dieser Klasse zurückgeben, nachdem sie eine Aktion ausgeführt haben. Auf diese Weise können Sie Methodenaufrufe verketten.
Durch Erstellen einer geeigneten Klassenhierarchie können Sie einen logischen Fluss zugänglicher Methoden erstellen.
Sie würden es so nennen
Sie können nur eine neue Instanz von erstellen
Query
. Die anderen Klassen haben einen geschützten Konstruktor. Der Punkt der Hierarchie besteht darin, Methoden zu "deaktivieren". Beispielsweise gibt dieGroupBy
Methode a zurück,GroupedQuery
die die Basisklasse von istQuery
und keineWhere
Methode hat (die where-Methode ist in deklariertQuery
). Daher ist es nicht möglich, rufenWhere
nachGroupBy
.Es ist jedoch nicht perfekt. Mit dieser Klassenhierarchie können Sie Mitglieder nacheinander ausblenden, aber keine neuen anzeigen.
Having
Löst daher eine Ausnahme aus, wenn sie zuvor aufgerufen wirdGroupBy
.Beachten Sie, dass Sie
Where
mehrmals anrufen können . Dies fügtAND
den vorhandenen Bedingungen neue Bedingungen hinzu . Dies erleichtert das programmgesteuerte Erstellen von Filtern aus einzelnen Bedingungen. Das gleiche ist möglich mitHaving
.Die Methoden, die Feldlisten akzeptieren, haben einen Parameter
params string[] fields
. Sie können entweder einzelne Feldnamen oder ein String-Array übergeben.Fließende Schnittstellen sind sehr flexibel und erfordern keine großen Überladungen von Methoden mit unterschiedlichen Parameterkombinationen. Mein Beispiel funktioniert mit Zeichenfolgen, der Ansatz kann jedoch auf andere Typen erweitert werden. Sie können auch vordefinierte Methoden für Sonderfälle oder Methoden deklarieren, die benutzerdefinierte Typen akzeptieren. Sie können auch Methoden wie
ExecuteReader
oder hinzufügenExceuteScalar<T>
. Auf diese Weise können Sie solche Abfragen definierenSelbst auf diese Weise erstellte SQL-Befehle können Befehlsparameter haben und so SQL-Injection-Probleme vermeiden und gleichzeitig das Zwischenspeichern von Befehlen durch den Datenbankserver ermöglichen. Dies ist kein Ersatz für einen O / R-Mapper, kann jedoch in Situationen hilfreich sein, in denen Sie die Befehle ansonsten mithilfe einer einfachen Zeichenfolgenverkettung erstellen würden.
quelle