Comments 58
Не претендую на абсолютную истину, но выглядит, как по мне, немного избыточно.
- Почему для получения репозитория используется Service Locator pattern? Почему бы не внедрить репозитории в контроллер через DI?
- Зачем интерфейсу репозитория знать о существовании
IStorage
, и почему бы не внедрить его через конструктор.
В чем вообще цель выделения IStorage
, если все гвоздями прибито к Entity Framework?
Тут недавно был по большей части терминологический спор о том, в чем состоят обязанности репозитория. Не хочется его начинать снова, но я считаю, что в "стандартном" трехзвенном приложении нужны минимально следующие абстракции:
- Сущности предметной области
- Сервисы — классы, реализующие операции над сущностями; содержащие бизнес-логику
- Репозитории — абстракции, позволяющие спрятать детали взаимодействия с хранилищем данных от сервисов.
то гораздо удобнее использовать метод вроде GetFiltered(query), чем в каждом месте, где это необходимо, писать Linq-выражения
… и на очередном добавляемом методе (хорошо если третьем, хуже если двадцатом) разработчика начинает брать сомнение. Ибо типовые LINQ-выражения легко поместить в метод расширения, а с традиционным репозитарием этот номер не пройдет.
В том числе и так.
Главная проблема репозиториев — они очень плохо ложатся на реляционную модель.
LINQ — вариант принципиально более гибкий.
Вы, конечно же, можете возразить — вместо 100 методов в репозитории будет один метод GetFiltered(query). Что есть query? Если IQueryable — то см. выше. А если писать запросы на самодельном DSL, то есть вероятность, что для большого проекта в итоге получится дорогая и ограниченная… надстройка над LINQ.
Что касается GetFiltered, то это пример, просто подразумевается функция, которая отбирает отфильтрованные объекты по одному или нескольким параметрам.
Но даже если их будет 100 в этом нет ничего страшного, если это действительно необходимые и полезные методы
Это та самая неподдерживаемая мусорка. А потом будет внезапный сюрприз в виде произвольных отчетов и все равно придется делать IQueryable.
В реальности бывают разные ситуации. Например, «отчеты» вообще часто генерируются на стороне сервера и затем представляются в виде «фейковых» сущностей из вьюх баз данных или как-то еще. Суть не в этом.
понятный класс с большим количеством методов
кратко именуемый "неподдерживаемая мусорка".
И все-равно правильнее иметь 100 методов в одном месте
вместо единственной реализации IQueryable
Какая еще мусорка?
Методы расширения не меняют контракт расширяемого интерфейса, поэтому их может быть ровно столько сколько нужно.
Это ничего, что ваш вариант на каждый чих требует дергать разработчика, реализующего взаимодействие с БД?
Ничего, что комбинировать результат двух методов репозитория снаружи нереально?
Ничего, что любая дополнительная фильтрация, требуемая клиентом, будет не в базе, а в памяти?
Тут смысл как раз в том, чтобы избежать фильтрации в памяти. Чтобы все было за кулисами, за репозиториями. В этом и суть шаблона. Нужны отфильтрованные, разбитые на страницы данные? Пожалуйста. Нужен объект по идентификатору? Пожалуйста. Нужно создать, удалить объект? Пожалуйста. А с целью оптимизации для каждой реализации репозитория будет собственный подход к каждому методу. Где-то достаточно Where-запроса, а где-то нужно выполнит хранимую процедуру или же обратиться ко вьюхе в базе или еще миллион моментов, но все это вас, как разраотчика, например, контроллера, не волнует и не касается.
Есть класс, есть описанные методы.
Их очень легко оказывается недостаточно для какой-нибудь очередной задачи. С IQueryable таких проблем практически нет.
Чтобы все было за кулисами, за репозиториями.
В том-то и проблема, что для реляционной модели характерны весьма разнообразные запросы с кучей фильтраций и соединений. А в традиционном репозитории шаг влево-шаг вправо от голого CRUD — и привет.
Их очень легко оказывается недостаточно для какой-нибудь очередной задачи. С IQueryable таких проблем практически нет.
Таких проблем нет, есть другие — размазанный и дублированный код доступа к данным, который расположен в не предназначенном для него слое, который невозможно протестировать и инкапсулировать, например, в базе, при необходимости.
А в традиционном репозитории шаг влево-шаг вправо от голого CRUD — и привет.
Ну, в общем-то, в этом и смысл репозитория, так-то можно и SQL писать в коде, что, в определенном приближении, равноценно LINQ запросам в бизнес-логике.
размазанный и дублированный код доступа к данным, который расположен в не предназначенном для него слое, который невозможно протестировать и инкапсулировать
С чего вдруг размазанный и дублированный? Методы расширения помогут, инкапсулировать в базе это не нужно.
Гораздо серьезнее другая проблема — использование ORM DTO в качестве объектов бизнес-логики, и реальная граница пролегает не по IQueryable, а там где DTO приходится мапить на бизнес, нередко вручную.
Ну, в общем-то, в этом и смысл репозитория
Что и делает данный паттерн сильно специфичным. Я иногда к нему прибегаю (в тех случаях, когда набор соединений и фильтраций заведомо ограничен, а требования по производительности велики), но реализую с полной изоляцией — никаких DTO в интерфейсах, чтение и модификация раздельно.
можно и SQL писать в коде, что, в определенном приближении, равноценно LINQ запросам в бизнес-логике
Неравноценно от слова совсем. "Определенное приближение" не сработало ни разу за всю мою карьеру. Конечно, IQueryable требует иного обращения чем IEnumerable, но это справедливо и для IObservable, а уж про Sprache вообще молчу.
И, разумеется, никакого LINQ over IQueryable в бизнес-логике нет по очень простой причине — IQueryable работает с DTO, а не с бизнес-объектами.
Тут смысл как раз в том, чтобы избежать фильтрации в памяти.
Вы точно в курсе что связка из IQueryable и linq может конвертироваться напрямую в sql запрос со всеми необходимыми where, order by, group by?? И никакой фильтрации в памяти приложения не будет, т.к. эти займётся сама БД.
Using IQueryable with Linq
- Как вы видете выборку данных с использованием репозиториев? (будет ли это новый репозиторий?)
- Что всетаки должен возвращать репозиторий IEnumerable или IQueryable?
1. Выбрать всех разработчиков с помощью репозитория разработчиков и затем выгружать их таски отдельными запросами с помощью репозитория тасок, но это неэффективно.
2. Выбрать всех разработчиков вместе с их тасками одним запросом, если это поддерживается хранилищем (если не поддерживается (например, мы работаем с JSON API) — первый вариант). Т. е. мы обращаемся к репозиторию разработчиков и он возвращает разработчиков + некий граф объектов при необходимости в виде свойств разработчиков (в контексте EF — свойств навигации).
1. Т. к. мы реализуем шаблон Единица работы, точкой входа у нас все-таки должен быть соответствующий объект, который существует в единственном экземпляре в контексте одного запроса. Теоретически можно инжектить репозитории и вообще все что угодно, но какой смысл? Инжектить десятки различных типов, затем по отдельности их все запрашивать. А где выполнять сохранение? А как иметь единый контекст? Это все решает UoW.
2. Интерфейс репозитория не знает об IStorage. Он знает только о контексте хранилища (о самом «представлении» базы данных, например, что бы это ни было в конкретном случае), т. к. в противном случае ему просто не с чем было бы работать.
Насчет выделения IStorage и EF. Я не согласен, что все прибито к EF. В простых случаях, где просто нужно сохранить немного объектов где-нибудь, можно всегда обходиться EF. Но в чем-то побольше источниками данных вполне могут выступать и HTTP-сервисы, и файловая система и так далее. Если мы завяжем реализацию на EF, придется потом все переписывать, если возникнет необходимость в чем-то еще. Вот и все.
Насчет обязанности репозиториев. Обязанностью репозитория является агрегация всех методов для манипуляции с данными определенного типа (одной сущностью) и сокрытие особенностей реализации этих манипуляций. Иногда есть смысл разделять CRUD-операции и операции предметной области на репозитории и сервисы, но чаще последние являются избыточными и я помещаю все на один уровень — уровень репозиториев. Т.е. необходимо вам получить объект по идентификатору или же набор объектов по определенным признакам (например, 10 самых популярных товаров в интернет-магазине за вчера) — все это делается одним методом одного репозитория. Т.е. запросы одного уровня.
Насчет абстракций в трехзвенном приложении. В общем-то я с вами согласен. За исключением того, что часто нет смысла в разделении репозиториев и сервисов.
Разделение на репозитории и сервисы нужно только в случае, если есть бизнес-логика.
Я идею теперь понял, забыл, что в названии статьи упоминался unit of work.
А где метод Save вызывается?
Вообще я считаю, что юнит ов ворк не самый удобный паттерн для приложений с бизнес логикой. Очень легко получить неконсистентность данных в памяти и в хранилище. Часто приходится вызывать сохранение несколько раз, транзакционность тоже через отдельные сохранения. После сбоя сохранения контекст может быть непригоден для использования.
Согласен, что в каждом случае необходимо исходить из задачи.
Какой смысл создавать Storage и IStorage, если с таким же успехом можно просто запросить StorageContext в контроллере?
Я пойму еще создать ItemManager со специфическими функциями для этой сущности, но зачем дублировать готовый функционал ef core?
Насчет дублирования готового функционала EF. Вы правы, DbContext также является реализацией Unit of work и можно использовать его напрямую. В простых приложениях это вполне применимо. Но что если на каком-то этапе потребуется, как в примере, использовать другой источник данных, вообще не основанный на EF. Например, JSON HTTP API или же, снова-таки случай из примера, перечисление в памяти для тестирования. Что тогда? Тогда придется либо признать, что сделать этого уже нельзя (без переписывания всего, что связано с механизмом взаимодействия с данными), либо сесть и все-таки внедрить слой абстракций, например, с помощью описанного в статье подхода, но все-равно с переписыванием всего. А ведь в реальных долгоиграющих проектах задачи и условия меняются постоянно.
Смотрите, предположим, у вас есть необходимость выбрать всех людей со средней зарплатой больше Х. Вы получаете соответствующий репозиторий из единицы работы и затем выполняете 1 метод, который просто принимает параметр Х. Вам не нужно заботиться о том, как именно реализован этот метод с точки зрения предметной области или с точки зрения базы данных или другого хранилища. Вы просто его используете. Он вполне может быть написан кем-то другим и его логика может 10 раз измениться в процессе, но остальной код менять не потребуется. Также если вам нужен конкретный человек, вы получаете его по идентификатору также из репозитория. Часто для решения первой задачи вводится еще слой сервисов, но он практически всегда избыточен и попросту дублирует наборы методов из репозиториев, поэтому почти всегда я размещаю ВСЕ методы для манипуляции объектами определенного типа в репозиторий. Представьте, что вы используете в контроллерах DbContext и DataSet напрямую. Вы будете везде писать один и тот же Linq-запрос, чтобы выбрать необходимые объекты? Или как вы сказали, создадите просто висящий в воздухе ItemManager? Тогда наверняка часть методов будут выполняться на DbSet, а часть — на ItemManager, что приведет к путанице и сделает еще более сложными описанные выше сценарии.
Хотелось бы видеть более качественные технические статьи в профильном блоге.
А с выходом ASP.NET CORE — повсеместно. Меня будто вновь воспитывают.
В этой статье я не пытался заново рассказать о самих шаблонах (по сути, здесь вообще нет их детального описания). Лишь показать простой пример реализации на новой технологии со встроенным DI. Сейчас я думаю, что стоило бы более детально рассказать о том, когда их следует применять, а когда — нет.
Я, честно, говоря, думал что глобальный DbContext уже вышел из моды. Мне больше такая штука нравится Аmbient DbContext
Вот бы такой же пример, но не с select, а с update, и чтобы были связанные сущности, транзакции, как в реальной жизни.
Абстракции можно оправдать только одним аргументом — возможность в будущем заменить EntityFramework на что-то другое, или вообще избавится от реляционной базы данных в пользу чего то другого. Но,
1. На практике это происходит очень редко;
2. Если это происходит, обычно простым написанием новых IBlaBlaRepository не обойтись — все-равно придется пол проекта перелапачивать чтобы эффективно использовать новый DAL и базу.
var names = dbContext.Employee.Where(e => e.Salary > 5000).Select(e => e.Name).ToArray();
не пришлось на интерфейс и реализацию вешать это:
IEnumerable<string> GetEmployeeNamesWithSalaryGreaterThan(decimal salary);
Если есть часто используемые запросы в базу которые хочется переиспользовать, можно в extension методы вынести.
EntityFreamwork сам по себе уже абстракция которая отделяет данные от всего приложения. Хоть убейте не понимаю как набор IBlaBlaRepository с кучей методов в каждом из них, лучше «разделяет приложение» чем одна IDbContextFactory которая создает DbContext с набором IQueryable.
IEnumerable<string> GetEmployeeNamesWithSalaryGreaterThan(decimal salary);
значительно лучше, чем
var names = dbContext.Employee.Where(e => e.Salary > 5000).Select(e => e.Name).ToArray();
Можно, конечно, писать эти условия Where прямо в контроллере, но это примерно то же самое, что и писать там непосредственно SQL-запросы. Посмотрите сами на этот код. А если там будет больше условий и если вы не писали этот запрос, как вам понять, что он делает, быстро? И сравните его с понятным названием метода, и тогда вы, возможно, поймете, зачем используются репозитории (или сервисы, кто как любит).
Также представьте, что вы написали несколько таких конструкций по проекту, а затем вам поступает задача изменить условие с БОЛЬШЕ на БОЛЬШЕ ИЛИ РАВНО. И что тогда? Будете везде лазить и изменять. И так делают, к сожалению, большинство.
По поводу EF уже абстракция повторюсь, существует не только EF. Посмотрите хотя бы на пример. Если вы на 100% уверены, что вашему проекту не потребуется что-то другое и знаете, что его ждет дальше — прекрасно, можно все упростить и использовать напрямую DbContext и linq-запросы в контроллерах.
Посмотрите сами на этот код. А если там будет больше условий и если вы не писали этот запрос, как вам понять, что он делает, быстро? И сравните его с понятным названием метода, и тогда вы, возможно, поймете, зачем используются репозитории (или сервисы, кто как любит).
Не пойму, зачем городить абсолютно некомпозабельный сервис там, где достаточно метода расширения.
Можно, конечно, писать эти условия Where прямо в контроллере, но это примерно то же самое, что и писать там непосредственно SQL-запросы.
Большая разница, linq типизированное а SQL запросы нет
Посмотрите сами на этот код. А если там будет больше условий и если вы не писали этот запрос, как вам понять, что он делает, быстро? И сравните его с понятным названием метода, и тогда вы, возможно, поймете, зачем используются репозитории (или сервисы, кто как любит).
Ну выносите запрос в приватный метод класса где он реально используется, делов то. Есть же полно способов навести красоту )
Также представьте, что вы написали несколько таких конструкций по проекту, а затем вам поступает задача изменить условие с БОЛЬШЕ на БОЛЬШЕ ИЛИ РАВНО. И что тогда? Будете везде лазить и изменять. И так делают, к сожалению, большинство.
Опять же, есть куча способов такие ситуации разрулить, например те же экстеншн методы о которых я упоминал. В этом то и вся суть, у вас полно пространства для маневров, вы не связываете себе руки и к каждой конкретной ситуации подходите отдельно. Если же вы решили связаться со своим репозиторием над EntityFramework, у вас выбора особого не остается — чуть что или новый репозиторий или новый метод в репозитории, потом к нему тесты, документация, и т.д.
По поводу EF уже абстракция повторюсь, существует не только EF. Посмотрите хотя бы на пример. Если вы на 100% уверены, что вашему проекту не потребуется что-то другое и знаете, что его ждет дальше — прекрасно, можно все упростить и использовать напрямую DbContext и linq-запросы в контроллерах.
Я на 100% уверен что если что-то такое произойдет, что потребует отказаться от EF, то это не обойдется простым написанием новых репозиториев. Можно конечно написать совершенную изоляцию слоя данных, но а) правильную изоляцию сделать сложно и довольно затратно; б) эффективность и производительность будет хуже. Так что это компромисс, кому что лучше ).
Насчет выноса в приватные методы класса (контроллера, например) чего-то для загрузки данных из бд — даже не будут комментировать.
В целом, вы правы. Есть «куча способов разрулить такие ситуации» и тут как раз описан один из них.
Service Locator, Protocol, Method Injection — просто сборник антипаттернов какой-то.
В качестве вишенки на торте — протекающая абстракция в виде наследования от DbContext.
И чего только люди ни придумают, лишь бы IQueryable не реализовывать.
Еще и реализовали как-то странно. Зачем там рефлексия с активатором, когда можно просто сделать new T()?
Давайте подробно по всему примеру:
- Класс модели таковым не является — это не модель, а DTO, чье главное свойство — автоматическая (де-) сериализация. Для ORM удобно, для бизнес-логики — ужасно.
- IStorageContext — маркерный интерфейс, никому на самом деле не нужен. Вся реальная работа все равно идет с его наследниками.
- IStorage — большой привет от антипаттерна ServiceLocator. До рантайма никак не понять, к каким же именно репозиториям есть доступ. С методом Save непонятно, сколько раз его можно вызывать и неясно, что происходит после неудачной попытки записи.
- IRepository — маркерный интерфейс, да еще и с Metod Injection. Непонятно, что будет делать реализация до первого вызова SetStorage, что прозойдет после второго и последующего вызовов, как понять, что вызов SetStorage уже выполнен и т.п. Протокол во всей красе.
- IItemRepository — интерфейс не обобщенный. Это что же, для каждой простой модели свой интерфейс репозитория определять?
- Storage — ручной перебор типов через отражение, плюс антипаттерн Constrained Construction — конструктор репозитория не должен иметь параметров. Вместо одной реализации на оба варианта предлагается почти полная копипаста.
Нормально спроектированных классов и интерфейсов нет ни одного. И это учебный пример в блоге компании Микрософт.
Наверное, круто, его везде прокидывать не задумываясь, открыт ли ваш контекст или нет, да?
ASP.NET Core: Пример реализации шаблонов проектирования Единица работы и Репозиторий