Pull to refresh

Comments 159

Нуу… Это вообще почти холиварный вопрос, использовать или не использовать РЕПО и свой юнитофворк поверх контекста. Считаю что вообще то ещё должен быть базовый класс для доменной сущности. А оборачивать или нет прокси контекст EF в РЕПО дело вкуса. Главное четко разделять слои от сервисов. DDD тут главное.
базовый класс для доменной сущности

Ну это как-то ортогонально созданию кастомного репозитория.


А вот насчет Repository и UnitOfWork — либо через год они превратятся в неподдерживаемое нечто (времени на рефакторинг не хватило). Либо со временем окажется, что Вы пишете на собственном фреймворке поверх EF (времени хватило).


А еще, такие репозитории — это пустая абстракиця. В них нет ни бизнес-логики, ни инфраструктурной логики. Это как адаптер. Только он используется не для интеграции двух разных модулей. А для интеграции EF и кода, который еще даже не написан.

А вот насчет Repository и UnitOfWork — либо через год они превратятся в неподдерживаемое нечто (времени на рефакторинг не хватило).

Как говорится, по себе людей не судят.
да, да, да! супер.
жаль, те кому это надо прочитать, не прочитают. ведь и так понятно, что в папочке DAL должен лежать UserRepository… с тыщей методов.
Зато теперь можно тыкнуть их носом в эту статью =)

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

ORM в целом не абстракция от базы данных, а репозиторий — абстракция от системы хранения.

ORM в целом не абстракция от базы данных
У вас тут «не» лишнее?

То, что некоторые ОРМ позволяют асбтрагироваться от конкретной РСУБД парой строчек в конфиге, не означает что ОРМ — абстракция от БД. Наоборот, ОРМ обеспечивает связь объектной и реляционной моделей. Реляционная модель в ОРМ не под капотом, а входной параметр.

Я бы абстрагировал программистов:)

Поддерживаю вашу борьбу с Repository, при живом EF в проекте. Но вот Specification по-моему, это оверхед, да еще и с переопределением операторов.

Тут я пожалуй соглашусь. Мне самому комфортнее с extension-методами. Особенно, если дополнить их возможностью вызова из ExpressionTree, как описано в конце статьи.


Спецификации — это не способ облегчить жизнь разработчику, а скорее способ оформить все "по феншую" (DDD, TDD, переиспользование и т.п.).


И еще иногда (очень редко) возникает потребность динамически комбинировать условия с выборки помощью OR. Для AND все просто — добавляем в цепочку запроса несколько .Where()
А для OR или спецификация, или PredicateBuilder из LinqKit

Есть ещё LinqSpecs. Ваша реализация очень похожа на неё. Я некоторое время назад тоже сравнивал варианты реализации спецификаций через extension'ы и expression'ы: https://habrahabr.ru/post/325280/

Значит, мне надо было поглубже копать Хабр, прежде чем свой велосипед писать =)
Напротив, не первый раз замечаю, если несколько человек независимо «изобретают» одно и тоже, значит идея здравая:)
На последнем проекте мы смогли элегантно решить проблему поиска по связанным сущностям с помощью вот такого трюка: habrahabr.ru/post/313394. EF Core больше не выбрасывает NotSupported, что позволяет использовать спецификации не только к целевым сущностям, но и к связанным. Возможно, будет полезно.

Насколько эта expression-магия наносит урон по производительности? Сможет ли EF кэшировать запросы в таком случае?

Это Вы про extension-методы? Хороший вопрос на самом деле. Надо проверять.


Но, чисто умозрительно, кэширование должно работать. Магия не встраивается внутрь EF. Она работает как декоратор вокруг LINQ IQueryable. И тот Expression, который содержится во внутреннем IQueryable из EntityFramework, уже не содержит никакой магии. Ну и компиляция SQL происходит из нормального ExpressionTree.


В принципе, когда мы добавляем в цепочку вызовов еще один LINQ-метод:


query.Where(...).Select(...).OrderBy(...)

мы точно так же модифицируем IQueryable.Expression.

Alex_ME, в общем, добавил я бенчмарки в свой проект. См. обновление статьи.


Просадка на компиляцию запроса получается в 15-30 %. А не в несколько раз, как без кеширования. И все равно укладывается в одну миллисекунду.

Repository как набор запросов

Удобен тем, что можно сделать реализацию Repository специально для тестирования, которые отдают те или иные тестовые данные. Так вы проверите бизнес-логику.

Тестировать сами Repository, как правило, нужды нет, так как в них и логики то особой нет. Но то что есть можно протестировать, как вы правильно заметили, с помощью in-memory DbContext.

То для реализации FilterByDateAndTag() мы не можем использовать два предыдущих метода.

В чем проблема Nullable?

IEnumerable<Post> FilterByDateAndTag(DateTime? date, string tag);

реализацию Repository специально для тестирования

Только вот замокать репозиторий с достаточно богатым API — та еще задачка. Лучше уж In-Memory Context. А вот IQueryable замокать элементарно — new List<T>().

Вы сами же предлагаете использовать все преимущества ORM, которые не замокаются через List, а учитывая особенности трансляции LINQ в SQL, замокать в List не получится и подавно. Это красивая утопия.

Репозитории делаются максимально простыми не в целях смены ORM или СУБД. А в целях создания искусственных ограничений, превращая БД в абстрактное хранилище данных. И в целях уменьшить или свести к нулю проблемы тестирования.

Соответственно, для 80-90% запросов, можно прекрасно прожить в рамках этого ограничения. А оставшиеся узкие места оптимизировать специальным образом, например, создавая специализированные реализации репозиториев для некоторых сущностей вместо Generic Repository.

И это всё также хорошо работает как со спецификациями, так и Query Extensions или Query Object.

Вот с чем не поспоришь, так это DAL, описанный вами в статье — действительно зло.
Так я не говорю, что репозиторий зло. Репозиторий это отличная абстракция, и она у нас уже есть — IDbSet.

А вот от догмы, что все запросы к MyEntity должны лежать в MyEntityRepository нужно отступить.
Generic Repository отлично выполняет роль барьера, за которым может стоять что угодно. Почему многие сразу впадают в крайность и думают, что интерфейс репозитория служит для замены одной ORM на другую? А если у меня появится несколько источников данных? А если я хочу в целях оптимизации для некоторых работ вообще использовать Dapper? Нет, IDbSet не отличная абстракция и вообще не абстракция репозитория. Извините, но нет :)
Ну, IDbSet предоставляет CRUD и расширенную фильтрацию. Все по Фаулеру =)
А что оптимизировать Dapper-ом в GenericRepository — CRUD или специфические запросы?
Если запросы, то это уже не Repository. Это DAO, или QueryHandler из CQRS, или вообще какой-то сервис. Короче, абстракции протекают.
Ну а Generic Repository не предоставляют CRUD и фильтрацию на IQueryable? Только у вас в таком случае есть возможность написать полностью свою реализацию, и данные брать где угодно, а значит есть возможность провести любую оптимизацию, какая только вздумается и разделить источники данных. С IDbSet вы прибиваете себя именно к EF во всех смыслах. CRUD тоже может быть весьма специфичен.

