Должны ли репозитории возвращать IQueryable?


66

Я видел много проектов, у которых есть репозитории, которые возвращают экземпляры IQueryable. Это позволяет выполнять дополнительные фильтры и сортировку IQueryableпо другому коду, что переводится в другой генерируемый SQL. Мне любопытно, откуда взялся этот паттерн и хорошая ли это идея.

Больше всего меня беспокоит то, что IQueryableобещание попасть в базу данных через некоторое время, когда она будет перечислена. Это означает, что ошибка будет выброшена за пределы хранилища. Это может означать, что исключение Entity Framework выдается на другом уровне приложения.

В прошлом я также сталкивался с проблемами с множественными активными наборами результатов (MARS) (особенно при использовании транзакций), и этот подход звучит так, как если бы это происходило чаще.

Я всегда вызывал AsEnumerableили ToArrayв конце каждого из своих выражений LINQ, чтобы убедиться, что база данных попадет перед выходом из кода репозитория.

Мне интересно, может ли возврат IQueryableбыть полезным в качестве строительного блока для уровня данных. Я видел довольно экстравагантный код с одним репозиторием, вызывающим другой репозиторий для создания еще большего IQueryable.


Какая разница, если база данных не была доступна во время выполнения отложенного запроса по сравнению со временем построения запроса? Потребляющий код должен был бы справиться с этой ситуацией независимо.
Стивен Эверс

Интересный вопрос, но на него, вероятно, будет сложно ответить. Простой поиск показывает довольно жаркие споры о вашем вопросе: duckduckgo.com/?q=repository+return+iqueryable
Corbin March

6
Все сводится к тому, хотите ли вы разрешить своим клиентам добавлять предложения Linq, которые могут выиграть от отложенного выполнения. Если они добавляют whereусловие к отложенному IQueryable, вам нужно только отправить эти данные по проводной связи, а не весь набор результатов.
Роберт Харви

Если вы используете шаблон репозитория - нет, вы не должны возвращать IQueryable. Вот хорошая причина .
Арнис Лапса

1
@ SteveEvers Существует огромная разница. Если в моем хранилище возникает исключение, я могу обернуть его типом исключения, специфичным для уровня данных. Если это произойдет позже, я должен захватить исходный тип исключения там. Чем ближе я к источнику исключения, тем больше вероятность, что я его знаю. Это важно для создания значимых сообщений об ошибках. ИМХО, разрешение специфичного для EF исключения покинуть мой репозиторий является нарушением инкапсуляции.
Трэвис Паркс

Ответы:


47

Возвращение IQueryable определенно предоставит потребителям хранилища больше гибкости. Это возлагает ответственность за сужение результатов на клиента, что, естественно, может быть как выгодой, так и опорой.

С другой стороны, вам не нужно создавать тонны методов репозитория (по крайней мере, на этом уровне) - GetAllActiveItems, GetAllNonActiveItems и т. Д. - для получения нужных данных. Опять же, в зависимости от ваших предпочтений, это может быть хорошо или плохо. Вы будете (/ должны) необходимо определить поведенческие контракты , которые ваши реализации прилипают, но когда это идет от вас.

Таким образом, вы можете поместить логику поиска извне вне хранилища и разрешить ее использование по желанию пользователя. Таким образом, использование IQueryable дает наибольшую гибкость и обеспечивает эффективные запросы, а не фильтрацию в памяти и т. Д., И может снизить необходимость использования множества специальных методов извлечения данных.

С другой стороны, теперь вы дали своим пользователям дробовик. Они могут делать то, что вы, возможно, не имели в виду (чрезмерное использование .include (), выполнение тяжелых тяжелых запросов и фильтрацию в памяти в соответствующих реализациях и т. Д.), Что в основном обойдёт стороной многоуровневый и поведенческий контроль, потому что вы дали полный доступ.

Таким образом, в зависимости от команды, их опыта, размера приложения, общего уровня и архитектуры ... это зависит


Обратите внимание, что вы можете, если вы хотите работать для него, вернуть свой IQueryable, который, в свою очередь, вызывает БД, но добавляет многоуровневые и поведенческие элементы управления.
PSR

1
@psr Не могли бы вы объяснить, что вы подразумеваете под этим?
Трэвис Паркс

1
@TravisParks - IQueryable не должен быть реализован БД, вы можете сами писать классы IQueryable - это довольно сложно. Таким образом, вы можете написать оболочку IQueryable вокруг базы данных IQueryable и иметь свой IQueryable, ограничивающий полный доступ к базовой БД. Но это достаточно сложно, чтобы я не ожидал, что это будет широко сделано.
PSR

2
Обратите внимание, что даже если вы не возвращаете IQueryables, вы все равно можете предотвратить создание многих методов (GetAllActiveItems, GetAllNonActiveItems и т. Д.), Просто передавая Expression<Func<Foo,bool>>свой метод. Эти параметры могут быть переданы Whereметоду напрямую, что позволяет потребителю выбирать свой фильтр без необходимости прямого доступа к IQueryable <Foo>.
Флейтер

