Чи повинні репозиторії повертати IQueryable?


66

Я бачив багато проектів, у яких є сховища, які повертають екземпляри IQueryable. Це дозволяє додаткові фільтри та сортування можна виконувати IQueryableза іншим кодом, що перекладається на різні генеровані SQL. Мені цікаво, звідки взялася ця закономірність і чи це гарна ідея.

Моя найбільша стурбованість - IQueryableце обіцянка потрапити в базу даних через деякий час, коли вона буде перерахована. Це означає, що помилка буде викинута за межі сховища. Це може означати, що виняток Entity Framework викидається в інший рівень програми.

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

Я завжди дзвонив AsEnumerableабо ToArrayв кінці кожного з моїх виразів LINQ, щоб переконатися, що база даних потрапила, перш ніж залишати код сховища.

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


Яка була б різниця, якщо db не був доступний під час виконання відкладеного запиту проти часу побудови запиту? Код споживання повинен мати справу з цією ситуацією незалежно.
Стівен Еверс

Цікаве запитання, але, напевно, важко відповісти. Простий пошук показує досить бурхливу дискусію щодо вашого запитання: duckduckgo.com/?q=repository+return+iqueryable
Корбін

6
Все зводиться до того, чи хочете ви дозволити своїм клієнтам додавати пропозиції Linq, які могли б отримати користь від відкладеного виконання. Якщо вони додають whereпропозицію до відкладеного IQueryable, вам потрібно буде надіслати ці дані лише через провід, а не весь набір результатів.
Роберт Харві

Якщо ви використовуєте шаблон репозиторію - ні, вам не слід повертати IQueryable. Ось приємно чому .
Арніс Лапса

1
@SteveEvers Існує величезна різниця. Якщо у моєму сховищі буде викинуто виняток, я можу обернути його типом винятків для рівня даних. Якщо це станеться пізніше, я маю захопити там вихідний тип винятку. Чим ближче я до джерела винятку, тим більше шансів дізнатися, що це спричинило. Це важливо для створення змістовних повідомлень про помилки. IMHO, що дозволяє виключенням, що стосується EF, залишати моє сховище - це порушення інкапсуляції.
Travis Parks

Відповіді:


47

Повернення IQueryable, безумовно, надасть більшу гнучкість споживачам сховища. Це покладає на клієнта відповідальність за звуження результатів, що, природно, може принести користь і суть.

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

Таким чином, ви можете розмістити логіку вишуканого пошуку поза сховищем і дозволити її використовувати, як хоче користувач. Таким чином, опромінення IQueryable дає найбільшу гнучкість і дозволяє ефективно запитувати, на відміну від фільтрації в пам'яті тощо, і може зменшити потребу в створенні тонни конкретних методів отримання даних.

З іншого боку, тепер ви дали своїм користувачам рушницю. Вони можуть робити те, чого ви, можливо, не планували (зловживайте .include (), роблячи великі важкі запити та здійснюючи фільтрацію в пам’яті у відповідних реалізаціях тощо), що в основному може поступово діяти на етапі управління шарами та поведінкою, оскільки ви надали повний доступ.

Тож залежно від команди, їх досвіду, розміру програми, загального шару та архітектури ... це залежить: - \


Зауважте, що ви можете, якщо хочете працювати на це, повернути свій IQueryable, який, у свою чергу, викликає БД, але додає шари та управління поведінкою.
psr

1
@psr Чи можете ви пояснити, що ви маєте на увазі під цим?
Travis Parks

1
@TravisParks - IQueryable не повинен бути реалізований БД, ви можете самостійно писати класи IQueryable - це досить важко. Таким чином, ви можете написати оболонку IQueryable навколо бази даних IQueryable і мати повний доступ до базової бази даних IQueryable. Але це досить важко, що я б не очікував, що це буде широко зроблено.
psr

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

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

29

Розкривати IQueryable публічним інтерфейсам - це не дуже добра практика. Причина принципова: ви не можете забезпечити реалізацію IQueryable, як це прийнято.

Публічний інтерфейс - це договір між постачальником та клієнтами. І в більшості випадків очікується повна реалізація. IQueryable - це чіт:

  1. IQueryable практично неможливо реалізувати. І наразі належного рішення немає. Навіть у Entity Framework є безліч непідтримуваних винятків .
  2. Сьогодні провайдери Queryable мають велику залежність від інфраструктури. Sharepoint має свою часткову реалізацію, і мій постачальник 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 і повертати вам результати. Але найважливіше те, що він працює всередині шару сховища, тому він все ще використовує переваги відкладеного виконання. Як тільки ви отримаєте результати, ви все ще можете LINQ-об’єкти на ньому, щоб вміст вашого серця.

Ще одна перевага такого підходу полягає в тому, що, оскільки вони є класами POCO, це сховище може жити за веб-службою або службою WCF; він може піддаватися впливу AJAX або інших абонентів, які не знають, що перше про LINQ.


4
Отже, ви б створили API запиту для заміни вбудованого в API запиту .NET?
Майкл Браун

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

7
@MikeBrown Суть цього питання - і моя відповідь - це те , що ви хочете , щоб виставити поведінку або переваги від IQueryable не піддаючи IQueryableсебе . Як ви будете робити це за допомогою вбудованого API?
GalacticCowboy

2
Це дуже гарна тавтологія. Ви не пояснили, чому ви хочете цього уникнути. Heck WCF Data Services виставляє IQueryable по всьому проводу. Я не намагаюся перебирати тебе, я просто не розумію цієї відрази до IQueyable людей, схоже.
Майкл Браун

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?

НІ, якщо ви хочете зробити testdriven development / unittesting, оскільки не існує простого способу створити макетний сховище для тестування вашого businesslogic (= репозиторія-споживача) у відриві від бази даних чи іншого постачальника послуг IQueryable.


6

З точки зору чистої архітектури, IQueryable - це проникна абстракція. В цілому він надає користувачеві занадто велику потужність. Як сказано, є місця, де це має сенс. Якщо ви використовуєте OData, IQueryable дозволяє надзвичайно легко надати кінцеву точку, яка легко фільтрується, відсортовується, групується ... тощо. Я фактично вважаю за краще створити DTO, на які мої сутності відображаються, а IQueryable повертає DTO замість цього. Таким чином я попередньо фільтрую свої дані і використовую лише метод IQueryable, щоб дозволити клієнтам налаштувати результати.


6

Я також зіткнувся з таким вибором.

Отже, підведемо підсумки позитивних та негативних сторін:

Позитивів:

  • Гнучкість. Це, безумовно, гнучко і зручно, щоб дозволити клієнтові створювати власні запити лише одним методом сховища

Мінуси:

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

Я б рекомендував не використовувати цей підхід. Розглянемо замість цього створити кілька Специфікацій та реалізувати методи репозиторію, які можуть запитувати базу даних, використовуючи ці. Специфікація є прекрасним способом інкапсуляції набору умов.


5

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

Якщо говорити, я вважаю, що просто виставляти сироту на сховищі і називати це щодня - це безвідповідально. Наявність корисних операцій та помічників для поширених запитів дозволяє централізувати логіку запиту, одночасно піддаючи меншу поверхню інтерфейсу споживачам сховища.

Перші кілька ітерацій проекту, викриття 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.