Od dłuższego czasu szukałem dobrego rozwiązania problemów przedstawionych przez typowy wzorzec Repozytorium (rosnąca lista metod zapytań specjalistycznych itp. Patrz: http://ayende.com/blog/3955/repository- is-the-new-singleton ).
Bardzo podoba mi się pomysł używania zapytań poleceń, szczególnie przy użyciu wzorca Specification. Jednak mój problem ze specyfikacją polega na tym, że odnosi się ona tylko do kryteriów prostych selekcji (w zasadzie klauzula where) i nie zajmuje się innymi kwestiami zapytań, takimi jak łączenie, grupowanie, selekcja podzbiorów lub projekcja itp. w zasadzie wszystkie dodatkowe obręcze, przez które musi przejść wiele zapytań, aby uzyskać prawidłowy zestaw danych.
(uwaga: używam terminu „polecenie” we wzorcu polecenia, znanym również jako obiekty zapytań. Nie mówię o poleceniu, jak w przypadku separacji polecenie / zapytanie, w którym istnieje rozróżnienie między zapytaniami a poleceniami (aktualizacja, usuwanie, wstawić))
Dlatego szukam alternatyw, które obejmują całe zapytanie, ale są na tyle elastyczne, że nie wystarczy zamienić repozytoria spaghetti na eksplozję klas poleceń.
Użyłem na przykład Linqspecs i chociaż znajduję wartość w możliwości przypisywania znaczących nazw kryteriom wyboru, to po prostu nie wystarczy. Być może szukam mieszanego rozwiązania, które łączy wiele podejść.
Szukam rozwiązań, które mogli opracować inni, aby rozwiązać ten problem lub rozwiązać inny problem, ale nadal spełnia te wymagania. W powiązanym artykule Ayende sugeruje bezpośrednie użycie kontekstu nHibernate, ale wydaje mi się, że znacznie komplikuje to twoją warstwę biznesową, ponieważ teraz musi ona również zawierać informacje o zapytaniach.
Oferuję za to nagrodę, gdy tylko upłynie okres oczekiwania. Więc proszę, spraw, aby Twoje rozwiązania były godne uznania, z dobrymi wyjaśnieniami, a ja wybiorę najlepsze rozwiązanie i zagłosuję w górę.
UWAGA: Szukam czegoś opartego na ORM. Nie musi być jawnie EF lub nHibernate, ale te są najczęstsze i pasowałyby najlepiej. Jeśli można go łatwo dostosować do innych ORMów, byłby to bonus. Kompatybilny z Linq również byłby miły.
AKTUALIZACJA: Jestem naprawdę zaskoczony, że nie ma tu wielu dobrych sugestii. Wygląda na to, że ludzie są albo całkowicie CQRS, albo całkowicie w obozie Repozytorium. Większość moich aplikacji nie jest na tyle skomplikowana, aby gwarantować CQRS (coś, co większość zwolenników CQRS chętnie twierdzi, że nie należy go używać).
AKTUALIZACJA: Wydaje się, że jest tu trochę zamieszania. Nie szukam nowej technologii dostępu do danych, ale raczej dobrze zaprojektowanego interfejsu między biznesem a danymi.
Idealnie, to, czego szukam, to coś w rodzaju skrzyżowania obiektów Query, wzorca specyfikacji i repozytorium. Jak powiedziałem powyżej, wzorzec specyfikacji zajmuje się tylko aspektem klauzuli gdzie, a nie innymi aspektami zapytania, takimi jak łączenia, sub-selekcje itp. Repozytoria zajmują się całym zapytaniem, ale po chwili wymykają się spod kontroli . Obiekty zapytań również zajmują się całym zapytaniem, ale nie chcę po prostu zastępować repozytoriów eksplozjami obiektów zapytań.
źródło
Odpowiedzi:
Zastrzeżenie: Ponieważ nie ma jeszcze żadnych świetnych odpowiedzi, zdecydowałem się opublikować fragment świetnego posta na blogu, który przeczytałem jakiś czas temu, skopiowany prawie dosłownie. Możesz znaleźć pełny post na blogu tutaj . Więc oto jest:
Możemy zdefiniować dwa następujące interfejsy:
public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
W
IQuery<TResult>
Określa komunikat, który definiuje konkretne zapytanie z danymi to powraca użyciuTResult
typu rodzajowego. Za pomocą wcześniej zdefiniowanego interfejsu możemy zdefiniować komunikat zapytania w następujący sposób:public class FindUsersBySearchTextQuery : IQuery<User[]> { public string SearchText { get; set; } public bool IncludeInactiveUsers { get; set; } }
Ta klasa definiuje operację zapytania z dwoma parametrami, której wynikiem będzie tablica
User
obiektów. Klasę obsługującą tę wiadomość można zdefiniować w następujący sposób:public class FindUsersBySearchTextQueryHandler : IQueryHandler<FindUsersBySearchTextQuery, User[]> { private readonly NorthwindUnitOfWork db; public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db) { this.db = db; } public User[] Handle(FindUsersBySearchTextQuery query) { return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray(); } }
Teraz możemy pozwolić konsumentom polegać na ogólnym
IQueryHandler
interfejsie:public class UserController : Controller { IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler; public UserController( IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler) { this.findUsersBySearchTextHandler = findUsersBySearchTextHandler; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; User[] users = this.findUsersBySearchTextHandler.Handle(query); return View(users); } }
Od razu ten model daje nam dużą elastyczność, ponieważ możemy teraz zdecydować, co wstrzyknąć do
UserController
. Możemy wstrzyknąć zupełnie inną implementację lub taką, która otacza rzeczywistą implementację, bez konieczności wprowadzania zmian wUserController
(i wszystkich innych użytkownikach tego interfejsu).IQuery<TResult>
Interfejs daje nam czas kompilacji wsparcia przy określaniu lub wstrzyknięcieIQueryHandlers
w naszym kodzie. Kiedy zamiast tego zmienimyFindUsersBySearchTextQuery
zwracaną wartośćUserInfo[]
(przez implementacjęIQuery<UserInfo[]>
),UserController
kompilacja nie powiedzie się, ponieważ ograniczenie typu ogólnego naIQueryHandler<TQuery, TResult>
nie będzie mogło zostać zmapowaneFindUsersBySearchTextQuery
doUser[]
.Wstrzyknięcie
IQueryHandler
interfejsu do konsumenta wiąże się jednak z pewnymi mniej oczywistymi problemami, które nadal wymagają rozwiązania. Liczba zależności naszych konsumentów może być zbyt duża i może prowadzić do przeciążenia konstruktora - gdy konstruktor przyjmuje zbyt wiele argumentów. Liczba zapytań wykonywanych przez klasę może się często zmieniać, co wymagałoby ciągłych zmian w liczbie argumentów konstruktora.Możemy rozwiązać problem polegający na tym, że zbyt wiele
IQueryHandlers
z nich musimy wprowadzić dodatkową warstwę abstrakcji. Tworzymy mediatora, który znajduje się między konsumentami a obsługą zapytań:public interface IQueryProcessor { TResult Process<TResult>(IQuery<TResult> query); }
Jest
IQueryProcessor
to nieogólny interfejs z jedną ogólną metodą. Jak widać w definicji interfejsu,IQueryProcessor
zależy to odIQuery<TResult>
interfejsu. Dzięki temu możemy mieć obsługę czasu kompilacji u naszych konsumentów, która jest zależna odIQueryProcessor
. Przepiszmy,UserController
aby używał nowegoIQueryProcessor
:public class UserController : Controller { private IQueryProcessor queryProcessor; public UserController(IQueryProcessor queryProcessor) { this.queryProcessor = queryProcessor; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; // Note how we omit the generic type argument, // but still have type safety. User[] users = this.queryProcessor.Process(query); return this.View(users); } }
UserController
Teraz zależy na zasadzieIQueryProcessor
, że może obsługiwać wszystkie nasze pytania. WUserController
„SSearchUsers
sposób wywołujeIQueryProcessor.Process
sposób przechodzi w zainicjowany przedmiotem zapytania. PonieważFindUsersBySearchTextQuery
implementujeIQuery<User[]>
interfejs, możemy przekazać go doExecute<TResult>(IQuery<TResult> query)
metody generycznej . Dzięki wnioskowaniu o typie C # kompilator jest w stanie określić typ ogólny, co oszczędza nam konieczności jawnego określania typu.Process
Znany jest również zwracany typ metody.Obecnie obowiązkiem wdrożenia prawa
IQueryProcessor
jest znalezienie odpowiedniegoIQueryHandler
. Wymaga to pewnego dynamicznego pisania i opcjonalnie użycia struktury Dependency Injection, a wszystko to można zrobić za pomocą zaledwie kilku wierszy kodu:sealed class QueryProcessor : IQueryProcessor { private readonly Container container; public QueryProcessor(Container container) { this.container = container; } [DebuggerStepThrough] public TResult Process<TResult>(IQuery<TResult> query) { var handlerType = typeof(IQueryHandler<,>) .MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = container.GetInstance(handlerType); return handler.Handle((dynamic)query); } }
QueryProcessor
Klasa tworzy specyficznyIQueryHandler<TQuery, TResult>
rodzaj na podstawie typu dostarczonego przykład zapytania. Ten typ służy do żądania od podanej klasy kontenera pobrania wystąpienia tego typu. Niestety musimy wywołaćHandle
metodę z użyciem refleksji (w tym przypadku używając słowa kluczowego dymamic C # 4.0), ponieważ w tym momencie nie jest możliwe rzutowanie instancji handlera, ponieważTQuery
argument generyczny nie jest dostępny w czasie kompilacji. Jednak jeśliHandle
nazwa metody nie zostanie zmieniona lub nie otrzyma innych argumentów, to wywołanie nigdy się nie powiedzie, a jeśli chcesz, bardzo łatwo jest napisać test jednostkowy dla tej klasy. Korzystanie z odbicia może nieco spaść, ale nie ma się czym martwić.Aby odpowiedzieć na jedno z twoich obaw:
Konsekwencją korzystania z tego projektu jest to, że w systemie będzie wiele małych klas, ale posiadanie wielu małych / skupionych klas (z wyraźnymi nazwami) jest dobrą rzeczą. Takie podejście jest zdecydowanie lepsze niż posiadanie wielu przeciążeń z różnymi parametrami dla tej samej metody w repozytorium, ponieważ można je pogrupować w jednej klasie zapytania. Więc nadal masz dużo mniej klas zapytań niż metod w repozytorium.
źródło
TResult
parametrIQuery
interfejsu nie jest użyteczny. Jednak w mojej zaktualizowanej odpowiedziTResult
parametr jest używany przezProcess
metodę wIQueryProcessor
celu rozwiązaniaIQueryHandler
atrybutu at.IQueryable
i upewniając się, że nie wyliczam kolekcji, a następnie zQueryHandler
właśnie wywołanego / łańcuchowego zapytań. Dało mi to elastyczność testowania jednostkowego moich zapytań i łączenia ich w łańcuch. Mam usługę aplikacji na górzeQueryHandler
, a mój kontroler jest odpowiedzialny za bezpośrednią rozmowę z usługą zamiast zMój sposób radzenia sobie z tym jest w rzeczywistości uproszczony i agnostyczny dla ORM. Mój pogląd na repozytorium jest następujący: Zadaniem repozytorium jest dostarczenie aplikacji modelu wymaganego dla kontekstu, więc aplikacja po prostu pyta repozytorium o to , czego chce, ale nie mówi mu, jak to zdobyć.
Dostarczam metodzie repozytorium Kryteria (tak, w stylu DDD), które zostaną użyte przez repozytorium do utworzenia zapytania (lub cokolwiek jest wymagane - może to być żądanie usługi sieciowej). Łączenia i grupy imho to szczegóły tego, jak, a nie co i kryteria powinny być tylko podstawą do zbudowania klauzuli where.
Model = ostateczny obiekt lub struktura danych potrzebna aplikacji.
public class MyCriteria { public Guid Id {get;set;} public string Name {get;set;} //etc } public interface Repository { MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria); }
Prawdopodobnie możesz użyć kryteriów ORM (Nhibernate) bezpośrednio, jeśli chcesz. Implementacja repozytorium powinna wiedzieć, jak używać kryteriów z bazowym magazynem lub DAO.
Nie znam Twojej domeny i wymagań modelu, ale byłoby dziwne, gdyby najlepszym sposobem było, aby aplikacja sama zbudowała zapytanie. Model zmienia się tak bardzo, że nie można zdefiniować czegoś stabilnego?
To rozwiązanie wyraźnie wymaga dodatkowego kodu, ale nie łączy reszty z ORM lub czymkolwiek, z czego korzystasz, aby uzyskać dostęp do magazynu. Repozytorium spełnia swoje zadanie, działając jako fasada, a IMO jest czyste, a kod „tłumaczenia kryteriów” jest wielokrotnego użytku
źródło
Zrobiłem to, poparłem to i cofnąłem.
Główny problem jest następujący: nie ważne jak to zrobisz, dodatkowa abstrakcja nie zapewni Ci niezależności. Z definicji wycieknie. Zasadniczo wymyślasz całą warstwę tylko po to, aby twój kod wyglądał uroczo ... ale nie zmniejsza to konserwacji, nie poprawia czytelności ani nie daje ci żadnego rodzaju agnostycyzmu modelowego.
Zabawne jest to, że odpowiedziałeś na swoje własne pytanie w odpowiedzi na odpowiedź Oliviera: „jest to zasadniczo dublowanie funkcjonalności Linq bez wszystkich korzyści, jakie daje Linq”.
Zadaj sobie pytanie: jak to możliwe?
źródło
Możesz użyć płynnego interfejsu. Podstawową ideą jest to, że metody klasy zwracają bieżącą instancję tej samej klasy po wykonaniu jakiejś akcji. To pozwala ci łączyć wywołania metod.
Tworząc odpowiednią hierarchię klas, można stworzyć logiczny przepływ dostępnych metod.
public class FinalQuery { protected string _table; protected string[] _selectFields; protected string _where; protected string[] _groupBy; protected string _having; protected string[] _orderByDescending; protected string[] _orderBy; protected FinalQuery() { } public override string ToString() { var sb = new StringBuilder("SELECT "); AppendFields(sb, _selectFields); sb.AppendLine(); sb.Append("FROM "); sb.Append("[").Append(_table).AppendLine("]"); if (_where != null) { sb.Append("WHERE").AppendLine(_where); } if (_groupBy != null) { sb.Append("GROUP BY "); AppendFields(sb, _groupBy); sb.AppendLine(); } if (_having != null) { sb.Append("HAVING").AppendLine(_having); } if (_orderBy != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderBy); sb.AppendLine(); } else if (_orderByDescending != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderByDescending); sb.Append(" DESC").AppendLine(); } return sb.ToString(); } private static void AppendFields(StringBuilder sb, string[] fields) { foreach (string field in fields) { sb.Append(field).Append(", "); } sb.Length -= 2; } } public class GroupedQuery : FinalQuery { protected GroupedQuery() { } public GroupedQuery Having(string condition) { if (_groupBy == null) { throw new InvalidOperationException("HAVING clause without GROUP BY clause"); } if (_having == null) { _having = " (" + condition + ")"; } else { _having += " AND (" + condition + ")"; } return this; } public FinalQuery OrderBy(params string[] fields) { _orderBy = fields; return this; } public FinalQuery OrderByDescending(params string[] fields) { _orderByDescending = fields; return this; } } public class Query : GroupedQuery { public Query(string table, params string[] selectFields) { _table = table; _selectFields = selectFields; } public Query Where(string condition) { if (_where == null) { _where = " (" + condition + ")"; } else { _where += " AND (" + condition + ")"; } return this; } public GroupedQuery GroupBy(params string[] fields) { _groupBy = fields; return this; } }
Możesz to tak nazwać
string query = new Query("myTable", "name", "SUM(amount) AS total") .Where("name LIKE 'A%'") .GroupBy("name") .Having("COUNT(*) > 2") .OrderBy("name") .ToString();
Możesz tylko utworzyć nowe wystąpienie
Query
. Pozostałe klasy mają chronionego konstruktora. Celem hierarchii jest „wyłączenie” metod. Na przykładGroupBy
metoda zwracaGroupedQuery
klasę bazowąQuery
i nie maWhere
metody (metoda where jest zadeklarowana wQuery
). Dlatego nie można dzwonićWhere
poGroupBy
.Nie jest jednak doskonały. Dzięki tej hierarchii klas możesz sukcesywnie ukrywać członków, ale nie pokazywać nowych. Dlatego
Having
zgłasza wyjątek, gdy jest wywoływany wcześniejGroupBy
.Pamiętaj, że można dzwonić
Where
kilka razy. Powoduje to dodanie nowych warunkówAND
do istniejących warunków. Ułatwia to programowe tworzenie filtrów na podstawie pojedynczych warunków. To samo jest możliwe zHaving
.Metody akceptujące listy pól mają parametr
params string[] fields
. Pozwala na przekazywanie nazw pojedynczych pól lub tablicy ciągów.Fluent interfejsy są bardzo elastyczne i nie wymagają tworzenia wielu przeciążeń metod z różnymi kombinacjami parametrów. Mój przykład działa ze stringami, jednak podejście to można rozszerzyć na inne typy. Możesz również zadeklarować predefiniowane metody dla przypadków specjalnych lub metody akceptujące typy niestandardowe. Możesz także dodać metody takie jak
ExecuteReader
lubExceuteScalar<T>
. Umożliwiłoby to zdefiniowanie takich zapytańvar reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true }) .Where(new CurrentMonthCondition()) .Where(new DivisionCondition{ DivisionType = DivisionType.Production}) .OrderBy(new StandardMonthlyReportSorting()) .ExecuteReader();
Nawet polecenia SQL skonstruowane w ten sposób mogą mieć parametry poleceń, a tym samym uniknąć problemów z wstrzykiwaniem SQL i jednocześnie umożliwiać buforowanie poleceń przez serwer bazy danych. Nie jest to zamiennik dla programu odwzorowującego O / R, ale może pomóc w sytuacjach, w których w przeciwnym razie utworzyłbyś polecenia przy użyciu prostej konkatenacji ciągów.
źródło