Czy repozytoria powinny zwracać IQueryable?

66

Widziałem wiele projektów z repozytoriami, które zwracają instancje IQueryable. Pozwala to na dodatkowe filtry, a sortowanie może odbywać się IQueryablewedług innego kodu, co przekłada się na generowanie innego kodu SQL. Jestem ciekawy, skąd wziął się ten wzór i czy to dobry pomysł.

Moją największą obawą jest to, że IQueryableobietnica trafi do bazy danych jakiś czas później, kiedy zostanie wyliczona. Oznacza to, że błąd zostanie wyrzucony poza repozytorium. Może to oznaczać, że wyjątek Entity Framework zostanie zgłoszony w innej warstwie aplikacji.

W przeszłości miałem również problemy z wieloma aktywnymi zestawami wyników (MARS) (szczególnie przy korzystaniu z transakcji) i takie podejście wydaje się, że częściej by się tak działo.

Zawsze wywoływałem AsEnumerablelub ToArrayna końcu każdego wyrażenia LINQ, aby upewnić się, że baza danych została trafiona przed opuszczeniem kodu repozytorium.

Zastanawiam się, czy zwracanie IQueryablemoże być przydatne jako element składowy warstwy danych. Widziałem dość ekstrawagancki kod z jednym repozytorium wywołującym inne repozytorium w celu zbudowania jeszcze większego IQueryable.

Travis Parks
źródło
Jaka byłaby różnica, gdyby baza danych nie była dostępna w czasie odroczonego wykonania zapytania w porównaniu do czasu budowy zapytania? Niezależnie od tego kod zużywający musiałby poradzić sobie z tą sytuacją.
Steven Evers
Ciekawe pytanie, ale prawdopodobnie trudno będzie na nie odpowiedzieć. Proste wyszukiwanie pokazuje dość gorącą debatę na temat twojego pytania: duckduckgo.com/?q=repository+return+iqueryable
Corbin
6
Wszystko sprowadza się do tego, czy chcesz pozwolić swoim klientom dodawać klauzule Linq, które mogłyby skorzystać z odroczonego wykonania. Jeśli dodają whereklauzulę do odroczonego IQueryable, wystarczy wysłać te dane przewodowo, a nie cały zestaw wyników.
Robert Harvey
Jeśli używasz wzorca repozytorium - nie, nie powinieneś zwracać IQueryable. Oto fajne dlaczego .
Arnis Lapsa
1
@SteveEvers Istnieje ogromna różnica. Jeśli w moim repozytorium zostanie zgłoszony wyjątek, mogę owinąć go typem wyjątku specyficznym dla warstwy danych. Jeśli stanie się to później, muszę przechwycić oryginalny typ wyjątku. Im bliżej jestem źródła wyjątku, tym bardziej prawdopodobne, że będę wiedział, co go spowodowało. Jest to ważne przy tworzeniu znaczących komunikatów o błędach. IMHO, zezwalanie na opuszczenie mojego repozytorium wyjątku specyficznego dla EF, stanowi naruszenie enkapsulacji.
Travis Parks

Odpowiedzi:

47

Zwrócenie IQueryable z pewnością zapewni większą elastyczność konsumentom repozytorium. Odrzuca odpowiedzialność zawężenia wyników do klienta, co oczywiście może być zarówno korzyścią, jak i kulą.

Z drugiej strony nie trzeba tworzyć wielu metod repozytorium (przynajmniej na tej warstwie) - GetAllActiveItems, GetAllNonActiveItems itp. - aby uzyskać potrzebne dane. W zależności od preferencji może to być dobre lub złe. Ci będą (/ powinien) należy zdefiniować umów behawioralne, które twoi implementacje przestrzegania, ale jeżeli idzie to do ciebie.

Dzięki temu możesz umieścić logikę pobierania poza repozytorium i pozwolić, aby była używana tak, jak chce użytkownik. Zatem udostępnienie IQueryable daje największą elastyczność i pozwala na wydajne zapytania w przeciwieństwie do filtrowania w pamięci itp., I może zmniejszyć potrzebę tworzenia mnóstwa określonych metod pobierania danych.

Z drugiej strony, teraz dałeś swoim użytkownikom strzelbę. Mogą robić rzeczy, których nie zamierzałeś (nadużywanie .include (), robienie ciężkich zapytań i filtrowanie w pamięci w ich odpowiednich implementacjach itp.), Które zasadniczo pomogłyby w nakładaniu warstw i kontroli behawioralnej, ponieważ dałeś pełny dostęp.

