Gibt es einen echten Vorteil für das generische Repository?

28

Ich habe einige Artikel über die Vorteile des Erstellens von generischen Repositorys für eine neue App gelesen ( Beispiel ). Die Idee scheint gut zu sein, da ich mit demselben Repository mehrere Dinge für verschiedene Entitätstypen gleichzeitig ausführen kann:

IRepository repo = new EfRepository(); // Would normally pass through IOC into constructor 
var c1 = new Country() { Name = "United States", CountryCode = "US" };
var c2 = new Country() { Name = "Canada", CountryCode = "CA" };
var c3 = new Country() { Name = "Mexico", CountryCode = "MX" };
var p1 = new Province() { Country = c1, Name = "Alabama", Abbreviation = "AL" };
var p2 = new Province() { Country = c1, Name = "Alaska", Abbreviation = "AK" };
var p3 = new Province() { Country = c2, Name = "Alberta", Abbreviation = "AB" };
repo.Add<Country>(c1);
repo.Add<Country>(c2);
repo.Add<Country>(c3);
repo.Add<Province>(p1);
repo.Add<Province>(p2);
repo.Add<Province>(p3);
repo.Save();

Der Rest der Implementierung des Repository ist jedoch stark von Linq abhängig:

IQueryable<T> Query();
IList<T> Find(Expression<Func<T,bool>> predicate);
T Get(Expression<Func<T,bool>> predicate);
T First(Expression<Func<T,bool>> predicate);
//... and so on

Dieses Repository-Muster funktionierte hervorragend für Entity Framework und bot praktisch eine 1: 1-Zuordnung der in DbContext / DbSet verfügbaren Methoden. Welchen Vorteil bietet dies angesichts der langsamen Einführung von Linq in andere Datenzugriffstechnologien außerhalb von Entity Framework gegenüber der direkten Arbeit mit DbContext?

Ich habe versucht, eine PetaPoco- Version des Repository zu schreiben , aber PetaPoco unterstützt Linq Expressions nicht. Daher ist das Erstellen einer generischen IRepository-Schnittstelle so gut wie nutzlos, es sei denn, Sie verwenden sie nur für die grundlegenden Funktionen GetAll, GetById, Add, Update, Delete und Save Methoden und verwenden Sie es als Basisklasse. Dann müssen Sie bestimmte Repositorys mit speziellen Methoden erstellen, um alle "where" -Klauseln zu behandeln, die ich zuvor als Prädikat übergeben konnte.

Ist das Muster für das generische Repository für andere Zwecke als Entity Framework nützlich? Wenn nicht, warum sollte es jemand verwenden, anstatt direkt mit Entity Framework zu arbeiten?


Der ursprüngliche Link entspricht nicht dem Muster, das ich in meinem Beispielcode verwendet habe. Hier ist ein ( aktualisierter Link ).

Sam
quelle
1
Geht es bei der Frage wirklich um "Advatange of Generic Repository" oder geht es eher darum, "wie man komplexe Abfragen hinter einer generischen Repository-Schnittstelle verbirgt"? Ist es möglich, generisch zu sein, wenn die Benutzeroberfläche und die Verwendung von linq abhängen? Unsere Repositoryapi verfügt über eine QueryByExample-Methode, die von der Suchtechnik völlig unabhängig ist und eine Verschiebung der Implementierung ermöglichen würde.
k3b
"Es ermöglicht mir, dasselbe Repository für verschiedene Entitätstypen zu verwenden." => Habe ich oder Sie nur falsch verstanden, was ein generisches Repository ist (auch in Bezug auf den genannten Artikel)? Generisches Repository bedeutete für mich immer, alle Repos mit einer einzigen Klasse oder Schnittstelle zu versehen und keine einzige Repository- Instanz zu haben , die die Persistenz aller Entitäten unabhängig von ihrem Typ verwaltet ...
guillaume31

Antworten:

35

Das generische Repository ist für Entity Framework sogar unbrauchbar (und meiner Meinung nach auch schlecht). Es bringt keinen zusätzlichen Wert für das, was bereits von bereitgestellt wird IDbSet<T>(was übrigens ein generisches Repository ist).

Wie Sie bereits festgestellt haben, ist das Argument, dass das generische Repository durch die Implementierung für andere Datenzugriffstechnologien ersetzt werden kann, ziemlich schwach, da es das Schreiben Ihres eigenen Linq-Providers erfordern kann.

