Alternatywy dla wzorca repozytorium do enkapsulacji logiki ORM?

24

Właśnie musiałem zmienić ORM i było to dość trudne zadanie, ponieważ logika zapytań przeciekała wszędzie. Gdybym kiedykolwiek musiał opracować nową aplikację, osobiście wolę enkapsulować całą logikę zapytań (używając ORM), aby zabezpieczyć ją przed zmianami. Wzorzec repozytorium jest dość kłopotliwy dla kodu i utrzymania, więc zastanawiałem się, czy istnieją jakieś inne wzorce rozwiązania problemu?

Mogę przewidzieć posty o nie dodawaniu dodatkowej złożoności, zanim będzie to naprawdę potrzebne, o zwinności itp., Ale interesują mnie tylko istniejące wzorce rozwiązujące podobny problem w prostszy sposób.

Moją pierwszą myślą było stworzenie repozytorium typu ogólnego, do którego w razie potrzeby dodaję metody do klas repozytorium typu za pomocą metod rozszerzenia, ale testowanie jednostkowe metod statycznych jest strasznie bolesne. TO ZNACZY:

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}
Dante
źródło
5
Co dokładnie jest związane z wzorcem repozytorium, który sprawia ci kłopot w kodowaniu i utrzymaniu?
Matthew Flynn
1
Istnieją alternatywy. Jednym z nich jest użycie wzorca obiektu zapytania, który choć nie był używany, wydaje się dobrym podejściem. Repozytorium IMHO jest dobrym wzorcem, jeśli jest właściwie stosowane, ale nie jest wszystkim i kończy się wszystko
dreza

Odpowiedzi:

14

Przede wszystkim: ogólne repozytorium należy uznać za klasę podstawową, a nie za pełne implementacje. Powinno to pomóc w wprowadzeniu powszechnych metod CRUD. Ale nadal musisz zaimplementować metody zapytania:

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

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

Druga:

Nie wracaj IQueryable. To nieszczelna abstrakcja. Spróbuj zaimplementować ten interfejs samodzielnie lub korzystać z niego bez wiedzy o podstawowym dostawcy db. Na przykład każdy dostawca db ma własny interfejs API do chętnego ładowania jednostek. Dlatego jest to nieszczelna abstrakcja.

Alternatywny

Alternatywnie możesz użyć zapytań (na przykład zdefiniowanych we wzorze rozdzielania poleceń / zapytań). CQS opisano w wikipedii.

Zasadniczo tworzysz klasy zapytań, które wywołujesz:

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


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

wielką zaletą zapytań jest to, że można używać różnych metod dostępu do danych dla różnych zapytań bez zaśmiecania kodu. możesz na przykład użyć usługi internetowej w jednej, nhibernate w drugiej, a ADO.NET w trzeciej.

Jeśli jesteś zainteresowany implementacją .NET, przeczytaj mój artykuł: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/

jgauffin
źródło
Czy alternatywny wzorzec, który zasugerowałeś, nie stworzyłby jeszcze większego bałaganu w większych projektach, skoro jakaś metoda we Wzorcu repozytorium jest teraz całą klasą?
Dante
3
Jeśli twoja definicja posiadania małych klas jest zagracona, cóż, tak. W takim przypadku nie powinieneś również próbować przestrzegać zasad SOLID, ponieważ ich zastosowanie zawsze spowoduje powstanie większej liczby (mniejszych) klas.
jgauffin
2
Mniejsze klasy są lepsze, ale czy nawigacja w istniejących klasach zapytań nie byłaby bardziej kłopotliwa niż, powiedzmy, przeglądanie inteligencji repozytorium (domeny), aby sprawdzić, czy istnieje niezbędna funkcjonalność, a jeśli nie, to zaimplementuj ją.
Dante
4
Struktura projektu staje się ważniejsza. Jeśli umieścisz wszystkie zapytania w przestrzeni nazw, takiej jak YourProject.YourDomainModelName.Queriesłatwa w nawigacji.
jgauffin
Jedną z rzeczy, które trudno mi zrobić w dobry sposób z CQS, są typowe zapytania. Na przykład, dane użytkownikowi, jedno zapytanie otrzymuje wszystkich powiązanych studentów (stopnie różnic, własne dzieci itp.). Teraz kolejne zapytanie musi pobrać dzieci dla użytkownika, a następnie wykonać na nich pewne operacje - więc zapytanie 2 może wykorzystać wspólną logikę w zapytaniu 1, ale nie ma prostego sposobu na zrobienie tego. Nie udało mi się znaleźć żadnej implementacji CQS, która by to ułatwiła. Repozytoria bardzo pomagają w tym zakresie. Nie jestem fanem żadnego wzorca, ale po prostu dzielę się moim zdaniem.
Mrchief
8