Więc w zależności od zespołu, ich doświadczenia, wielkości aplikacji, ogólnego nakładania warstw i architektury… to zależy: - \

hanzolo
źródło
Zauważ, że możesz, jeśli chcesz na to popracować, zwrócić IQueryable, który z kolei wywołuje DB, ale dodaje warstwy i kontrolę behawioralną.
psr
1
@psr Czy możesz wyjaśnić, co przez to rozumiesz?
Travis Parks
1
@TravisParks - IQueryable nie musi być implementowany przez DB, możesz samodzielnie pisać klasy IQueryable - to po prostu dość trudne. Możesz więc napisać opakowanie IQueryable wokół bazy danych IQueryable i mieć swój IQueryable limit pełnego dostępu do bazowej bazy danych. Ale to jest na tyle trudne, że nie spodziewałbym się, że będzie to powszechnie robione.
psr
2
Zauważ, że nawet jeśli nie zwrócisz IQueryables, nadal możesz zapobiec konieczności tworzenia wielu metod (GetAllActiveItems, GetAllNonActiveItems itp.), Po prostu przekazując Expression<Func<Foo,bool>>do swojej metody. Parametry te można przekazać Wherebezpośrednio do metody, umożliwiając konsumentowi wybór filtra bez potrzeby bezpośredniego dostępu do IQueryable <Foo>.
Flater,
1
@hanzolo: Zależy od tego, co masz na myśli przez refaktoryzację. To prawda, że ​​zmiana projektu (np. Dodanie nowych pól lub zmiana relacji) jest bardziej uciążliwa, gdy masz warstwową i luźno sprzężoną bazę kodu; ale refaktoryzacja jest mniej uciążliwa, gdy trzeba zmienić tylko jedną warstwę. Wszystko zależy od tego, czy spodziewasz się przeprojektowania aplikacji, czy też po prostu refaktoryzujesz implementację tej samej podstawowej architektury. W każdym razie nadal opowiadałbym się za luźnym sprzężeniem, ale nie ma się co do tego, że przyczynia się to do refaktoryzacji przy zmianie podstawowych koncepcji domen.
Flater,
29

Udostępnianie IQueryable interfejsom publicznym nie jest dobrą praktyką. Powód jest fundamentalny: nie można zapewnić IQueryable realizacji, jak się mówi.

Interfejs publiczny to umowa między dostawcą a klientami. W większości przypadków oczekuje się pełnego wdrożenia. IQueryable to oszustwo:

  1. IQueryable jest prawie niemożliwe do wdrożenia. A obecnie nie ma odpowiedniego rozwiązania. Nawet Entity Framework ma wiele nieobsługiwanych wyjątków .
  2. Obecnie dostawcy, których dotyczy zapytanie, są w dużym stopniu zależni od infrastruktury. Program Sharepoint ma własną częściową implementację, a dostawca My SQL ma wyraźną częściową implementację.

Wzór repozytorium daje nam wyraźną reprezentację poprzez warstwowanie, a co najważniejsze, niezależność realizacji. Możemy więc zmienić przyszłe źródło danych.

Pytanie brzmi: „ Czy powinna istnieć możliwość zastąpienia źródła danych? ”.

Jeśli jutro część danych można przenieść do usług SAP, bazy danych NoSQL lub po prostu do plików tekstowych, czy możemy zagwarantować prawidłowe wdrożenie interfejsu IQueryable?

( więcej dobrych uwag na temat tego, dlaczego jest to zła praktyka)

Artru
źródło
Ta odpowiedź nie jest sama w sobie bez konsultacji z niestabilnym linkiem zewnętrznym. Zastanów się nad podsumowaniem przynajmniej kluczowych punktów poczynionych przez zasoby zewnętrzne i staraj się ukrywać swoją opinię przed odpowiedzią („najgorszy pomysł, jaki słyszałem”). Zastanów się także nad usunięciem fragmentu kodu, który wydaje się niezwiązany z niczym IQueryable.
Lars Viklund,
1
Dziękuję @LarsViklund, odpowiedź została zaktualizowana o przykłady i opis problemu
Artru,
19

Realistycznie masz trzy możliwości, jeśli chcesz odroczyć wykonanie:

  • Zrób to w ten sposób - ujawnij IQueryable.
  • Zaimplementuj strukturę, która udostępnia określone metody dla określonych filtrów lub „pytań”. ( GetCustomersInAlaskaWithChildrenponiżej)
  • Zaimplementuj strukturę, która udostępnia silnie typowany interfejs API filtru / sortowania / strony i buduje IQueryablewewnętrznie.