Абстракции начинают «протекать», когда одно пытаются натянуть на другое. Необходимо ограничить обязанности и чётко понимать, для чего вводится абстракция. Когда это звучит вот так: «репозиторий для замены ORM» — это совершенно некорректная задача и обязанность. Зачем его заменять вообще? Абстракция репозитория это абстракция хранилище данных. Она прекрасно мокается, не зависит от внутренностей и багов ORM, которые можно подпереть в случае чего костыликом до лучших времён. Репозиторий может быть прокси, вычисляющим время запроса, вести аудит и поддерживать любую инфраструктуру. На маленьких проектах это может и не заметно, и там можно обойтись IDbSet, или вообще просто пробрасывать DbContext, чего мелочиться?
IDbSet — всего лишь интерфейс. И реализовать его не сложнее чем кастомный. Единственный косяк, что лежит он в assembly EntityFramework. Ну так до Microsoft начало доходить. И теперь они выпускают по два пакета: MyPackage.dll и MyPackage.Abstractions.dll. Может и до EntityFramework-а доберутся.
IDbSet сильно привязан к EF: Attach, Find — эти методы уместны именно для EF, и не имеют ничего общего с концепцией репозитория. Косяк в том, что одно выдаётся за другое, только за похожесть :) Опять же. Почему всё таки, не отдавать тогда просто DbContext везде? Там ещё больше ORM-specific полезных вещей.

А можно не завязываться на DbContext, а разделить чтение и запись и для чтения использовать QueryObject. Если в проекте только реляционная, то IQueryable уже есть и не надо ничего своего изобретать.

Я так вижу. Если вы хотите работать именно с ORM и завязываться на ORM, не предусматривая никакой возможности для развития системы за рамками выбранного компонента, то городить абстракции не нужно конечно. И нормально будет передавать DbContext, используя все плюшки. Query Extensions неплохо инкапсулируют код запросов, позволяя вставлять их в подзапросы. Спецификации вообще не жизнеспособны для запросов, колоссальный оверхед с минимальным выхлопом. Но они хороши для системы валидации, например, или для системы проверки прав досутпа. Но всё это ограничивается небольшим размером и сложностью проектов.
Когда это звучит вот так: «репозиторий для замены ORM» — это совершенно некорректная задача и обязанность.

А если я хочу в целях оптимизации для некоторых работ вообще использовать Dapper?

А это разве не замена ORM, хоть и для отдельных мест?


Кстати, как Вы реализуете Generic Repository с IQueryable для Dapper? Как я понимаю, Dapper работает с чистыми SQL-запросами, а не LINQ. Реализуете свой LINQ-провайдер?


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


Честно говоря, я вообще не могу сходу придумать, какой интерфейс должен иметь репозиторий, чтобы можно было реализовать и EF, и Dapper. Описанные Specification так же не подходят, они реализованы на Expression. Самим транслировать в SQL? Потеряется плюсы Dapper, тогда уж лучше Linq2Db взять. Единственное, что приходит в голову — тот ужасный DAO с кучей методов.

Поэтому есть смысл использовать QueryObject, а не Repository и возвращать DTO, а не доменные объекты. Expression<Func<TDto, bool>> подойдёт в качестве спецификации. Для Dapper придётся написать свой Visitor для разбора выражения, но это не так сложно, если Dapper используется точечно.


А вот если тяжелой ORM в проекте нет и все на Dapper, то проще скопировать архитектуру StackOverflow.

Не могу понять один момент. Как я понимаю, основной профит Dapper — то, что он имеет минимальный оверхед, и запросы самописные, и материализация быстрая. Если мы будем разбирать выражения и генерировать SQL то мы потеряем в производительности. И, возможно, рано или поздно, критические места придется вынести в самописные скрипты или вообще вызов хранимой процедуры. Куда в таком случае поместить этот вызов? В репозиторий GetSomeFooWithBar?


Поэтому есть смысл использовать QueryObject, а не Repository и возвращать DTO, а не доменные объекты.

А где связь между QueryObject и возвращением DTO? Разве он не может содержать SQL-код, который извлечет из БД доменный объект?

В EF самое тормозное — материализация объектов, ибо Reflection. На сколько тормозным будет разбор деревьев зависит от вас. В реальности проще всего написать реализацию только под те выражения, которые вы собираетесь поддерживать, а все остальное NotSupportedException.

QueryObject может возвращать что угодно. Мы пришли к выводу, что в read-части DTO лучше: легче запрос, меньше затрат на материализацию, нет проблем с сериализацией и накладных расходов на дополнительный маппинг, нет опасности нарваться на lazy-load и связанные коллекции (опытный разработчик сразу такое заметит, а junior — нет).

Доменный объект чаще нужен по id во write-подсистеме. Его проще получать из DbContext'а или своего UoW, если есть в наличии. Специально на эту тему не думали, получилось из практики. Возможно, другие проекты с другой спецификой нужно проектировать иначе.
Только вот замокать репозиторий с достаточно богатым API

Нужно разделять репозитории по смыслу. Чтобы сохранялся in-memory контекст — делается UnitOfWork и через него происходит получение репозиториев разных типов.

Вот пример от мелкомягких: implementing-the-repository-and-unit-of-work-patterns.

Плюсов много. К примеру когда я хочу добавить индексы в таблицу — я пересматриваю все запросы и сразу оптом их добавляю. Все запросы в одном месте, не нужно искать их по коду сервисов.
Я вообще сторонник группировки кода по фичам, а не по паттернам, которые этот код реализует. Как компромисс — можно сделать репоизтории partial, и раскидать их по каталоагм с фичами. Так можно и все запросы посмотреть по сущности, и в огромном файле не запутаться.
В примере от MS на каждую фичу свой репозиторий а общий контекс обеспечивается
с помощью UnitOfWork. Зачем partial?

Я в одном из проектов разнес репозитории по модулям (т.е. в каждом из модулей только те репозитории, которые с ним связаны). Модули можно использовать независимо или объединять (при этом они могут использовать один контекст, если нужно, к примеру один модуль зависит от другого).

Для меня важно что этот подход рекомендуемый, он понятен всем. Мне не пришлось придумывать свой велосипед.

Вообще забавный пример. Там предлагают передавать в .Include() строковые названия. Прощай Type Safety. Да и репозитории там не по фичам, а по сущностями. А что если UserRepository используется вообще везде, но с вызовом разных методов?


А Репозиторий по фичам называется уже "сервис" =).

Вообще забавный пример. Там предлагают передавать в .Include() строковые названия. Прощай Type Safety.

Ну так Include() в EF и принимает строки. А вот в EF Core уже expression.


А что если UserRepository используется вообще везде, но с вызовом разных методов?