Najpierw myślę, że mówię, że ORM jest już wystarczająco duża abstrakcja w bazie danych. Niektóre ORM zapewniają powiązanie ze wszystkimi popularnymi relacyjnymi bazami danych (NHibernate ma powiązania z MSSQL, Oracle, MySQL, Postgress, itp.). Więc tworzenie nowej abstrakcji nie wydaje mi się opłacalne. Argumentowanie, że trzeba „wyabstrahować” tę ORM, jest bez znaczenia.

Jeśli nadal chcesz zbudować tę abstrakcję, byłbym przeciwny wzorowi repozytorium. Był świetny w erze czystego SQL, ale jest dość kłopotliwy w obliczu współczesnej ORM. Głównie dlatego, że ostatecznie wdrażasz większość funkcji ORM, takich jak operacje CRUD i zapytania.

Gdybym miał budować takie abstrakcje, użyłbym tych zasad / wzorów

  • CQRS
  • Wykorzystaj jak najwięcej istniejących funkcji ORM
  • Hermetyzuj tylko złożone logiki i zapytania
  • Staraj się ukryć „architekturę” ORM w architekturze

W konkretnej implementacji użyłbym operacji CRUD ORM bezpośrednio, bez owijania. Zrobiłbym również proste zapytania bezpośrednio w kodzie. Ale złożone zapytania byłyby zamknięte w ich własnych obiektach. Aby „ukryć” ORM, próbowałbym transparentnie wstrzyknąć kontekst danych do obiektów usługi / interfejsu użytkownika i zrobić to samo z obiektami zapytań.

Ostatnią rzeczą, którą chciałbym powiedzieć, jest to, że wiele osób korzysta z ORM, nie wiedząc, jak go używać i jak najlepiej z niego „czerpać zyski”. Ludzie polecający repozytoria są zwykle tego rodzaju.

Jako lekturę polecam blog Ayende , szczególnie ten artykuł .

Euforyk
źródło
+1 „Ostatnią rzeczą, którą chciałbym powiedzieć, jest to, że wiele osób korzysta z ORM, nie wiedząc, jak z niego korzystać i jak uzyskać z niego jak najlepsze„ zyski ”. Ludzie polecający repozytoria są zwykle tego rodzaju”
Misters
1

Problem z nieszczelną implementacją polega na tym, że potrzebujesz wielu różnych warunków filtrowania.

Możesz zawęzić ten interfejs, jeśli zaimplementujesz metodę repozytorium FindByExample i użyjesz jej w ten sposób

// 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
źródło
0

Zastanów się, czy twoje rozszerzenia działają na IQueryable zamiast IRepository.

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

Aby przeprowadzić test jednostkowy:

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

Do testowania nie jest wymagane wyśmiewanie kibiców. Możesz także łączyć te metody rozszerzeń, aby w razie potrzeby tworzyć bardziej złożone zapytania.

Jared S.
źródło
5
-1 a) IQueryablejest złą matką, a raczej interfejsem BOGA. Wdrażanie go jest bardzo potrzebne i jest bardzo mało (jeśli w ogóle) kompletnych LINQ do dostawców Sql. b) Metody przedłużania uniemożliwiają przedłużenie (jedna z podstawowych zasad OOP).
jgauffin
0

Napisałem całkiem fajny wzorzec obiektu zapytania dla NHibernate tutaj: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Działa przy użyciu takiego interfejsu:

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);
}

Biorąc pod uwagę klasę jednostki POCO taką jak ta:

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

Możesz budować obiekty zapytania takie jak to:

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; }
}

A następnie użyj ich w następujący sposób:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });
Shayne
źródło
1
Chociaż ten link może odpowiedzieć na pytanie, lepiej jest dołączyć tutaj istotne części odpowiedzi i podać link w celach informacyjnych. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie.
Dan Pichelman,
Przepraszam, spieszyłem się. Odpowiedź zaktualizowana, aby pokazać przykładowy kod.
Shayne
1
+1 za ulepszenie posta z większą zawartością.
Songo,