Alternativen zum Repository-Muster zur Kapselung der ORM-Logik?

24

Ich musste nur ein ORM auswechseln und es war eine relativ entmutigende Aufgabe, da die Abfragelogik überall leckte. Wenn ich jemals eine neue Anwendung entwickeln müsste, wäre es meine persönliche Präferenz, die gesamte Abfragelogik (unter Verwendung eines ORM) zu kapseln, um sie zukunftssicher für Änderungen zu machen. Das Repository-Muster ist recht schwierig zu codieren und zu pflegen. Daher habe ich mich gefragt, ob es noch andere Muster gibt, mit denen das Problem behoben werden kann.

Ich kann Beiträge vorhersehen, in denen es darum geht, keine zusätzliche Komplexität hinzuzufügen, bevor es tatsächlich benötigt wird, agil zu sein usw. Aber ich bin nur daran interessiert, wie vorhandene Muster ein ähnliches Problem auf einfachere Weise lösen.

Mein erster Gedanke war, ein generisches Typrepository zu haben, zu dem ich über Erweiterungsmethoden nach Bedarf Methoden zu bestimmten Typrepository-Klassen hinzufüge, aber das Testen statischer Methoden durch Einheiten ist furchtbar schmerzhaft. IE:

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}
Dante
quelle
5
Was genau ist mit dem Repository-Muster, dessen Code und Pflege Sie als schwierig empfinden?
Matthew Flynn
1
Es gibt Alternativen da draußen. Eine davon ist die Verwendung des Query Object-Musters, das, obwohl ich es nicht verwendet habe, ein guter Ansatz zu sein scheint. Repository IMHO ist ein gutes Muster, wenn es richtig verwendet wird, aber nicht das A und
O

Antworten:

14

Zuallererst: Ein generisches Repository sollte als Basisklasse und nicht als vollständige Implementierung betrachtet werden. Es sollte Ihnen helfen, die gängigen CRUD-Methoden zu implementieren. Sie müssen jedoch noch die Abfragemethoden implementieren:

public class UserRepository : EntityFrameworkRepository<User>, IUserRepository
{
    public UserRepository(DbContext context){}

    public ICollection<Person> FindRetired()
    {
        return context.Persons.Where(p => p.Status == "Retired").ToList();
    }
}

Zweite:

Kehre nicht zurück IQueryable. Es ist eine undichte Abstraktion. Versuchen Sie, diese Schnittstelle selbst zu implementieren, oder verwenden Sie sie ohne Kenntnis des zugrunde liegenden Datenbankanbieters. Zum Beispiel hat jeder DB-Provider eine eigene API zum eifrigen Laden von Entitäten. Deshalb ist es eine undichte Abstraktion.

Alternative

Alternativ können Sie Abfragen verwenden (z. B. wie durch das Trennmuster Befehl / Abfrage definiert). CQS ist in Wikipedia beschrieben.

Sie erstellen grundsätzlich Abfrageklassen, die Sie aufrufen:

public class GetMyMessages
{
    public GetMyMessages(IDbConnection connection)
    {
    }


    public Message[] Execute(DateTime minDate)
    {
        //query
    }
}

Das Tolle an Abfragen ist, dass Sie verschiedene Datenzugriffsmethoden für verschiedene Abfragen verwenden können, ohne den Code zu überladen. Sie können beispielsweise einen Webdienst in einem, einen anderen in einem anderen und ADO.NET in einem dritten verwenden.

Wenn Sie an einer .NET-Implementierung interessiert sind, lesen Sie meinen Artikel: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/

jgauffin
quelle
Wäre das alternative Muster, das Sie vorgeschlagen haben, bei größeren Projekten noch mehr Unordnung zu schaffen, da eine Methode im Repository-Muster jetzt eine ganze Klasse ist?
Dante
3
Wenn Ihre Definition von kleinen Klassen unübersichtlich ist, ja. In diesem Fall sollten Sie nicht versuchen, die SOLID-Prinzipien zu befolgen, da ihre Anwendung immer mehr (kleinere) Klassen hervorbringt.
jgauffin
2
Kleinere Klassen sind besser, aber wäre die Navigation in vorhandenen Abfrageklassen nicht mühsamer als das Durchsuchen des Intellisense eines (Domain-) Repositorys, um festzustellen, ob sich die erforderliche Funktionalität darin befindet, und wenn nicht, implementieren Sie sie.
Dante
4
Die Projektstruktur wird wichtiger. Wenn Sie alle Abfragen in einen Namespace wie YourProject.YourDomainModelName.Queriesdiesen einfügen, ist die Navigation einfach.
jgauffin
Eine Sache, die ich mit CQS schwer tun kann, sind häufig gestellte Fragen. Beispielsweise werden mit einer Abfrage für einen Benutzer alle zugeordneten Schüler (unterschiedliche Noten, eigene Kinder usw.) abgerufen. Jetzt muss eine andere Abfrage untergeordnete Elemente für einen Benutzer abrufen und sie dann bearbeiten. Abfrage 2 kann also die allgemeine Logik in Abfrage 1 nutzen, aber es gibt keine einfache Möglichkeit, dies zu tun. Ich konnte keine CQS-Implementierung finden, die dies auf einfache Weise ermöglicht. Repositories helfen dabei sehr. Ich bin kein Fan von beiden Mustern, sondern teile nur meine Einstellung.
Mrchief
8