Несколько репозиториев по решаемым задачам? У меня в проекте были слишком распухшие репозитории и сервисы, возможно, такое разделение хорошая мысль, но не уверен на 100%.

У этого метода всегда было две перегрузки.

Nullable?

Тогда уж лучше сразу


IEnumerable<Post> Filter(PostCriteria criteria);

class PostCriteria
{
    public int? AuthorId { get; set; }
    public DateTime? Date { get; set; }
    public string Tag { get; set; }
    // и т.п.
}

Но я про другое. Для extension-методов:


.FilterByDateAndTag(date, tag)
// эквивалентно
.FilterByDate(date).FilterByTag(tag)

И их можно использовать в подзапросах в в отличие от..


context.Users.Select(user => new
{
    User = user,
    Posts = user.Posts.FilterByDate(today).ToList(),
})
Тогда уж лучше сразу

Зависит от количества параметров. Если их много и много разных вариаций вызова — то да, объединяем в пакет.

Я не стал изобретать велосипед, делаю по рекомендациям от MS: использование Repository и UnitOfWork. Они же сами рекомендуют такой способ работы с данными.

Минус вашего подхода — у вас все запросы разбросаны по слою сервисов (бизнес-логики). И когда вы захотите добавить индексы — вы долго и нудно в профайлере будете выискивать тормозные запросы, после чего добавлять индексы. Или же будете лазить по всей бизнес логике, выискивать там запросы и смотреть какие индексы вам нужно добавить.
И когда вы захотите добавить индексы — вы долго и нудно в профайлере будете выискивать тормозные запросы, после чего добавлять индексы.
Так разве это не лучший способ узнать где нужны индексы? Зачем заниматься сомнительной работой гадая какой нужен индекс для какого запроса. Есть инструмент, бери и используй, не гадай.
Профайлер — это крайняя мера, когда вы что-то пропустили. А так лучше все индексы закладывать в архитектуру, для этого нужно знать все свои запросы.
Мое мнение, по поводу индексов, это преувеличенная причина.
Как уже писали в комментариях, зато в репозитарии можно добавить доп функциональность типа логирования, прав доступа и т. д.

Оффтоп: как ведет себя ToList() внутри лямбды в Select? Всегда думал, что ToList производит материализацию запроса, а что происходит когда он в середине запроса?

Никак не ведет себя, он при трансляции в SQL игнорируется.


Видимый эффект — свойство в анонимном классе получается не IEnumerable<>, а List<>

Спасибо. Спасли от возможного лишнего выстрела в ногу.

Мнение 1.
Repository Pattern with C# and Entity Framework, Done Right



Мнение 2.
Pluralsight — Entity Framework in the Enterprise Julie Lerman говорит что долго использовала R паттерн, и до сих пор многие из ее знакомых его используют. Это как привычка.

Для меня это место хранения часто используемых запросов к сущности.

Я тоже смотрел это видео перед написанием статьи =) На первый взгляд, там все правильно и красиво. Единственный вопрос — а зачем?


место хранения часто используемых запросов к сущности

А вот тут уже суровая реальность: Вы будете выносить запросы в репозиторий по мере рефакторинга. А Ваш коллега просто напихает от балды. Со временем все равно класс распухнет. Потому что его ответственность "часто используемые запросы" слишком размыта. Группировать запросы надо. Но по бизнес-кейсам, а не по сущностями.

Со временем все равно класс распухнет.

Это проблема процесса, а не паттерна. Причем проблема, легко подбираемая на первом же code review.
А вдруг, при реализации нашего чудо-репозитория мы воспользовались уникальными особенностями конкретного ORM?


А какая разница, какими особенностями мы воспользовались? У нас есть интерфейс, который говорит, что если попросить User UserOfId(UserId userId) то получим пользователя. Внутри конкретного репозитория это может быть сделано как угодно, главное, что в итоге есть User.

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

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

Интерфейс репозитория, это как документация, он сообщает нам, что, например, где-то есть бизнес-процесс, которому нужно получить всех пользователей с датой регистрации не позднее какого-то числа. И это нужно не потому, что мы хотим вывести это где-то в интерфейсе, а потому что мы будем проводить какие-то манипуляции с такими пользователями, принимать какое-то бизнес-решение. Если же нужно просто выводить список ограниченный по дате, используем то что удобно.
А как понять, должен быть метод именно в репозитории или еще где?
Почему не прикреплять этот метод к обработчику бизнес-процесса?

Вот например есть бизнес-процесс: модератор хабов просматривает список пользователей по дате, и принимает решение — выдавать ли инвайт. Где поместить метод фильтрации пользователей?
Можно еще так ответить. Представьте, что вы используете разделение на Command и Query в слое сервисов, даже если вы этого не делаете. И вот те обращения к БД, которые вы делали бы в Command если бы они у вас были, вы делаете через Репозиторий. А все остальные обращения — так как вам удобно.

О как! Ну, такой подход имеет право на жизнь. Можно правда еще поизвращаться чуть-чуть:
Сделать репозиторий как partial class, и растащить UserRepository.cs по папкам, соответствующим разным фичам. Что угодно, только бы не нарушать SRP =)

Поместить там, где хочется. Начиная от обращения к базе прямо в контроллере заканчивая особым QueryRepository который содержит все эти запросы, которые мы часто где-то используем для вывода данных пользователю. По-умолчанию я бы выбрал эти данные где-то в сервисе или запросе (запросе из слоя сервисов, а не БД).
Момент просмотра модератором списка хабов, это еще не момент выполнения какого-то кода в бизнес-логике.
Итак, вы предлагает пользоваться стандартным апи. Хорошо.
Как быть с правами? Например, у меня в приложении своя система прав. Все через стандартный «репозиторий» ходят мимо прав?

А у Вас что, права на сущности распространяются, а не на бизнес-операции?
Обычно система прав живет в слое сервисов. А их можно сделать уже такими, как Вы захотите.
Ну или вообще AOP.

А репозиторий и есть доменный сервис. Конкретные реализации в инфраструктурном слое, а интерфейс в доменном.

И на сущности и на бизнес операции. Стандартная система прав — это всегда объект, субъект и операция (низкоуровневая, crud etc).
И тут я вас не очень понял, как вы предлагаете обойтись без объекта.

Допустим:
Руководитель отдела может оперировать только своими сотрудниками.


Если мы добавим фильтрацию в репозиторий, то в сервисе уже будут уже отфильтрованные сотрудники. Если реализовывать в сервисе — то нужно цеплять эту фильтрацию везде, где надо (разумеется, можно вынести в Extension метод или еще куда-нибудь) и постараться не забыть и ничего не сломать в процессе.

Мне нравится репозиторий как набор запросов.
Только не один гигантский на весь проект, а набор маленьких по сущностям и фичам.


В чем преимущество


  • легко мокать (если они маленькие).
  • сразу видно какие именно операции с базой могут выполняться

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


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