Das zweite häufige Argument für vereinfachte Komponententests ist ebenfalls falsch, da das Verspotten von Repository / Set mit speicherinternem Datenspeicher den Linq-Anbieter durch einen anderen mit unterschiedlichen Funktionen ersetzt. Der Linq-to-Entities-Anbieter unterstützt nur einen Teil der Linq-Funktionen - er unterstützt sogar nicht alle auf der IQueryable<T>Benutzeroberfläche verfügbaren Methoden . Die gemeinsame Nutzung von Ausdrucksbäumen zwischen Datenzugriffsebene und Geschäftslogikebene verhindert das Fälschen der Datenzugriffsebene - die Abfragelogik muss getrennt werden.

Wenn Sie eine starke "generische" Abstraktion wünschen, müssen Sie auch andere Muster einbeziehen. In diesem Fall müssen Sie eine abstrakte Abfragesprache verwenden, die vom Repository in eine bestimmte Abfragesprache übersetzt werden kann, die von der verwendeten Datenzugriffsebene unterstützt wird. Dies wird durch das Spezifikationsmuster erledigt. Linq on IQueryableist eine Spezifikation (für die Übersetzung ist jedoch ein Provider erforderlich - oder ein benutzerdefinierter Besucher, der den Ausdrucksbaum in eine Abfrage übersetzt). Sie können jedoch Ihre eigene vereinfachte Version definieren und diese verwenden. Beispielsweise verwendet NHibernate die Kriterien-API. Der einfachste Weg ist jedoch die Verwendung eines bestimmten Repositorys mit bestimmten Methoden. Diese Methode ist am einfachsten zu implementieren, am einfachsten zu testen und am einfachsten in Komponententests zu fälschen, da die Abfragelogik vollständig verborgen und hinter der Abstraktion getrennt ist.