Wolę trzeci (chociaż pomogłem kolegom wdrożyć drugi), ale oczywiście wiąże się to z pewną konfiguracją i konserwacją. (T4 na ratunek!)

Edycja : Aby usunąć zamieszanie wokół tego, o czym mówię, rozważ następujący przykład skradziony IQueryable może zabić psa, ukraść żonę, zabić wolę życia itp.

W scenariuszu, w którym ujawniłbyś coś takiego:

public class CustomerRepo : IRepo 
{ 
     private DataContext ct; 
     public Customer GetCustomerById(int id) { ... } 
     public Customer[] GetCustomersInAlaskaWithChildren() { ... } 
} 

API, o którym mówię, pozwoliłoby ci ujawnić metodę, która pozwoliłaby ci wyrazić GetCustomersInAlaskaWithChildren(lub dowolną inną kombinację kryteriów) w elastyczny sposób, a repo wykonałoby to jako IQueryable i zwróci ci wyniki. Ale ważne jest to, że działa w warstwie repozytorium, więc nadal korzysta z odroczonego wykonania. Po odzyskaniu wyników nadal możesz LINQ-do-obiektów na nim, aby zadowolić swoje serce.

Kolejną zaletą takiego podejścia jest to, że ponieważ są to klasy POCO, to repozytorium może żyć za usługą WWW lub WCF; może być narażony na AJAX lub inne osoby dzwoniące, które nie znają pierwszej rzeczy na temat LINQ.

GalacticCowboy
źródło
4
Więc zbudowałbyś API zapytań, aby zastąpić wbudowane API .NET zapytań?
Michael Brown
4
Brzmi dla mnie całkiem bez sensu
oz.
7
@MikeBrown punktu to pytanie - i moją odpowiedź - jest to, że chcesz, aby odsłonić zachowanie lub te zalety z IQueryable bez narażania IQueryablesię . Jak zamierzasz to zrobić dzięki wbudowanemu interfejsowi API?
GalacticCowboy
2
To bardzo dobra tautologia. Nie wyjaśniłeś, dlaczego chcesz tego uniknąć. Heck WCF Data Services ujawnia IQueryable w poprzek drutu. Nie próbuję cię zaczepiać, po prostu nie rozumiem tej niechęci do IQueryable.
Michael Brown
2
Jeśli zamierzasz użyć IQueryable, to dlaczego miałbyś w ogóle używać repozytorium? Repozytoria mają na celu ukrycie szczegółów implementacji. (Ponadto WCF Data Services jest nieco nowszy niż większość rozwiązań, nad którymi pracowałem w ciągu ostatnich 4 lat, które korzystały z takiego repozytorium ... Więc nie jest prawdopodobne, że zostaną zaktualizowane w najbliższym czasie, aby tylko zrobić użycie nowej błyszczącej zabawki.)
GalacticCowboy
8

Naprawdę jest tylko jedna prawidłowa odpowiedź: zależy to od sposobu użycia repozytorium.

Z jednej strony repozytorium jest bardzo cienkim opakowaniem wokół DBContext, dzięki czemu można wstrzyknąć okleinę testowalności wokół aplikacji sterowanej bazą danych. Naprawdę nie ma rzeczywistych oczekiwań, że można by tego użyć w sposób rozłączony bez przyjaznego DB LINQ, ponieważ twoja aplikacja CRUD nigdy nie będzie tego potrzebować. Więc na pewno, dlaczego nie użyć IQueryable? Prawdopodobnie wolałbym IEnumarable, ponieważ dostajesz większość korzyści [czytaj: opóźnione wykonanie] i nie czuje się tak brudny.

O ile nie siedzisz w tak ekstremalnej sytuacji, starałbym się wykorzystać duch wzorca repozytorium i zwrócić odpowiednie zmaterializowane kolekcje, które nie mają wpływu na podstawowe połączenie z bazą danych.

Wyatt Barnett
źródło
6
Jeśli chodzi o powrót IEnumerable, ważne jest, aby pamiętać, że ((IEnumerable<T>)query).LastOrDefault()różni się bardzo od query.LastOrDefault()(zakładając, że queryjest IQueryable). Dlatego warto wrócić, IEnumerablejeśli i tak będziesz przechodzić przez wszystkie elementy.
Lou
7
 > Should Repositories return IQueryable?

NIE, jeśli chcesz przeprowadzić testowanie / testowanie, ponieważ nie ma łatwego sposobu na utworzenie próbnego repozytorium w celu przetestowania swojego businesslogic (= repozytorium-konsument) w izolacji od bazy danych lub innego dostawcy IQueryable.