Свой же репозиторий может в методе All() возвращать не всю таблицу, а только доступные пользователю записи, например.


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

Но это не репозиторий — это DAL-сервис.

Ок, спасибо за поправку. Я употребил "репозиторий" в смысле объекта для доступа к данным в хранилище.

Набор запросов — это как раз спецификации. Каноническая реализация должна отвечать требованиям компонуемости, т.е. Если есть ASpec и BSpec, то можно сделать aAndB = aSpec && bSpec; и aOrB = aSpec || bSpec; В .NET expressions не без особой уличной магии отлично выполняют роль спецификаций.

Я имел ввиду объект, который содержит методы типа ПолучитьВсехАктивныхПользователей, ОбновитьОнлайнСтатус, и т.п., которые отражают предметную область.


Спецификации — это (компонуемое) описание операций над набором данных, по сути + универсальный метод их исполнения.

Считаю, что в идеале бизнес-слой не должен ни использовать DbSet как репозиторий, ни ссылаться на EntityFramework вообще. Это должно остаться в конкретной реализации DAL.

Как я уже писал в комментарии выше, нужно различать зависимость между архитектурными слоями и зависимость от NuGet пакетов. Если бы EntityFramework был разделен на два пакета: абстракции и реализация, как сейчас Microsoft делает для многих пакетов под dotnet core, то можно было бы "собрать" слой домена из интерфейса репозитория в пакете EntityFramework.Abstractions и набора доменных сущностей в Вашей сборке. А слой DAL — собственно сам EF.


Сейчас конечно интерфейс IDbSet лежит прямо в EF, и не совсем подходит в качестве классического репозитория. Но кастомный репозиторий получится еще хуже, по крайней мере если Вы не потратите существенное время на проектирование.

Использовать интерфейс описанный в EF — не значит абстрагироваться от EF. Этот интерфейс нужен был разработчикам EF для их внутренних целей(например позволить в будущем менять конкретные типы возвращаемые за ним), а не для того чтобы его кто-то другой реализовывал.

Никакой домен не должен ссылаться на EF.Abstractions, потому что с точки зрения домена нет EF, есть рейсы, покупатели, брони и прочие скидки.

Честная слоёная архитектура подразумевает сильную избыточность. Вы же хотите срезать углы на каждом повороте. Так не бывает. У вас либо честные слои, разрезанная ответственность и тестируемость в изоляции и сильная избыточность кода, либо макароны из зависимостей на какие то библиотеки в домене, но зато быстрая разработка.

На крупных кроссплатформенных проектах ничего подобного в принципе не позволительно. Любая зависимость на внешнюю систему (EF в вашем случае) ОБЯЗАТЕЛЬНО абстрагируется архитектурным швом. Потому что нет никаких гарантий что на платформе X она в принципе поддерживается. Верить можно в libc и изредка в pthreads. Хотя хардварщики наверное и тут плюнули бы мне в лицо :)

Почитайте немного про кольцевые архитектуры (типа Clean Architecture, но она не единственная), и про Dependency Inversion.
Тут я с Вами согласен. Мне не удалось поучаствовать в кроссплатформных проектах. Потому что до недавнего времени .NET и кроссплатформа были несовместимы =)

Другой вопрос, что код с зависимостью от внешних абстракций ничего не теряет в поддерживаемости и тестируемости. Он теряет в гибкости. А нужна ли Вам такая гибкость?

И при начале разработки надо ответить на вопрос, мы действительно собираемся менять операционную систему / БД / ORM?

Если у нас «коробочное решение», то ответ — Да. А если внутренний сервис, то скорее всего «Нет».
И при начале разработки надо ответить на вопрос, мы действительно собираемся менять операционную систему / БД / ORM?

Неправильно сформулированный вопрос в контексте обсуждения :) Правильный: "мы действительно собираемся изолировать бизнес-логику от инфрастрктуры или нет?"

Ну, я Вам про цели, а Вы мне про средства. Тогда уж — какие преимущества мы получим, если изолирует бизнес-логику от инфраструктуры? И важны ли кто преимущества для данного конкретного проекта.

Упрощение поддержки и развития. Собственно ради этого большинство архитектурных решений и принимается.

Да, но за каждый новый слой абстракции надо платить. И будет ли от этого упрощение поддержки — зависит от проекта.

Конечно зависит. Эмпирическое правило, которое вывел для себя: если при инфраструктурных изменениях нужно лезть туда, где в целом лежит бизнес-логика, то пора изолировать. Равно как и наоборот. При этом следует отличать изменения и дополнения. Грубо, изолировать надо когда сложно следовать принципам SOLID, прежде всего SRP и OCP.

почему? в каждом проекте с самописными репозиториями я видел классы на 1к+ строк. в единственном виденном проекте на спецификациях был структурированный, сгруппированный код, в котором легко ориентироваться и вносить изменения. и ни разу я не встречал случая смены реализации DAL. это если отталкиваться только от опыта, что субъективно.

если объективно — то какие преимущества дает абстракция над EF?
Попробуйте представить это иначе. Не абстракция над EF, а класс с помощью EF реализующий контракт бизнес уровня на доступ к данным.

Если вы изначально проектируете от доменного уровня, вам вообще не важно, EF там или файлы с json вычитываются с SMB шары. Домен просто хочет IUserDataStore. И что прикольно, пока в начале проекта вы пишете доменный уровень — у вас в тестах на моках всё будет крутиться безо всяких EF и в принципе баз данных.
но зачем тогда EF? использовать его только как источник данных избыточно, тот же даппер делает это намного проще.
А если вам хватает Dapper, чтобы реализовать ваш IUserDataStore, то может на нем и стоит остановиться.

Абстракция уровня доступа к данных как раз об этом — бизнесу не должно быть важно, как берутся данные.
>вам вообще не важно, EF там или файлы с json вычитываются с SMB шары.

Нет, важно. Никакое вычитывание с SMB, прости господи, «шары», и никакой json не даст того, зачем к типичному энтерпрайзному проекту прикрепляют базу — MS SQL, разумеется. Ни производительности, ни надёжности, ни возможностей.

К типичному энтерпрайзному проекту базу применяют по умолчанию. MS SQ не по умолчанию — как минимум есть Oracle. Но такой типичный энтерпрайзный проект быстро превращается то ли в спагетти, то ли в лазанью, если в нём абстракции текут или их вообще нет, если у одного класса 100500 ответственностей, а постановки бизнес-задач вынуждены оперировать операциями типа "загрузить из таблицы customer пользователей, у которых год не было активностей и для каждого сделать http-запрос на домен very-cool-sms-provider.com POST /send/{customer.phone/} "{message: "забыл про нас? ща напомним"}"

Ох как классно расписано. Я пытался сделать что-то вроде спецификаций, но для такой красоты не хватило, наверное, владения языком. Спасибо!
А если create/update/delete требует выполнить много операций для некоторых сущностей?

  • Сохранить версию сущности в специальную таблицу.
  • Обновить audit-лог.
  • Обновить связанные сущности.


