Comments 159
базовый класс для доменной сущности
Ну это как-то ортогонально созданию кастомного репозитория.
А вот насчет Repository и UnitOfWork — либо через год они превратятся в неподдерживаемое нечто (времени на рефакторинг не хватило). Либо со временем окажется, что Вы пишете на собственном фреймворке поверх EF (времени хватило).
А еще, такие репозитории — это пустая абстракиця. В них нет ни бизнес-логики, ни инфраструктурной логики. Это как адаптер. Только он используется не для интеграции двух разных модулей. А для интеграции EF и кода, который еще даже не написан.
жаль, те кому это надо прочитать, не прочитают. ведь и так понятно, что в папочке DAL должен лежать UserRepository… с тыщей методов.
У меня за последние три года несколько раз случались холивары на эту тему с товарищами по команде.
Причем в разных конторах. Вот, решил оформить свои мысли в статью.
ORM в целом не абстракция от базы данных, а репозиторий — абстракция от системы хранения.
ORM в целомУ вас тут «не» лишнее?неабстракция от базы данных
Я бы абстрагировал программистов:)
Тут я пожалуй соглашусь. Мне самому комфортнее с extension-методами. Особенно, если дополнить их возможностью вызова из ExpressionTree, как описано в конце статьи.
Спецификации — это не способ облегчить жизнь разработчику, а скорее способ оформить все "по феншую" (DDD, TDD, переиспользование и т.п.).
И еще иногда (очень редко) возникает потребность динамически комбинировать условия с выборки помощью OR. Для AND все просто — добавляем в цепочку запроса несколько .Where()
А для OR или спецификация, или PredicateBuilder из LinqKit
Есть ещё LinqSpecs. Ваша реализация очень похожа на неё. Я некоторое время назад тоже сравнивал варианты реализации спецификаций через extension'ы и expression'ы: https://habrahabr.ru/post/325280/
Насколько эта expression-магия наносит урон по производительности? Сможет ли EF кэшировать запросы в таком случае?
Это Вы про extension-методы? Хороший вопрос на самом деле. Надо проверять.
Но, чисто умозрительно, кэширование должно работать. Магия не встраивается внутрь EF. Она работает как декоратор вокруг LINQ IQueryable. И тот Expression, который содержится во внутреннем IQueryable из EntityFramework, уже не содержит никакой магии. Ну и компиляция SQL происходит из нормального ExpressionTree.
В принципе, когда мы добавляем в цепочку вызовов еще один LINQ-метод:
query.Where(...).Select(...).OrderBy(...)
мы точно так же модифицируем IQueryable.Expression
.
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 или СУБД. А в целях создания искусственных ограничений, превращая БД в абстрактное хранилище данных. И в целях уменьшить или свести к нулю проблемы тестирования.
Соответственно, для 80-90% запросов, можно прекрасно прожить в рамках этого ограничения. А оставшиеся узкие места оптимизировать специальным образом, например, создавая специализированные реализации репозиториев для некоторых сущностей вместо Generic Repository.
И это всё также хорошо работает как со спецификациями, так и Query Extensions или Query Object.
Вот с чем не поспоришь, так это DAL, описанный вами в статье — действительно зло.
А вот от догмы, что все запросы к MyEntity должны лежать в MyEntityRepository нужно отступить.
А что оптимизировать Dapper-ом в GenericRepository — CRUD или специфические запросы?
Если запросы, то это уже не Repository. Это DAO, или QueryHandler из CQRS, или вообще какой-то сервис. Короче, абстракции протекают.
Абстракции начинают «протекать», когда одно пытаются натянуть на другое. Необходимо ограничить обязанности и чётко понимать, для чего вводится абстракция. Когда это звучит вот так: «репозиторий для замены ORM» — это совершенно некорректная задача и обязанность. Зачем его заменять вообще? Абстракция репозитория это абстракция хранилище данных. Она прекрасно мокается, не зависит от внутренностей и багов ORM, которые можно подпереть в случае чего костыликом до лучших времён. Репозиторий может быть прокси, вычисляющим время запроса, вести аудит и поддерживать любую инфраструктуру. На маленьких проектах это может и не заметно, и там можно обойтись IDbSet, или вообще просто пробрасывать DbContext, чего мелочиться?
Когда это звучит вот так: «репозиторий для замены 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-код, который извлечет из БД доменный объект?
QueryObject может возвращать что угодно. Мы пришли к выводу, что в read-части DTO лучше: легче запрос, меньше затрат на материализацию, нет проблем с сериализацией и накладных расходов на дополнительный маппинг, нет опасности нарваться на lazy-load и связанные коллекции (опытный разработчик сразу такое заметит, а junior — нет).
Доменный объект чаще нужен по id во write-подсистеме. Его проще получать из DbContext'а или своего UoW, если есть в наличии. Специально на эту тему не думали, получилось из практики. Возможно, другие проекты с другой спецификой нужно проектировать иначе.
Только вот замокать репозиторий с достаточно богатым API
Нужно разделять репозитории по смыслу. Чтобы сохранялся in-memory контекст — делается UnitOfWork и через него происходит получение репозиториев разных типов.
Вот пример от мелкомягких: implementing-the-repository-and-unit-of-work-patterns.
Плюсов много. К примеру когда я хочу добавить индексы в таблицу — я пересматриваю все запросы и сразу оптом их добавляю. Все запросы в одном месте, не нужно искать их по коду сервисов.
с помощью 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 производит материализацию запроса, а что происходит когда он в середине запроса?
Мнение 2.
Pluralsight — Entity Framework in the Enterprise Julie Lerman говорит что долго использовала R паттерн, и до сих пор многие из ее знакомых его используют. Это как привычка.
Для меня это место хранения часто используемых запросов к сущности.
Я тоже смотрел это видео перед написанием статьи =) На первый взгляд, там все правильно и красиво. Единственный вопрос — а зачем?
место хранения часто используемых запросов к сущности
А вот тут уже суровая реальность: Вы будете выносить запросы в репозиторий по мере рефакторинга. А Ваш коллега просто напихает от балды. Со временем все равно класс распухнет. Потому что его ответственность "часто используемые запросы" слишком размыта. Группировать запросы надо. Но по бизнес-кейсам, а не по сущностями.
А вдруг, при реализации нашего чудо-репозитория мы воспользовались уникальными особенностями конкретного ORM?
А какая разница, какими особенностями мы воспользовались? У нас есть интерфейс, который говорит, что если попросить User UserOfId(UserId userId) то получим пользователя. Внутри конкретного репозитория это может быть сделано как угодно, главное, что в итоге есть User.
Очень многие используют репозиторий для получения вообще любых данных, хотя по идее репозиторий описывает то, что нужно бизнес-логике.
В интерфейсе репозитория, и в реализации соответственно, мы создаем такие методы, которые нам нужны для применения каких-то изменений к моделям. Если нам где-то нужно вывести постраничный список пользователей с разными условиями — это не имеет отношения к сохранению данных этих пользователей или манипуляцией их списком как одним целым. И в этой ситуации нам не нужны никакие репозитории, можно хоть прямо в базу запросы слать.
Интерфейс репозитория, это как документация, он сообщает нам, что, например, где-то есть бизнес-процесс, которому нужно получить всех пользователей с датой регистрации не позднее какого-то числа. И это нужно не потому, что мы хотим вывести это где-то в интерфейсе, а потому что мы будем проводить какие-то манипуляции с такими пользователями, принимать какое-то бизнес-решение. Если же нужно просто выводить список ограниченный по дате, используем то что удобно.
Почему не прикреплять этот метод к обработчику бизнес-процесса?
Вот например есть бизнес-процесс: модератор хабов просматривает список пользователей по дате, и принимает решение — выдавать ли инвайт. Где поместить метод фильтрации пользователей?
Момент просмотра модератором списка хабов, это еще не момент выполнения какого-то кода в бизнес-логике.
Как быть с правами? Например, у меня в приложении своя система прав. Все через стандартный «репозиторий» ходят мимо прав?
А репозиторий и есть доменный сервис. Конкретные реализации в инфраструктурном слое, а интерфейс в доменном.
И тут я вас не очень понял, как вы предлагаете обойтись без объекта.
Допустим:
Руководитель отдела может оперировать только своими сотрудниками.
Если мы добавим фильтрацию в репозиторий, то в сервисе уже будут уже отфильтрованные сотрудники. Если реализовывать в сервисе — то нужно цеплять эту фильтрацию везде, где надо (разумеется, можно вынести в Extension метод или еще куда-нибудь) и постараться не забыть и ничего не сломать в процессе.
Мне нравится репозиторий как набор запросов.
Только не один гигантский на весь проект, а набор маленьких по сущностям и фичам.
В чем преимущество
- легко мокать (если они маленькие).
- сразу видно какие именно операции с базой могут выполняться
При этом необязательно отказываться от всего описанного в статье: репозиторий вполне может возвращать IQueryable, и к нему можно применять спецификацию.
Обобщенный репозиторий плох тем, что он не ограничивает никак ни чтение данных (т.к. доступна вся таблица), ни запись.
Свой же репозиторий может в методе All() возвращать не всю таблицу, а только доступные пользователю записи, например.
Также репозиторий как набор запросов позволяет легко и безболезненно для остального кода проводить оптимизацию отдельных методов, перенося их в базу как функции или вьюшки.
Но это не репозиторий — это DAL-сервис.
Набор запросов — это как раз спецификации. Каноническая реализация должна отвечать требованиям компонуемости, т.е. Если есть ASpec и BSpec, то можно сделать aAndB = aSpec && bSpec; и aOrB = aSpec || bSpec; В .NET expressions не без особой уличной магии отлично выполняют роль спецификаций.
Как я уже писал в комментарии выше, нужно различать зависимость между архитектурными слоями и зависимость от NuGet пакетов. Если бы EntityFramework был разделен на два пакета: абстракции и реализация, как сейчас Microsoft делает для многих пакетов под dotnet core, то можно было бы "собрать" слой домена из интерфейса репозитория в пакете EntityFramework.Abstractions и набора доменных сущностей в Вашей сборке. А слой DAL — собственно сам EF.
Сейчас конечно интерфейс IDbSet лежит прямо в EF, и не совсем подходит в качестве классического репозитория. Но кастомный репозиторий получится еще хуже, по крайней мере если Вы не потратите существенное время на проектирование.
Никакой домен не должен ссылаться на EF.Abstractions, потому что с точки зрения домена нет EF, есть рейсы, покупатели, брони и прочие скидки.
Честная слоёная архитектура подразумевает сильную избыточность. Вы же хотите срезать углы на каждом повороте. Так не бывает. У вас либо честные слои, разрезанная ответственность и тестируемость в изоляции и сильная избыточность кода, либо макароны из зависимостей на какие то библиотеки в домене, но зато быстрая разработка.
На крупных кроссплатформенных проектах ничего подобного в принципе не позволительно. Любая зависимость на внешнюю систему (EF в вашем случае) ОБЯЗАТЕЛЬНО абстрагируется архитектурным швом. Потому что нет никаких гарантий что на платформе X она в принципе поддерживается. Верить можно в libc и изредка в pthreads. Хотя хардварщики наверное и тут плюнули бы мне в лицо :)
Почитайте немного про кольцевые архитектуры (типа Clean Architecture, но она не единственная), и про Dependency Inversion.
Другой вопрос, что код с зависимостью от внешних абстракций ничего не теряет в поддерживаемости и тестируемости. Он теряет в гибкости. А нужна ли Вам такая гибкость?
И при начале разработки надо ответить на вопрос, мы действительно собираемся менять операционную систему / БД / ORM?
Если у нас «коробочное решение», то ответ — Да. А если внутренний сервис, то скорее всего «Нет».
И при начале разработки надо ответить на вопрос, мы действительно собираемся менять операционную систему / БД / ORM?
Неправильно сформулированный вопрос в контексте обсуждения :) Правильный: "мы действительно собираемся изолировать бизнес-логику от инфрастрктуры или нет?"
Ну, я Вам про цели, а Вы мне про средства. Тогда уж — какие преимущества мы получим, если изолирует бизнес-логику от инфраструктуры? И важны ли кто преимущества для данного конкретного проекта.
Упрощение поддержки и развития. Собственно ради этого большинство архитектурных решений и принимается.
Да, но за каждый новый слой абстракции надо платить. И будет ли от этого упрощение поддержки — зависит от проекта.
Конечно зависит. Эмпирическое правило, которое вывел для себя: если при инфраструктурных изменениях нужно лезть туда, где в целом лежит бизнес-логика, то пора изолировать. Равно как и наоборот. При этом следует отличать изменения и дополнения. Грубо, изолировать надо когда сложно следовать принципам SOLID, прежде всего SRP и OCP.
если объективно — то какие преимущества дает абстракция над EF?
Если вы изначально проектируете от доменного уровня, вам вообще не важно, EF там или файлы с json вычитываются с SMB шары. Домен просто хочет IUserDataStore. И что прикольно, пока в начале проекта вы пишете доменный уровень — у вас в тестах на моках всё будет крутиться безо всяких EF и в принципе баз данных.
Нет, важно. Никакое вычитывание с SMB, прости господи, «шары», и никакой json не даст того, зачем к типичному энтерпрайзному проекту прикрепляют базу — MS SQL, разумеется. Ни производительности, ни надёжности, ни возможностей.
К типичному энтерпрайзному проекту базу применяют по умолчанию. MS SQ не по умолчанию — как минимум есть Oracle. Но такой типичный энтерпрайзный проект быстро превращается то ли в спагетти, то ли в лазанью, если в нём абстракции текут или их вообще нет, если у одного класса 100500 ответственностей, а постановки бизнес-задач вынуждены оперировать операциями типа "загрузить из таблицы customer пользователей, у которых год не было активностей и для каждого сделать http-запрос на домен very-cool-sms-provider.com POST /send/{customer.phone/} "{message: "забыл про нас? ща напомним"}"
github.com/NoMoreLoad/CoreSpecs
Забавно получилось)
- Сохранить версию сущности в специальную таблицу.
- Обновить audit-лог.
- Обновить связанные сущности.
А есть ещё логика импорта/экспорта сущностей, которая может быть довольно нетривиальна.
Если нет цели абстрагироваться от EntityFramework, то такие задачи удобнее всего решать на уровне DbContext. Например, в моем велосипеде такое есть.
А если такая побочная логика слишком нетривиальна, то лучше вообще использовать CommandHandler из CQRS. Там по крайней мере можно запрятать каждый аспект (история, аудит лог и т.п.) в свой декоратор вокруг CommandHandler. А иначе все это осядет в нашем многострадальном репоизтории.
А как связаны 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
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...
Главная проблема подхода "работать с EF напрямую, отправляя запросы из бизнес-логики" — сложность замены схемы хранилища. Например, ввода версионности. Или перехода на EAV. Или перехода с EAV… Пока весь проект не будет переписан — он даже не скомпилируется.
Ладно еще одному таком переписыванием заниматься, но когда команда параллельно разрабатывает новые фичи — можно вешаться.
Ну, в DbSet уже есть ГетБайАйди, называется Find (жаль только что object принимает).
Репозиторий это коллекция доменных объектов. А тут у нас все вперемешку лежит. Да еще и UoW сбоку приклепали. Не скажу, что получилось неудобно. Но это скорее про уменьшение boilerplate, чем про разделение ответственностей.
> Попробуем совместить оба подхода. Добавим метод ToExpression():
Какой в этом смысл? Не лучше ли просто определить методы расширения на Expression<Func<>>?
![image](https://habrastorage.org/getpro/habr/comment_images/c94/53a/533/c9453a533db4b456a7fb39a4ffce221f.png)
Автор не потрудился почитать описание паттерна Репозиторий, какие проблемы он решает, а так же не понял где у него проблема в принципе(в разных источниках). Хотя с 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/
— Сначала люди начитаются туториалов, и пишут свой абстрактный репозиторий. Почему-то им кажется, что они-то спроектируют его лучше уже готовых.
— Потом начинают использовать его в других проектах внутри конторы.
— Потом оформляют в NuGet пакет.
И чем зависимость от своего пакета абстракций лучше зависимости от пакета абстракций Microsoft? Как-то NIH-синдромом попахивает. Это даже безотносительно затрат на поддержку своей реализации.
Ну так и качается скил :)
Сначала мы не знаем ничего (Неосознанная Некомпетентность).
Читаем туториал. С абсолютной уверенностью, что мы делаем лучшую реализацию в мире, запиливаем это в проект.
Глаза горят, добавляем соседу в проект. Выделяем в пакет.
Потом, набив шишки в 2-3 проектах, идем искать ответы на новые вопросы.
Оказывается что проблем то не мало.
И тут мы понимаем что чего-то не знаем (Осознанная Некомпетентность).
Начинаем усиленно читать, смотреть конференции. Всеми путями получаем как можно больше знаний.
И когда познаем дзен — переходим в состояние Осознанной Компетенции.
Умеем использовать Репозитории, пилить CQRS и EventSourcing.
Видим правильные проблемы. И умеем их правильно решать.
Это естественный процесс. И очень длительный, если конечно у вас под рукой нет компетентного наставника.
Я не один проект написал с жирными репозиториями, пока научился этому.
Главное задавать как можно больше вопросов и искать ответы. Критически мыслить.
А зависимость от своего интерфейса лучше тем, что:
- это наш интерфейс, и только мы можем его менять. Никакой сторонний пакет вдруг не скажет что метод депрекейтед или изменит порядок аргументов. Даже если что-то и поменяется — внутри нашего приложения ничего не изменится, т.к. мы только изменим имплементацию этого интерфейса а одном месте.
- в интерфейсе будут только те методы, которые нам действительно нужны. Тогда когда мы заходим переехать с MySQL на NoSql — мы будем знать точно обьем работ.
- это просто тестировать
NIH синдром — это если бы мы пилили действительно всю ОРМ с дата мепперами и юнит оф ворк. А с Репозиторием чаще всего получается как я описал в комменте выше. Т.е. затраты на поддержку === 0. Ведь мы по факту используем то что предоставляет фреймворк, просто внутрь нашего приложения не просочится инородный интерфейс.
Весь паттерн Репозиторий — это про интерфейс, а не про реализацию.
Таким образом, чтобы написать Generic Repository нужно:
Собраться с мыслями.
Спроектировать интерфейс.
Написать реализацию под выбранный в проекте ORM.
Написать реализацию под альтернативные ORM
…
А зачем вообще в таком случае использовать какую бы то ни было ORM? Почему бы просто не использовать запросы на чистом SQL в реализации самого репозитория в таком случае? Не холивара ради пишу, реально не понимаю и хочу понять. Традиционный аргумент о привлечении дополнительного слоя абстракции ради независимости от конкретных СУБД вроде как не в счёт т.к. всё-равно у нас есть пункты «Спроектировать интерфейс» и «Написать реализацию под альтернативные ORM».
Запросы на чистом SQL писать тяжело, а вычитывать из DataReader — нудно и многословно. И все проверки — только в рантайме.
Только не надо добавлять методы с использующие SQL в репозиторий. Это совершенно другая зона ответственности. И назначение другое.
В целом тут на лицо не правильный подход к использованию Репозитория как паттерна. Отсюда и не верное и переусложненное решение.
В этом случае у вас у репозитория будет две отвественности — коллекция объектов и маппинг объектов на СУБД. Другое дело, что для маппинга необязательно использовать универсальные ОРМ, а можно написать свой маппинг с чистым SQL
Это черная магия конечно =) Впрочем, все в соответствии с C# Language Specification [7.11.2], как я уже упомянул в статье.
The operationx && y
is evaluated asT.false(x) ? x : T.&(x, y)
The operationx || y
is evaluated asT.true(x) ? x : T.|(x, y)
Мы хотим, чтобы выполнялась всегда правая часть тернарного оператора. Поэтому и operator true()
, и operator false()
должны возвращать false
.
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) — лишь частный, пускай и частый случай.
Основная цель репозитория в моем понимании — не возможность быстро поменять слой хранения, а очистить бизнес-логику от деталей реализации слоя хранения. Разнести две сложных концепции в разные слои исключительно с односторонней зависимостью.
То есть когда мы пишем Foo(bar)
— это нормально и ООП не нарушает, а как только написали bar.Foo()
— это ой-ой, кошмар и нарушение ООП?
У вас же уже есть слой репозитория в виде контекста. Зачем нужен еще один?
Вы уверены, что это нужно делать именно одним контекстом? У него не будет практически ни одного свойства, которое ожидается от контекста (транзакционность, трансляция LINQ в SQL). По-моему, такое нужно делать как можно более явным, а не инкапсулировать.
Как вы собираетесь делать выборки, если у вас доступна только одна таблица? Да никак, в любых случаях кроме самых тривиальных это просто не работает.
На том же msdn тоже про DbContext указано, что это репозиторий, а вот про DbSet подобного нет.
Т.е. ребята сами нарушают SRP. Океееей.
В данном случае в этом нет ничего плохого — задачи ведь концептуально связаны. Принцип — это не правило, а лишь рекомендация к действию. Нарушать его не просто можно, но во многих случаях — нужно. Хорошего кода без нарушения SRP вы никогда не напишите (если приложение хоть сколько-нибудь сложное).
Например, по Фаулеру, репозиторий — медиатор между доменным слоем и слоем датамаппера, обеспечивающий возможности выборки по критериям.
И где здесь о том, что репозиторий должен содержать все требуемые данные?
Как вы собираетесь делать выборки, если у вас доступна только одна таблица? Да никак, в любых случаях кроме самых тривиальных это просто не работает.
Очевидно же, путем запроса нескольких репозиториев.
DbContext, DbSet и прочий LINQ — это не классический репозиторий, это сущности, поддерживающие описание запросов к источнику данных на .NET совместимом языке. Он обязан иметь доступ ко всем возможным сущностям, т.к. неизвестно, какие будут запросы.
Потому что если он не содержит — то операцию выполнить нельзя.
> Он обязан иметь доступ ко всем возможным сущностям, т.к. неизвестно, какие будут запросы.
Это же верно про репозиторий.
> Очевидно же, путем запроса нескольких репозиториев.
Тогда в чем смысл наличия репозитория? Предполагается, что он должен скрывать какую-то логику за слоем абстракции, ведь так?
Репозиторий не содержит данных обычно, а инкапсулирует логику доступа к хранилищу, реализуя для него абстрактный интерфейс, не зависящий от типа хранилища.
Репозиторий может отвечать как за единственную сущность, так и за несколько. Некоторые практики предполагают создание отдельного репозитория для каждой сущности или агрегата без связей репозиториев друг с другом. Условный "джойн" для сущностях связанных, но не принадлежащих одна другой осуществляется на уровне клиентов репозиториев.
И это не репозитории, потому что в реальности ф-ю репозитория выполнять они не могут. На практике запросы требуют выборок по нескольким сущностям, и если репозиторий не предполагает такой возможности, то он просто не способен инкапсулировать никакой нетривиальной логики. И, значит, не нужен, потому что ничего не делает.
Даже некоторые СУБД не могут выполнять выборки с операциями типа JOIN и дажу WHERE c условием отличным от сравнения с первичным ключом — они не СУБД по вашей логике?
Репозиторий не обязан предоставлять доступ ко всем данных всех хранилищ в любой комбинации. Функция репозитория — инкапсулировать логику хранения, предоставляя публичный интерфейс подобный коллекции.
Он обязан предоставлять такой доступ к данным, чтобы можно было делать требуемые выборки. В _некоторых_ случаях «по репозиторию на сущность» будет достаточно, но в общем случае — нет.
> Функция репозитория — инкапсулировать логику хранения, предоставляя публичный интерфейс подобный коллекции.
Ну вот DbContext эту логику инкапсулирует и интерфейс предоставляет.
А всякие репозитории, которые пишутся поверх него — обычно не предоставляют.
Он обязан предоставлять такой доступ к данным, чтобы можно было делать требуемые выборки.
Он обязан предоставлять ровно такой доступ к данным, который объявлен в его публичном контракте. И как раз в общем случае репозитория на сущность будет достаточно, а в некоторых — нет, прежде всего из-за ограничений по времени и памяти на стороне клиента, когда ему придётся совершать операции над данными из двух репозиториев, которые СУБД могла бы совершить более эффективно.
Мы сейчас говорим именно о том, каким должен быть публичный контракт. Совершенно очевидно, что чтобы что-то было репозиторием, его публичный контракт должен удовлетворять определенным условиям.
> прежде всего из-за ограничений по времени и памяти на стороне клиента, когда ему придётся совершать операции над данными из двух репозиториев, которые СУБД могла бы совершить более эффективно.
Репозиторий за тем и нужен, чтобы сообщить хранилищу: «эй, произведи-ка вот эти операции, и по-эффективнее там!». Если вам приходится данные через репозиторий сперва выгружать, а потом как-то нетривиально обрабатывать (хотя можно было сделать это средствами хранилища), то это и говорит о том, что ваш репозиторий — очень плохой, негодный. И как раз принцип «репозиторий на сущность» к такому и приводит обычно.
Совершенно очевидно, что чтобы что-то было репозиторием, его публичный контракт должен удовлетворять определенным условиям.
Да, очевидно, вернее по определению. Должен представлять данные хранилища для клиента как коллекцию объектов в памяти. Обычно под фразой "коллекция объектов" имеется в виду "коллекция однотипных (с учётом наследования) объектов". Коллекции могут реализовывать сложные условия поиска с участием других коллекций, слабосвязанных с целевой, но это не обязательное требование.
Репозиторий за тем и нужен, чтобы сообщить хранилищу: «эй, произведи-ка вот эти операции, и по-эффективнее там!».
Репозиторий нужен, чтобы развязать хранилище и его клиента. За эффективностью выборки стучитесь в базу напрямую или вообще реализуйте логику на её стороне. Ну и сейчас всё чаще встречаются системы, где у репозитория по факту только единственный метод чтения — получение объекта по идентификатору для последующего изменения и сохранения его состояния. Сложные выборки осуществляются путём проецирования операционного хранилища на другие, оптимизированные под эти сложные выборки, пускай и банальным view.
Подобное развязывание — идея совершенно утопическая. На практике все равно приходится использовать как специфические возможности ОРМ, так и хранилища. И если если для написания любого нетривиального запроса приходится репозиторий бипассить, то тогда зачем он нужен? Разве что во время прототипирования — но, опять же, нафига усложнять написание прототипа лишними слоями абстракции?
EntityFramework: (анти)паттерн Repository