1
@hanzolo: Зависит от того, что вы имеете в виду под рефакторингом. Это правда, что изменение дизайна (например, добавление новых полей или изменение отношений) более болезненно, когда у вас есть многоуровневая и слабо связанная кодовая база; но рефакторинг - это меньше боли, когда вам нужно сменить только один слой. Все зависит от того, ожидаете ли вы перепроектирования приложения или просто рефакторинг вашей реализации той же базовой архитектуры. Я бы все еще выступал за слабую связь в любом случае, но вы не ошиблись, что это добавило бы к рефакторингу при изменении основных концепций домена.
Флейтер

29

Предоставление IQueryable публичным интерфейсам не является хорошей практикой. Причина фундаментальна: вы не можете обеспечить реализацию IQueryable, как об этом говорят.

Открытый интерфейс - это договор между провайдером и клиентами. И в большинстве случаев ожидается полная реализация. IQueryable это чит:

  1. IQueryable практически невозможно реализовать. И в настоящее время нет правильного решения. Даже Entity Framework имеет множество исключений UnsupportedExceptions .
  2. Сегодня провайдеры запросов могут сильно зависеть от инфраструктуры. Sharepoint имеет свою частичную реализацию, а My SQL провайдер имеет отдельную частичную реализацию.

Шаблон репозитория дает нам четкое представление по слоям и, самое главное, независимость реализации. Таким образом, мы можем изменить в будущем источник данных.

Вопрос « Должна ли быть возможность замены источника данных? ».

Если завтра часть данных может быть перемещена в службы SAP, базу данных NoSQL или просто в текстовые файлы, можем ли мы гарантировать правильную реализацию интерфейса IQueryable?

( больше хороших моментов о том, почему это плохая практика)


Этот ответ не стоит сам по себе, не обращаясь к изменчивой внешней ссылке. Подумайте об обобщении, по крайней мере, ключевых моментов, высказанных внешним ресурсом, и постарайтесь не допустить личного мнения в ваш ответ («худшая идея, которую я слышал»). Также подумайте об удалении фрагмента кода, который, кажется, не связан ни с чем IQueryable.
Ларс Виклунд

1
Спасибо @LarsViklund, ответ обновляется с примерами и описанием проблемы
Artru

19

Реально, у вас есть три варианта, если вы хотите отсроченное выполнение:

  • Делай так - выставляй IQueryable.
  • Реализуйте структуру, которая предоставляет конкретные методы для определенных фильтров или «вопросов». ( GetCustomersInAlaskaWithChildrenниже)
  • Реализуйте структуру, которая предоставляет строго типизированный API фильтра / сортировки / страницы и строит IQueryableвнутренне.

Я предпочитаю третий (хотя я помогал коллегам также реализовать второй), но, очевидно, здесь есть некоторые настройки и обслуживание. (Т4 на помощь!)

Изменить : Чтобы устранить путаницу вокруг того, о чем я говорю, рассмотрите следующий пример, украденный из IQueryable Может убить вашу собаку, украсть вашу жену, убить вашу волю к жизни и т. Д.

В сценарии, где вы бы выставили что-то вроде этого:

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

API, о котором я говорю, позволил бы вам предоставить метод, который позволял бы вам GetCustomersInAlaskaWithChildrenгибко выражать (или любую другую комбинацию критериев), а репо выполнял бы его как IQueryable и возвращал бы вам результаты. Но важно то, что он работает внутри уровня хранилища, поэтому он все еще использует преимущества отложенного выполнения. Как только вы вернете результаты, вы все равно сможете привязать к ним объекты, которые вам нравятся.

Другое преимущество такого подхода состоит в том, что, поскольку они являются классами POCO, этот репозиторий может находиться за сетевым или WCF-сервисом; он может быть подвержен AJAX или другим абонентам, которые не знают в первую очередь о LINQ.


4
Итак, вы бы создали API запросов, чтобы заменить встроенный API запросов .NET?
Майкл Браун

4
Звучит довольно бессмысленно для меня
Оз

7
@MikeBrown Суть этого вопроса - и мой ответ - это то , что вы хотите , чтобы выставить поведение или преимущества от IQueryable не подвергая IQueryableсебя . Как вы собираетесь это сделать со встроенным API?
GalacticCowboy

2
Это очень хорошая тавтология. Вы не объяснили, почему вы хотите этого избежать. Heck WCF Data Services предоставляет IQueryable по сети. Я не пытаюсь понять тебя, я просто не понимаю, что такое отвращение к людям IQueryable.
Майкл Браун

2
Если вы просто собираетесь использовать IQueryable, то зачем вам вообще использовать репозиторий? Репозитории предназначены для сокрытия деталей реализации. (Кроме того, WCF Data Services несколько новее, чем большинство решений, над которыми я работал за последние 4 года, в которых использовался такой репозиторий ... Поэтому маловероятно, что они будут обновлены в ближайшее время, чтобы сделать использование блестящей новой игрушки.)
GalacticCowboy