А есть ещё логика импорта/экспорта сущностей, которая может быть довольно нетривиальна.

Если нет цели абстрагироваться от EntityFramework, то такие задачи удобнее всего решать на уровне DbContext. Например, в моем велосипеде такое есть.


А если такая побочная логика слишком нетривиальна, то лучше вообще использовать CommandHandler из CQRS. Там по крайней мере можно запрятать каждый аспект (история, аудит лог и т.п.) в свой декоратор вокруг CommandHandler. А иначе все это осядет в нашем многострадальном репоизтории.

В репоизториях. У меня есть базовый репозиторий и много репозиториев-наследников для каждой сущности. Где можно переопределять логику. Если всё пихать в DbContext, то будет куча методов, типа DeleteUser, DeleteGroup и т.п. А так у меня правильное удаление групп и юзеров лежит в UserRepository.Delete и GroupRepostiroy.Delete удобно использовать, легко поддерживать не надо усложнять себе жизнь.

А как связаны UserRepository и GroupRepostiroy, если надо удалять пользователей вместе с группой? Через DI, или эвенты?


Похоже, мы не поняли друг друга. Под "решать на уровне DbContext" я понимаю вот что


Скрытый текст
class User : IAuditable { }

public override int SaveChanges()
{ 
    foreach (var auditable in ChangeTracker.Entries().OfType<IAuditable>())
    {
        // do whatever you want
    }
    return base.SaveChanges();
}

Если не хочется такое руками писать — есть EntityFramework.Triggers


или CommandHandler
class DeleteUserHandler : ICommandHandler<DeleteUserCommand>
{
   public void Handle(DeleteUserCommand command)
   {
        // do whatever you want
   }
}

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


В CommandHandler же только одна операция. И можно легко написать декораторы:


Скрытый текст
class AuditableCommandHandler<TCommand> : ICommandHandler<TCommand>
{
    public AuditableCommandHandler(ICommandHandler<TCommand> inner) { }
}

Считаю, что как-только проект перестает CRUD, то Generic Repository должен вводиться принудительно. И не в замене ORM дело, а в полном абстрагировании от системы персистентности. Их под капотом может быть даже несколько одновременно. А может и вообще не быть (для тестов, например).


Удалить из интерфейса все уникальные фичи каждой библиотеки.

Не то, что удалить, а даже вводить их не надо.


Объяснить товарищам по команде, почему они не могут теперь пользоваться уникальными фичами любимого ORM

Объяснить товарищам где они могут увидеть свою любимую ORM

Считаю, что как-только проект перестает CRUD, то Generic Repository должен вводиться принудительно. И не в замене ORM дело, а в полном абстрагировании от системы персистентности. Их под капотом может быть даже несколько одновременно. А может и вообще не быть (для тестов, например).

На бумаге это все выглядит гладко, но как реализовать запросы? 99% Generic Repository имеют LINQ-интерфейс, но не все системы персистентности его поддерживают. Если вы хотите подключить Dapper, Elastic Search (под последний что-то есть, но так себе)?


Как реализовать интерфейс запросов, независимый от системы хранения данных?


Только в случае DAO легко видится: есть запрос GetActiveUsers, одна реализация с LINQ+EF, другая на чистом SQL к Dapper...

Классические спецификации, тоже являющиеся частью домена, внутри реализации репозитория преобразуются в LINQ, SQL, HTTP, вызовы Redis, чего угодно в общем.

Главная проблема подхода "работать с EF напрямую, отправляя запросы из бизнес-логики" — сложность замены схемы хранилища. Например, ввода версионности. Или перехода на EAV. Или перехода с EAV… Пока весь проект не будет переписан — он даже не скомпилируется.


Ладно еще одному таком переписыванием заниматься, но когда команда параллельно разрабатывает новые фичи — можно вешаться.

хорошо, допустим даже вы не будете менять ОРМ потом но где тогда размещать все эти ГетБайАйди? В DBContext? Это искажает его ответственность.

Ну, в DbSet уже есть ГетБайАйди, называется Find (жаль только что object принимает).

Больше абстракций богу абстракций! Это уже не репозиторий даже. Это как Session в Nhibernate.
Репозиторий это коллекция доменных объектов. А тут у нас все вперемешку лежит. Да еще и UoW сбоку приклепали. Не скажу, что получилось неудобно. Но это скорее про уменьшение boilerplate, чем про разделение ответственностей.
> В LINQ to Entities в качестве спецификаций используется Expression<Func<T, bool>>. Но такие выражения нельзя комбинировать с помощью булевских операторов и использовать в LINQ to Objects.
> Попробуем совместить оба подхода. Добавим метод ToExpression():

Какой в этом смысл? Не лучше ли просто определить методы расширения на Expression<Func<>>?

Можно и так. Как раз этот подход реализуется в LinqKit

Спасибо, хорошая статья! Тоже отказались от репозиториев, когда нужна быстрая разработка и внедрение лишние слои только отнимают время (это же не только разработка — длительные обсуждения тоже). Есть слой BL — использует EF.
CURD is a dairy product obtained by coagulating milk in a process called curdling. The coagulation can be caused by adding rennet or any edible acidic substance such as lemon juice or vinegar, and then allowing it to sit. The increased acidity causes the milk proteins (casein) to tangle into solid masses, or curds.
image
Если реализовать базовый generic класс для repository, который покрывает базовые потребности CRUD + пробросить наружу нужные интерфейсы DbSet. То можно получить крайне тонкую обертку над EF и отвязать бизнес логику от доступа к данным. Для сложных задач по доступу данных мы можем наследоваться от generic класса и реализовать нужные методы. Ярким примером жесткой связки бизнес логики и логики хранения данных может послужить ASP.NET Identity. В проекте, где используется, к приему Dapper и EF вообще не нужен, вам нужно будет переопределить множество моделей + реализовать собственные storage. Этот тот самый пример, когда жесткое связывание хранения данных и бизнес логики дает осложнения потом.
От себя добавлю, что ОРМ бывает меняется в начале разработки проекта, когда тестируются различные варианты. А после этого уже незачем избавляться от удобной абстракции. + различный добавочный функционал по обновлению каких-либо полей внутри Update\Delete.

Автор не потрудился почитать описание паттерна Репозиторий, какие проблемы он решает, а так же не понял где у него проблема в принципе(в разных источниках). Хотя с QueryObject еще не все так плохо и они вполне жизнеспособны, но в другом месте.


Начну с хорошего:


  • В папочке с QueryObject в принципе легче ориентороваться.
  • Их проще писать, проще внедрять в сервисы.
  • Проще вводить новые версии.
  • Проще моки писать в тестах — не надо все зависимости репозотория переопределять.
  • Еще отлично встроятся в read часть вашего приложения, если вы запилили CQRS.
    На этом все.