Ladislav Mrnka
quelle
Nur eine kurze Frage, NHibernate hat die, ISessiondie für allgemeine Unit-Testzwecke leicht zu verspotten ist, und ich würde das 'Repository' auch gerne löschen, wenn ich mit EF zusammenarbeite - aber gibt es eine einfachere und unkompliziertere Möglichkeit, dies neu zu erstellen? Etwas Ähnliches ISessionund das ISessionFactory, IDbContextdenn soweit ich das
beurteilen
Nein, es gibt kein IDbContext- wenn Sie möchten IDbContext, können Sie einfach eines erstellen und es in Ihrem abgeleiteten Kontext implementieren.
Ladislav Mrnka
Sie sagen also, dass IDbSet <T> für alltägliche MVC-Apps ein ausreichend generisches Repository bereitstellen sollte, um das Repository-Muster vollständig zu vergessen. Außer in besonderen Situationen, in denen Sie beispielsweise keine DAL-Referenzen in Ihrem MVC-Projekt zulassen.
ProfK
1
Gute Antwort. Einige Entwickler erstellen ohne Grund nur eine weitere Abstraktionsebene. IMO, EF benötigen weder ein generisches Repository noch eine Arbeitseinheit (sie sind
eingebaut
1
IDbSet <T> ist ein generisches Repository mit einer Abhängigkeit vom Entity Framework, das den Zweck der Verwendung eines generischen Repositorys zum Abstrahieren der Abhängigkeit vom Entity Framework aufhebt.
Joel McBeth
7

Das Problem ist nicht das Repository-Muster. Es ist eine gute Sache, eine Abstraktion zwischen dem Abrufen von Daten und dem Abrufen von Daten zu haben.

Hier geht es um die Umsetzung. Die Annahme, dass ein beliebiger Ausdruck zum Filtern verwendet werden kann, ist bestenfalls problematisch.

Wenn Sie dafür sorgen, dass ein Repository für alle Ihre Objekte direkt funktioniert, geht der Punkt daneben. Datenobjekte werden selten oder nie direkt Geschäftsobjekten zugeordnet. In solchen Situationen ist es weniger sinnvoll, T an Filter zu übergeben. Und wenn Sie so viele Funktionen bereitstellen, können Sie sicher sein, dass Sie nicht alles unterstützen können, sobald ein anderer Anbieter hinzukommt.

Telastyn
quelle
Es ist also sinnvoller, ein Repository pro Datenobjekt oder eine Gruppe eng verwandter Objekte mit bestimmten Methoden wie GetById (int id), SortedList () usw. zu haben. Sende ich Listen von Datenobjekten aus einem Repository zurück oder transformiere ich sie auch in Geschäftsobjekte mit den erforderlichen Feldern? Ein bisschen dachte, das war, was in der Service / Business-Schicht passiert ist.
Sam
1
@sam - Ich bevorzuge eher spezifischere Repositories, ja. Ob sie übersetzen oder nicht, hängt davon ab, wo sich die Dinge wahrscheinlich ändern werden. Wenn Ihre Domain gut definiert ist, würde ich Dinge in der Form der Domain zurückgeben. Wenn die Daten gut definiert sind, würde ich Dinge in Form der Datenstrukturen zurückgeben. Wenn beides nicht der Fall ist, hätte ich eine Entität, die als gut definierte Grundlage für den Aufbau und die Anpassung an diese dient.
Telastyn
1
Es gibt keinen Grund, warum Sie keine spezifischen Repositorys haben können, die die allgemeinen Daten an ein generisches Repository delegieren, sondern auch zusätzliche repository-spezifische Methoden für komplexere Aufrufe. Ich habe es mehrmals so gesehen.
Eric King
@EricKing habe ich auch, und es war gut dort, aber es neigt dazu, etwas Abstraktion zu verlieren, da das gemeinsame Zeug dazu neigt, nur aufgrund der Gemeinsamkeit zu existieren, wie die Daten gespeichert werden (GetByID erfordert zum Beispiel Tabellen mit IDs).
Telastyn
@Telastyn Ja, dem würde ich zustimmen, aber es kommt auch bei bestimmten Repositories vor. Undichte Abstraktionen sind nicht spezifisch für generische Repositorys. (Wow, hätte ich das ungeschickter
Eric King
2

Der Wert einer generischen Datenschicht (ein Repository ist eine bestimmte Art von Datenschicht) ermöglicht es dem Code, den zugrunde liegenden Speichermechanismus zu ändern, ohne dass sich dies auf den aufrufenden Code auswirkt.

Theoretisch funktioniert das gut. In der Praxis ist die Abstraktion, wie Sie gesehen haben, oft undicht. Die Mechanismen für den Zugriff auf Daten unterscheiden sich von den Mechanismen in einem anderen. In einigen Fällen schreiben Sie den Code am Ende zweimal: einmal in der Business-Schicht und wiederholen ihn in der Datenschicht.

Die effektivste Methode zum Erstellen einer generischen Datenschicht besteht darin, die verschiedenen Arten von Datenquellen zu kennen, die die Anwendung im Voraus verwendet. Wie Sie gesehen haben, kann die Annahme, dass LINQ oder SQL universell sind, ein Problem sein. Der Versuch, neue Datenspeicher nachzurüsten, führt wahrscheinlich zu einem Überschreiben.

[Bearbeiten: Folgendes hinzugefügt.]

Dies hängt auch davon ab, was die Anwendung von der Datenschicht benötigt. Wenn die Anwendung nur Objekte lädt oder speichert, kann die Datenschicht sehr einfach sein. Mit dem Erfordernis, zu suchen, zu sortieren und zu filtern, steigt die Komplexität der Datenschicht und Abstraktionen werden undicht (z. B. das Offenlegen von LINQ-Abfragen in der Frage). Sobald Benutzer jedoch ihre eigenen Abfragen stellen können, muss das Kosten-Nutzen-Verhältnis der Datenschicht sorgfältig abgewogen werden.

akton
quelle
1

In fast allen Fällen lohnt es sich, eine Codeschicht über der Datenbank zu haben. Ich würde im Allgemeinen ein "GetByXXXXX" -Muster in diesem Code bevorzugen - es ermöglicht Ihnen, die dahinter stehenden Abfragen nach Bedarf zu optimieren, während die Benutzeroberfläche frei von Datenschnittstellen-Unordnung bleibt.

Generika auszunutzen ist auf jeden Fall faires Spiel - eine Load<T>(int id)Methode zu haben macht jede Menge Sinn. Das Erstellen von Repositorys um LINQ ist jedoch das Äquivalent von SQL-Abfragen in den 2010er Jahren, die überall gelöscht werden, mit ein wenig zusätzlicher Sicherheit.

Wyatt Barnett
quelle
0

Nun, mit dem angegebenen Link kann ich sehen, dass es ein praktischer Wrapper für eine ist DataServiceContext, aber weder Code-Manipulationen reduziert noch die Lesbarkeit verbessert. Außerdem ist der Zugang zu DataServiceQuery<T>behindert, was die Flexibilität von .Where()und einschränkt .Single(). Es sind auch AddRange()keine Alternativen vorgesehen. Es ist auch nichts Delete(Predicate)vorgesehen, was nützlich sein könnte ( repo.Delete<Person>( p => p.Name=="Joe" );um Joe-s zu löschen). Etc.

Fazit: Eine solche API blockiert die native API und beschränkt sie auf einige einfache Operationen.

aiodintsov
quelle
Du hast Recht. Das war nicht der Artikel mit dem Muster, das ich in meinem Beispielcode habe. Ich werde versuchen, den Link zu finden, wenn ich nach Hause komme.
Sam
Aktualisierter Link am Ende der Frage hinzugefügt.
Sam