k3b
źródło
6

Z czystego punktu widzenia architektura IQueryable jest nieszczelną abstrakcją. Zasadniczo zapewnia użytkownikowi zbyt dużą moc. Biorąc to pod uwagę, są miejsca, w których ma to sens. Jeśli używasz OData, IQueryable sprawia, że ​​niezwykle łatwo jest zapewnić punkt końcowy, który można łatwo filtrować, sortować, grupować ... itd. W rzeczywistości wolę tworzyć DTO, na które odwzorowują moje byty, a IQueryable zwraca DTO. W ten sposób wstępnie filtruję moje dane i naprawdę używam tylko metody IQueryable, aby umożliwić klientom dostosowanie wyników.

Slick86
źródło
6

Taki wybór też miałem.

Podsumujmy więc pozytywne i negatywne strony:

Zalety:

  • Elastyczność. Jest zdecydowanie elastyczny i wygodny, umożliwiając stronie klienta tworzenie niestandardowych zapytań za pomocą tylko jednej metody repozytorium

Negatywne:

  • Niestabilny . (w rzeczywistości będziesz testować podstawową implementację IQueryable, która zwykle jest twoją ORM. Zamiast tego powinieneś przetestować swoją logikę)
  • Zaciera odpowiedzialność . Nie można powiedzieć, co robi ta konkretna metoda twojego repozytorium. W rzeczywistości jest to boska metoda, która może zwrócić cokolwiek zechcesz w całej bazie danych
  • Niebezpieczne . Bez ograniczeń dotyczących tego, co można przeszukiwać za pomocą tej metody repozytorium, w niektórych przypadkach takie implementacje mogą być niebezpieczne z punktu widzenia bezpieczeństwa.
  • Problemy z buforowaniem (lub, ogólnie mówiąc, wszelkie problemy przed lub po przetwarzaniu). Na ogół niemądre jest na przykład buforowanie wszystkiego w aplikacji. Spróbujesz buforować coś, co spowoduje znaczne obciążenie bazy danych. Ale jak to zrobić, jeśli masz tylko jedną metodę repozytorium, której klienci używają do wydawania praktycznie dowolnego zapytania do bazy danych?
  • Problemy z logowaniem . Rejestrowanie czegoś w warstwie repozytorium sprawi, że najpierw sprawdzisz IQueryable, aby sprawdzić, czy to konkretne połączenie zostanie zarejestrowane, czy nie.

Odradzałbym stosowanie tego podejścia. Zamiast tego rozważ utworzenie kilku Specyfikacji i zaimplementuj metody repozytorium, które mogą wyszukiwać w bazie danych za ich pomocą. Wzorzec specyfikacji to świetny sposób na zawarcie zestawu warunków.

Vladislav Rastrusny
źródło
5

Myślę, że ujawnienie IQueryable w twoim repozytorium jest całkowicie akceptowalne na początkowych etapach rozwoju. Istnieją narzędzia interfejsu użytkownika, które mogą bezpośrednio współpracować z IQueryable i obsługiwać sortowanie, filtrowanie, grupowanie itp.

Biorąc to pod uwagę, myślę, że samo ujawnienie surowca w repozytorium i nazwanie go dniem jest nieodpowiedzialne. Posiadanie przydatnych operacji i pomocników zapytań dla typowych zapytań pozwala scentralizować logikę zapytania, jednocześnie pokazując mniejszą powierzchnię interfejsu konsumentom repozytorium.

W przypadku pierwszych kilku iteracji projektu publiczne udostępnienie IQueryable w repozytorium pozwala na szybszy rozwój. Przed pierwszą pełną wersją zalecam uczynienie operacji zapytania prywatną lub chronioną i umieszczenie dzikich zapytań pod jednym dachem.

Michael Brown
źródło
4

To, co widziałem (a co sam realizuję), to:

public interface IEntity<TKey>
{
    TKey Id { get; set; }
}

public interface IRepository<TEntity, in TKey> where TEntity : IEntity<TKey>
{
    void Create(TEntity entity);
    TEntity Get(TKey key);
    IEnumerable<TEntity> GetAll();
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> expression);
    void Update(TEntity entity);
    void Delete(TKey id);
}

Umożliwia to elastyczność wykonywania IQueryable.Where(a => a.SomeFilter)zapytań w repozytorium concreteRepo.GetAll(a => a.SomeFilter)bez ujawniania jakiejkolwiek działalności LINQy.

dav_i
źródło