Теперь о плохом. Репозиторий.
В первую очередь Репозиторий — это интерфейс коллекции доменных объектов. Он абстрагирует нашу систему хранения данных. А ORM — Это вообще не про это. ORM про маппинг сток из базе на объекты.
Эти 2 паттерна решают разные проблемы и используются по разному.


Даже Фарулер говорит:


A Repository mediates between the domain and data mapping layers.
https://martinfowler.com/eaaCatalog/repository.html

Data Mapping Layer — это как раз таки Ваша ORM.


Использование класса из ORM вместо интерфейса репозитория — это циклическая зависимость в вашем коде. Если вы захотите заменить систему хранения данных — вы попали. Хотя в некоторых случаях такое упрощение в полне себе решение. Никому эти репозитории в блоге или сайте визитке не нужны.


Пример из реального проекта.
Одним из решений для задачи — было запилить EventSourcing. В качестве первого решения, было решено использовать MySQL как хранилище данных. Так было проще начать.
Со временем оказалось что:


  • все события просто напросто физически не влезут в него, хард не резиновый.
  • использовать MySQL финансово не выгодно.

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


Это то что касается Репозиториев.


Теперь про количество методов в Репозиториях.
Репозиторий используется совместно с ОРМ. Сейчас мы обратим внимание на то что он используется именно для Маппинга Доменных Объектов. И возвращает коллекцию доменных объектов.
Большинство методов, от которых растет Репозиторий и на которые грешит автор — это методы для чтения данных.
Так вот. Если вы подумали хорошо, когда проектировали ваше приложение — то наверное запилили бы CQRS. Тогда в вашем слое C были бы UseCase, которые использовали бы Репозитории с ОРМ.
А в слое Q были бы ваши QueryObject или что по проще, без ОРМ. Он там нафиг не впился.


Другими словами — Репозитории с ОРМ вам нужны там, где бизнесс логика. Там где просто вывод данных — вам и ОРМ не впилась, используйте SQL.


Вы можете посмотреть видео Marco Pivetta "Doctrine Best Practices". Не надо плеваться что это Доктрина, принципы в ОРМ одни и те же.


А нафиг оно вообще все надо? Зачем усложнять?
Все эти дела с перозиториями, орм, интерфейсам, и т.д. и т.п. порой появляются на ровном месте.
Ваше решение должно быть обосновано, внимание, ТРЕБОВАНИЯМИ К ПРОЕКТУ, а не личными хотелками.
Если вы пишите блог — не надо там квери обьекты городить или отдельные репозитории вводить. Берите репозиторий ОРМ и в продакшин. А если у вас что-то больше — обязательно проверьте не будет ли меняться система хранения данных в конкретной части приложения. Ведь мы можем писать в мускуль, а клиенту отдавать из редиса. И то и то можно спрятать в репозитории. И когда прийдет время заменить имплементацию на что-то еще — вы не будете плакать.


Для лучшего понимания рекомендовал бы посмотреть видео Евгения Кривошеева "Осознанность проектирования" и "Как не угробить архитектуру сразу же". Можно найти тут же на хабре. Там еще интересное.

Ну, во-первых, не знаю как другие ОРМ, а EntityFramework предоставляет сразу несколько паттернов: DataMapper, QueryBuilder, Repository, UnitOfWork. И незачем их переизобретать.


Во-вторых не класс из ОРМ вместо репозитория, а интерфейс из ОРМ.


А в остальном согласен.

Да немного сумбурно получилось. Но я не призываю ничего переизобретать.
Речь про то что надо думать о зависимостях в проекте.


Я тут капитана очевидность включу не на долго:
Обычно создается класс наследующий репозиторий из ОРМ и имплементирующий наш интерфейс.
Но интерфейс определяется в самом приложении. Нельзя использовать интерфейс из ОРМ. Т.к. это будет нарушение принципа инверсии зависимостей.


Формулировка:
  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Первый пункт как раз об этом.
кэп офф.


И это конфликтует с Вашим выводом:


Уважаемые коллеги, не пытайтесь абстрагироваться от выбранного Вами фреймворка! Как правило, фреймворк уже предоставляет достаточный уровень абстракции. А иначе зачем он нужен?

Я это к тому что — не стоит выдавать такие утверждения. В интернетах полно неопытных ребят. И вы можете наблюдать их в комментариях к статье тоже. И если они без понимания проблемы рванут прикручивать ваше решение — это до добра не доведет.


Самое печельное, что они искренне уверены, что они знают как надо использовать фреймворк.
http://sergeykorol.ru/blog/competence/

Только на практике чаще бывает наоборот. По крайней мере в .NET.
— Сначала люди начитаются туториалов, и пишут свой абстрактный репозиторий. Почему-то им кажется, что они-то спроектируют его лучше уже готовых.
— Потом начинают использовать его в других проектах внутри конторы.
— Потом оформляют в NuGet пакет.

И чем зависимость от своего пакета абстракций лучше зависимости от пакета абстракций Microsoft? Как-то NIH-синдромом попахивает. Это даже безотносительно затрат на поддержку своей реализации.

Ну так и качается скил :)


Сначала мы не знаем ничего (Неосознанная Некомпетентность).
Читаем туториал. С абсолютной уверенностью, что мы делаем лучшую реализацию в мире, запиливаем это в проект.
Глаза горят, добавляем соседу в проект. Выделяем в пакет.
Потом, набив шишки в 2-3 проектах, идем искать ответы на новые вопросы.
Оказывается что проблем то не мало.


И тут мы понимаем что чего-то не знаем (Осознанная Некомпетентность).
Начинаем усиленно читать, смотреть конференции. Всеми путями получаем как можно больше знаний.
И когда познаем дзен — переходим в состояние Осознанной Компетенции.
Умеем использовать Репозитории, пилить CQRS и EventSourcing.
Видим правильные проблемы. И умеем их правильно решать.


Это естественный процесс. И очень длительный, если конечно у вас под рукой нет компетентного наставника.
Я не один проект написал с жирными репозиториями, пока научился этому.
Главное задавать как можно больше вопросов и искать ответы. Критически мыслить.


А зависимость от своего интерфейса лучше тем, что:


  1. это наш интерфейс, и только мы можем его менять. Никакой сторонний пакет вдруг не скажет что метод депрекейтед или изменит порядок аргументов. Даже если что-то и поменяется — внутри нашего приложения ничего не изменится, т.к. мы только изменим имплементацию этого интерфейса а одном месте.
  2. в интерфейсе будут только те методы, которые нам действительно нужны. Тогда когда мы заходим переехать с MySQL на NoSql — мы будем знать точно обьем работ.
  3. это просто тестировать

NIH синдром — это если бы мы пилили действительно всю ОРМ с дата мепперами и юнит оф ворк. А с Репозиторием чаще всего получается как я описал в комменте выше. Т.е. затраты на поддержку === 0. Ведь мы по факту используем то что предоставляет фреймворк, просто внутрь нашего приложения не просочится инородный интерфейс.