Zuerst denke ich, dass ORM schon groß genug ist, um über Ihre Datenbank zu abstrahieren. Einige ORMs bieten Bindungen für alle gängigen relationalen Datenbanken (NHibernate verfügt über Bindungen für MSSQL, Oracle, MySQL, Postgress usw.). Daher erscheint es mir nicht rentabel, eine neue Abstraktion darüber aufzubauen. Außerdem ist die Argumentation, dass Sie dieses ORM "abstrahieren" müssen, bedeutungslos.

Wenn Sie diese Abstraktion dennoch erstellen möchten, würde ich gegen das Repository-Muster verstoßen. Es war großartig im Zeitalter von reinem SQL, aber es ist im Hinblick auf modernes ORM ziemlich mühsam. Dies liegt hauptsächlich daran, dass Sie die meisten ORM-Funktionen wie CRUD-Operationen und Abfragen erneut implementieren.

Wenn ich solche Abstraktionen bauen würde, würde ich diese Regeln / Muster verwenden

  • CQRS
  • Verwenden Sie so viele vorhandene ORM-Funktionen wie möglich
  • Verkapseln Sie nur komplexe Logik und Abfragen
  • Versuchen Sie, die "Installation" des ORM in einer Architektur zu verstecken

In der konkreten Implementierung würde ich CRUD-Operationen von ORM direkt verwenden, ohne einen Wrapping. Ich würde auch einfache Abfragen direkt im Code machen. Komplexe Abfragen würden jedoch in eigenen Objekten gekapselt. Um den ORM "auszublenden", würde ich versuchen, den Datenkontext transparent in ein Dienst- / UI-Objekt einzufügen und das gleiche tun, um Objekte abzufragen.

Das Letzte, was ich sagen möchte, ist, dass viele Leute ORM benutzen, ohne zu wissen, wie man es benutzt und wie man den besten "Profit" daraus macht. Die Leute, die Repositories empfehlen, sind normalerweise von dieser Art.

Als empfohlene Lektüre würde ich Ayendes Blog nennen , insbesondere diesen Artikel .

Euphorisch
quelle
+1 "Das Letzte, was ich sagen möchte, ist, dass viele Leute ORM benutzen, ohne zu wissen, wie man es benutzt und wie man den besten" Profit "daraus macht. Leute, die Repositories empfehlen, sind normalerweise von dieser Art"
Misters
1

Das Problem mit der undichten Implementierung besteht darin, dass Sie viele verschiedene Filterbedingungen benötigen.

Sie können diese API eingrenzen, wenn Sie eine Repository-Methode FindByExample implementieren und sie folgendermaßen verwenden

// find all retired persons
Person filter = new Person {Status=PersonStatus.Retired};
IEnumerable<Person> found = personRepository.FindByExample(filter);


// find all persons who have a dog named "sam"
Person filter = new Person();
filter.AddPet(new Dog{Name="sam"});
IEnumerable<Person> found = personRepository.FindByExample(filter);
k3b
quelle
0

Erwägen Sie, Ihre Erweiterungen auf IQueryable anstatt auf IRepository laufen zu lassen.

public static class PersonExtensions
{
    public static IQueryable<Person> AreRetired(this IQueryable<Person> people)
    {
        return people.Where(p => p.Status == "Retired");
    }
}

Zum Komponententest:

List<Person> people = new List<Person>();
people.Add(new Person() { Name = "Bob", Status = "Retired" });
people.Add(new Person() { Name = "Sam", Status = "Working" });

var retiredPeople = people.AsQueryable().AreRetired();
// assert that Bob is in the list
// assert that Sam is not in the list

Zum Testen ist keine Verspottung erforderlich. Sie können diese Erweiterungsmethoden auch kombinieren, um bei Bedarf komplexere Abfragen zu erstellen.

Jared S
quelle
5
-1 a) IQueryableist eine schlechte Mutter oder vielmehr eine GOTT-Schnittstelle. Die Implementierung ist sehr erfolgreich und es gibt nur sehr wenige (wenn überhaupt) vollständige LINQ to Sql-Anbieter. b) Verlängerungsmethoden machen eine Verlängerung (eines der grundlegenden OOP-Prinzipien) unmöglich.
jgauffin
0

Ich habe hier ein hübsches Muster für Abfrageobjekte für NHibernate geschrieben: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Es funktioniert mit einer Schnittstelle wie folgt:

public interface IDatabases
{
    ISessionManager SessionManager { get; }

    T Query<T>(IDatabaseQuery<T> query);
    T Query<T>(ICachedDatabaseQuery<T> query);

    void Command(IDatabaseCommand command);
    T Command<T>(IDatabaseCommand<T> command);
}

Bei einer POCO-Entitätsklasse wie folgt:

class Database1Poco
{
    public int Property1 { get; set; }
    public string Property2 { get; set; }
}

Sie können Abfrageobjekte wie folgt erstellen:

class Database1PocoByProperty1 : DatabaseQuery<Database1Poco>
{
    public override Database1Poco Execute(ISessionManager sessionManager)
    {
        return sessionManager.Session.Query<Database1Poco>().SingleOrDefault(x => x.Property1 == Property1);
    }

    public int Property1 { get; set; }
}

Und dann benutze sie so:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });
Shayne
quelle
1
Während dieser Link die Frage beantworten kann, ist es besser, die wesentlichen Teile der Antwort hier einzuschließen und den Link als Referenz bereitzustellen. Nur-Link-Antworten können ungültig werden, wenn sich die verlinkte Seite ändert.
Dan Pichelman
Entschuldigung, ich hatte es eilig. Antwort aktualisiert, um Beispielcode anzuzeigen.
Shayne
1
+1 für die Verbesserung des Beitrags mit mehr Inhalt.
Songo