8

На самом деле есть только один законный ответ: это зависит от того, как хранилище будет использоваться.

В одном крайнем случае ваш репозиторий представляет собой очень тонкую оболочку вокруг DBContext, поэтому вы можете внедрить тестируемость в приложение, управляемое базой данных. На самом деле нет никаких реальных ожиданий, что это может быть использовано автономно без поддержки дружественной БД LINQ, потому что вашему приложению CRUD это никогда не понадобится. Тогда конечно, почему бы не использовать IQueryable? Я бы, вероятно, предпочел бы IEnumarable, поскольку вы получаете большинство преимуществ [читай: отложенное выполнение], и он не выглядит таким грязным.

Если вы не сидите в таком экстремальном состоянии, я бы изо всех сил старался воспользоваться духом шаблона репозитория и вернуть соответствующие материализованные коллекции, которые не имеют никакого отношения к базовой связи с базой данных.


6
Что касается возвращения IEnumerable, важно отметить, что ((IEnumerable<T>)query).LastOrDefault()он сильно отличается от query.LastOrDefault()(предполагается, что queryэто IQueryable). Таким образом, имеет смысл возвращаться, IEnumerableесли вы все равно собираетесь перебирать все элементы.
Лу

7
 > Should Repositories return IQueryable?

НЕТ, если вы хотите провести тестирование на основе разработки / модульного тестирования, потому что нет простого способа создать фиктивный репозиторий для тестирования вашего businesslogic (= репозитория-потребителя) в изоляции от базы данных или другого поставщика IQueryable.


6

С точки зрения чистой архитектуры IQueryable - это утечка абстракций. Как правило, он предоставляет пользователю слишком много энергии. При этом есть места, где это имеет смысл. Если вы используете OData, IQueryable делает чрезвычайно простым предоставление конечной точки, которая легко фильтруется, сортируется, группируется ... и т. Д. Я на самом деле предпочитаю создавать DTO, на которые отображаются мои сущности, а IQueryable возвращает DTO. Таким образом, я предварительно фильтрую свои данные и действительно использую только метод IQueryable, чтобы разрешить клиенту настраивать результаты.


6

Я тоже столкнулся с таким выбором.

Итак, подведем итоги положительных и отрицательных сторон:

Положительных:

  • Гибкость. Это определенно гибкий и удобный способ позволить клиентской стороне создавать пользовательские запросы с помощью всего одного метода хранилища.

Отрицательных:

  • Не проверяется . (вы на самом деле будете тестировать базовую реализацию IQueryable, которая обычно является вашей ORM. Но вместо этого вы должны проверить свою логику)
  • Размывает ответственность . Невозможно сказать, что делает этот конкретный метод вашего хранилища. На самом деле, это Божий метод, который может вернуть все, что вам нравится во всей вашей базе данных.
  • Опасное . Без ограничений на то, что может быть запрошено этим методом хранилища, в некоторых случаях такие реализации могут быть небезопасными с точки зрения безопасности.
  • Проблемы с кэшированием (или, вообще говоря, любые проблемы до или после обработки). Например, обычно неразумно кэшировать все в своем приложении. Вы попытаетесь что-то кэшировать, что приведет к значительной загрузке вашей базы данных Но как это сделать, если у вас есть только один метод хранилища, который клиенты используют для создания практически любого запроса к базе данных?
  • Проблемы с регистрацией . Регистрация чего-либо на уровне хранилища заставит вас сначала проверить IQueryable, чтобы проверить, зарегистрирован этот конкретный вызов или нет.

Я бы рекомендовал не использовать этот подход. Вместо этого рассмотрите возможность создания нескольких спецификаций и реализации методов репозитория, которые могут запрашивать базу данных, используя их. Шаблон спецификации - отличный способ инкапсулировать набор условий.


5

Я думаю, что показ IQueryable в вашем хранилище вполне приемлем на начальных этапах разработки. Существуют инструменты пользовательского интерфейса, которые могут работать с IQueryable напрямую и обрабатывать сортировку, фильтрацию, группировку и т. Д.

При этом, я думаю, что просто разоблачить crud в хранилище и назвать его днем, безответственно. Наличие полезных операций и помощников по запросам для общих запросов позволяет централизовать логику для запроса, одновременно предоставляя потребителям хранилища меньшую поверхность интерфейса.

Для первых нескольких итераций проекта публичное раскрытие IQueryable в репозитории обеспечивает более быстрый рост. Перед первым полным выпуском я бы порекомендовал сделать операцию запроса частной или защищенной, а подстановочные запросы под одной крышей.


4

То, что я видел, сделано (и то, что я сам реализую):

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

Это позволяет вам гибко выполнять IQueryable.Where(a => a.SomeFilter)запросы к репо, concreteRepo.GetAll(a => a.SomeFilter)не подвергая риску бизнес LINQy.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.