Весь паттерн Репозиторий — это про интерфейс, а не про реализацию.

UFO just landed and posted this here
Я имею в виду когда нужен не граф связанных сущностей, а именно что dto. Например коммент + логин автора, а не коммент+автор
Таким образом, чтобы написать Generic Repository нужно:
Собраться с мыслями.
Спроектировать интерфейс.
Написать реализацию под выбранный в проекте ORM.
Написать реализацию под альтернативные ORM


А зачем вообще в таком случае использовать какую бы то ни было ORM? Почему бы просто не использовать запросы на чистом SQL в реализации самого репозитория в таком случае? Не холивара ради пишу, реально не понимаю и хочу понять. Традиционный аргумент о привлечении дополнительного слоя абстракции ради независимости от конкретных СУБД вроде как не в счёт т.к. всё-равно у нас есть пункты «Спроектировать интерфейс» и «Написать реализацию под альтернативные ORM».

Запросы на чистом SQL писать тяжело, а вычитывать из DataReader — нудно и многословно. И все проверки — только в рантайме.

Верный вопрос и предложение. CQRS как раз об этом.
Только не надо добавлять методы с использующие SQL в репозиторий. Это совершенно другая зона ответственности. И назначение другое.
В целом тут на лицо не правильный подход к использованию Репозитория как паттерна. Отсюда и не верное и переусложненное решение.

В этом случае у вас у репозитория будет две отвественности — коллекция объектов и маппинг объектов на СУБД. Другое дело, что для маппинга необязательно использовать универсальные ОРМ, а можно написать свой маппинг с чистым SQL

gnaeus, cкажите, в файле Specification.cs на строке возвращающей результат указано нечто неожиданное:
public static bool operator true(Specification<T> spec)
{
    return false;
}

Подскажите, это тонкий расчёт или ошибка?

Это черная магия конечно =) Впрочем, все в соответствии с C# Language Specification [7.11.2], как я уже упомянул в статье.


The operation x && y is evaluated as T.false(x) ? x : T.&(x, y)
The operation x || y is evaluated as T.true(x) ? x : T.|(x, y)

Мы хотим, чтобы выполнялась всегда правая часть тернарного оператора. Поэтому и operator true(), и operator false() должны возвращать false.

В итоге все становиться очень похоже на то как сделано в django Managers и Q-objects

Manager — расширение над QuerySet, со своими методами, Q-objects — те самая спецификации, которые можно комбинировать и использовать для запросов.
Все замечательно, если у вас одна БД или БД одного типа. А если легаси система, или несколько разных систем и бизнес-объект надо собирать из разных мест?

А вот это как раз "fair use". Ваш репозиторий — это не адаптер над ORM, который транслирует методы практически один-в-один с точностью до переименования. А фасад поверх API нескольких разнородных систем.


Но это если сложность Вашей бизнес-логики оправдывает внедрение DDD. Ведь снаружи Ваш репозиторий — это in-memory коллекция. А внутри — преобразование доменной модели в несколько других + вызов нескольких внешних систем + возможно, координатор распределенных транзакций.


А если надо просто сделать REST-сервис поверх пары БД и одного SOAP (как это обычно и бывает), тогда проще CQRS.

CQRS и Repository никак друг друга не исключают. Как Repository не подразумевает DDD.


Repository по сути локальная абстракция для хранилища данных — независимая от реализации хранилища представление данных для клиента репозитория в виде in-memory коллекции сущностей домена. Что часто конкретная реализация сводится к трансляции методов ORM (вернее ORM-библиотеки, кроме собственно ORM, реализующей обычно кучу паттернов, среди которых есть и что-то похожее на классический Repository) — лишь частный, пускай и частый случай.


Основная цель репозитория в моем понимании — не возможность быстро поменять слой хранения, а очистить бизнес-логику от деталей реализации слоя хранения. Разнести две сложных концепции в разные слои исключительно с односторонней зависимостью.

Все классно ровно до того момента пока у нас повсеместно используется один контекст и одна база, да причем еще и SQL. Как только появляются разные базы для разных целей, то слой из репозиториев становится уместным и даже необходимым, т.к. он гарантирует единообразный доступ к данным. Кроме того, репозитории часто помогают абстрагироваться от использования монструозных моделей с бесконечно живущими контекстами. И не надо забывать что использование экстеншен методов нарушает ООП, потому как методы оказываются снаружи и чтобы реализовать их, придется иметь доступ к внутренним особенностям объекта.

То есть когда мы пишем Foo(bar) — это нормально и ООП не нарушает, а как только написали bar.Foo() — это ой-ой, кошмар и нарушение ООП?

А кто сказал что Foo(bar) это нормально? Надо как-то живой пример. В теории слишком много различных трактований.
Но вы почему-то высказались не против передачи объектов в функцию вообще, а против методов-расширений. Вот я и спрашиваю в чем именно расширения провинились по сравнению с обычными функциями.
И те и те провинились. Расширения ничем не отличаются от функций, это же почти синтаксический сахар.
В таком случае следующий вопрос. Если у сущности User есть свойство UpdatedAt и к нему можно обращаться при составлении запроса — то почему к нему нельзя обратиться из функции которая принимает User параметром?
Потому что вы не знаете что еще эта функция может взять у User'a. Надо стараться передавать только самый минимум данных. Передача всего юзера это избыточное разглашение. Bad Design, короче.
> Передача всего юзера это избыточное разглашение. Bad Design, короче.

Разглашаются только публичные поля, в чем избыточность?

Выделить интерфейс UserTimestamps, сделать функцию принимающей инстанс этого интерфейса, а по факту передавать User как имплементацию — не решение?

> Как только появляются разные базы для разных целей, то слой из репозиториев становится уместным и даже необходимым

У вас же уже есть слой репозитория в виде контекста. Зачем нужен еще один?
А как мне с одним контекстом использовать и SQL и, например, MongoDB?
Зачем с одним? Инжектите другой контекст и работаете.
Вы не знаете, как работает DI? Я думаю, знаете. В чем именно состоит вопрос, уточните. В каком именно месте вы видите проблему?
Ну я правда не понимаю как использовать контекст (если имеется ввиду Entity Framework DB Context) для работы с MongoDB
Ну это вы зачем-то предложили использовать EF с монгой :)
Это будет очень криво, но _в принципе_ использовать можно, так же как и с любой другой бд.
А где, простите, я предложил использовать EF с монгой?
А какой «контекст» вы подразумевали? Я подумал, что это DbContext из EF.

Вы уверены, что это нужно делать именно одним контекстом? У него не будет практически ни одного свойства, которое ожидается от контекста (транзакционность, трансляция LINQ в SQL). По-моему, такое нужно делать как можно более явным, а не инкапсулировать.

И контекст это не репозиторий, а юнит оф ворк.
Это и то и другое. Если убрать из контекста SaveChanges() и связанные с ним вещи — останется чистый классический репозиторий ака слой абстракции, скрывающий низкоуровневые особенности источника данных, и предоставляющий функционал маппера + queryBuilder'a.
Конечно нет, репозиторий это именно контекст. DbSet репозиторием быть никак не может, т.к. представляет только одну таблицу. Репозиторий же по определению содержит все требуемые данные.
А можно увидеть ссылку на это определение?
Например, по Фаулеру, репозиторий — медиатор между доменным слоем и слоем датамаппера, обеспечивающий возможности выборки по критериям.
Как вы собираетесь делать выборки, если у вас доступна только одна таблица? Да никак, в любых случаях кроме самых тривиальных это просто не работает.

На том же msdn тоже про DbContext указано, что это репозиторий, а вот про DbSet подобного нет.
Вот цитата из MSDN: A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that it can be used to query from a database and group together changes that will then be written back to the store as a unit. DbContext is conceptually similar to ObjectContext.
Т.е. ребята сами нарушают SRP. Океееей.
> Т.е. ребята сами нарушают SRP.

В данном случае в этом нет ничего плохого — задачи ведь концептуально связаны. Принцип — это не правило, а лишь рекомендация к действию. Нарушать его не просто можно, но во многих случаях — нужно. Хорошего кода без нарушения SRP вы никогда не напишите (если приложение хоть сколько-нибудь сложное).
Например, по Фаулеру, репозиторий — медиатор между доменным слоем и слоем датамаппера, обеспечивающий возможности выборки по критериям.

И где здесь о том, что репозиторий должен содержать все требуемые данные?


Как вы собираетесь делать выборки, если у вас доступна только одна таблица? Да никак, в любых случаях кроме самых тривиальных это просто не работает.

Очевидно же, путем запроса нескольких репозиториев.


DbContext, DbSet и прочий LINQ — это не классический репозиторий, это сущности, поддерживающие описание запросов к источнику данных на .NET совместимом языке. Он обязан иметь доступ ко всем возможным сущностям, т.к. неизвестно, какие будут запросы.

> И где здесь о том, что репозиторий должен содержать все требуемые данные?

Потому что если он не содержит — то операцию выполнить нельзя.

> Он обязан иметь доступ ко всем возможным сущностям, т.к. неизвестно, какие будут запросы.

Это же верно про репозиторий.

> Очевидно же, путем запроса нескольких репозиториев.

Тогда в чем смысл наличия репозитория? Предполагается, что он должен скрывать какую-то логику за слоем абстракции, ведь так?

Репозиторий не содержит данных обычно, а инкапсулирует логику доступа к хранилищу, реализуя для него абстрактный интерфейс, не зависящий от типа хранилища.


Репозиторий может отвечать как за единственную сущность, так и за несколько. Некоторые практики предполагают создание отдельного репозитория для каждой сущности или агрегата без связей репозиториев друг с другом. Условный "джойн" для сущностях связанных, но не принадлежащих одна другой осуществляется на уровне клиентов репозиториев.

> Некоторые практики предполагают создание отдельного репозитория для каждой сущности или агрегата без связей репозиториев друг с другом.

И это не репозитории, потому что в реальности ф-ю репозитория выполнять они не могут. На практике запросы требуют выборок по нескольким сущностям, и если репозиторий не предполагает такой возможности, то он просто не способен инкапсулировать никакой нетривиальной логики. И, значит, не нужен, потому что ничего не делает.

Даже некоторые СУБД не могут выполнять выборки с операциями типа JOIN и дажу WHERE c условием отличным от сравнения с первичным ключом — они не СУБД по вашей логике?


Репозиторий не обязан предоставлять доступ ко всем данных всех хранилищ в любой комбинации. Функция репозитория — инкапсулировать логику хранения, предоставляя публичный интерфейс подобный коллекции.

> Репозиторий не обязан предоставлять доступ ко всем данных всех хранилищ в любой комбинации.

Он обязан предоставлять такой доступ к данным, чтобы можно было делать требуемые выборки. В _некоторых_ случаях «по репозиторию на сущность» будет достаточно, но в общем случае — нет.

> Функция репозитория — инкапсулировать логику хранения, предоставляя публичный интерфейс подобный коллекции.

Ну вот DbContext эту логику инкапсулирует и интерфейс предоставляет.

А всякие репозитории, которые пишутся поверх него — обычно не предоставляют.
Он обязан предоставлять такой доступ к данным, чтобы можно было делать требуемые выборки.

Он обязан предоставлять ровно такой доступ к данным, который объявлен в его публичном контракте. И как раз в общем случае репозитория на сущность будет достаточно, а в некоторых — нет, прежде всего из-за ограничений по времени и памяти на стороне клиента, когда ему придётся совершать операции над данными из двух репозиториев, которые СУБД могла бы совершить более эффективно.

> Он обязан предоставлять ровно такой доступ к данным, который объявлен в его публичном контракте.

Мы сейчас говорим именно о том, каким должен быть публичный контракт. Совершенно очевидно, что чтобы что-то было репозиторием, его публичный контракт должен удовлетворять определенным условиям.

> прежде всего из-за ограничений по времени и памяти на стороне клиента, когда ему придётся совершать операции над данными из двух репозиториев, которые СУБД могла бы совершить более эффективно.

Репозиторий за тем и нужен, чтобы сообщить хранилищу: «эй, произведи-ка вот эти операции, и по-эффективнее там!». Если вам приходится данные через репозиторий сперва выгружать, а потом как-то нетривиально обрабатывать (хотя можно было сделать это средствами хранилища), то это и говорит о том, что ваш репозиторий — очень плохой, негодный. И как раз принцип «репозиторий на сущность» к такому и приводит обычно.
Совершенно очевидно, что чтобы что-то было репозиторием, его публичный контракт должен удовлетворять определенным условиям.

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


Репозиторий за тем и нужен, чтобы сообщить хранилищу: «эй, произведи-ка вот эти операции, и по-эффективнее там!».

Репозиторий нужен, чтобы развязать хранилище и его клиента. За эффективностью выборки стучитесь в базу напрямую или вообще реализуйте логику на её стороне. Ну и сейчас всё чаще встречаются системы, где у репозитория по факту только единственный метод чтения — получение объекта по идентификатору для последующего изменения и сохранения его состояния. Сложные выборки осуществляются путём проецирования операционного хранилища на другие, оптимизированные под эти сложные выборки, пускай и банальным view.

> Репозиторий нужен, чтобы развязать хранилище и его клиента. За эффективностью выборки стучитесь в базу напрямую или вообще реализуйте логику на её стороне.

Подобное развязывание — идея совершенно утопическая. На практике все равно приходится использовать как специфические возможности ОРМ, так и хранилища. И если если для написания любого нетривиального запроса приходится репозиторий бипассить, то тогда зачем он нужен? Разве что во время прототипирования — но, опять же, нафига усложнять написание прототипа лишними слоями абстракции?
Sign up to leave a comment.

Articles