Я не умею готовить репозиторий или он просто не очень?

Очень часто встречаю статьи и вопросы в стиле "лучшие решения/практики для asp.net mvc" и почти везде рассказывается про такой классый паттерн как репозиторий который прям вот вообще очень крут и дает кучу абстракции и дает тестировать что хочешь. Ну вот к примеру первая же ссылка в гугле:
https://www.asp.net/mvc/overview/older-versions/ge...
Я понимаю зачем нужен паттерн я не понимаю другое:
1)
как сделать выборку из списка сущностей.
ну вот к примеру у меня есть таблица Log в которую я пишу все действия юзеров, и там реально ОЧЕНЬ много записей, очень очень много записей. Мне нужно например достать все записи для определенного юзера, или допустим за определенный период. Как мне это сделать?
public IEnumerable<Student> GetLogs()
        {
            return context.Logs.ToList();
        }

Если я использую GetLogs он сразу достанет все записи в память, а если к примеру у меня памяти столько нету? И зачем всё тащить в шарп из базы чтоб сделать выборку если я это могу сделать в базе? Или мне нужно писать под каждую фильтровку свой метод для интерфейса и его реализацию? Типа GetLogsByUserID потом GetLogsByDate и тд до бесконечности

2) Как сделать выборку из двух таблиц? Ну вот хочу достать все логи юзеров с ролью админ с их имейлами к примеру.
без репозитория я сделаю чтот типа:
var logs = (from l in db.Logs
                  join u in db.Users on l.userID equals u.id
                  where u.roleID == 1
                  select new { l , u }).ToList();

Если использовать репозиторий опять же, что мне нужно достать все записи логов потом всех юзеров потом уже делать join? Или мне писать свой метод в интерфейс и реализацию который будет делать только джоинить 2 таблицы? В какой репозиторий users или logs?

Если мне нужен в 1 контроллере/сервисе не 1 репозиторий? мне по 1 их подключать? может мне 15 надо. Или мне спецальный класс завести в которомом сразу будут все репозитории(как dbContext прям)?

Все используют lazy loading? Типа определяют в User коллекцию логов которая при обращении подгрузится? Но если я возьму коллекцию User вместе с логами там же будет подзапрос на каждую строку таблицы. А если мне нужно не только логи? 2-3 таблицы например, будет 2-3 подзапроса на каждую запись в таблице. Вы это используете и вас устраивает?

И главный вопрос. Нужно ли вообще применять этот паттерн если ты не используешь юнит тестирование, либо не используешь его для покрытия 100% всего кода а только отдельные хелперы? Ведь для перехода на другую бд достаточно будет сменить провайдер если используешь ef, и если вы используете орм врядли вы полностью от неё откажетесь. Зачем писать кучу кода "наперед" с принципом "а вот вдруг мы поменяем бд/ос", да это скорей всего уменьшит изменения кода по сравнению с полной переписыванием проекта. Но если ты уверен что ос и бд не поменяется?
Изачем он для юнит тестов если можно спокойно moqнуть весь dbcontext?

Прошу отвечать людей которые реально работают над проектами которыми пользуются. Потому что позицию теоретиков которые начитались таких статей я уже знаю.
  • Вопрос задан
  • 1464 просмотра
Решения вопроса 1
Valeriy1991
@Valeriy1991
Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
Коллеги, добрый день!

Почему никто не упомянул про паттерн UnitOfWork? Паттерн репозиторий становится очень удобным в использовании, если:
  1. это паттерн GenericRepository;
  2. при необходимости можно добавить конкретные репозитории (я их называю ConcreteRepository;
  3. ну и конечно же, если всем этим управляет UnitOfWork;


При использовании такой связки у Вас:
  • если много сущностей и есть GenericRepository, то вам не нужно плодить репозиторий на каждую сущность - достаточно сделать так: var unit = UnitOfWork.Repository<UserLog>();
  • пункт выше автоматически решает проблему "как выбрать данные из 2-3-4-... таблиц" - с репозиториями вы работаете как с DbSet в EF (кстати, сам DbContext из EF, по сути, реализует паттерн UnitOfWork и GenericRepository);
  • автоматически решается вопрос расширения вашего репозитория методами на каждый чих (т.е. надо получить список логов для конкретного пользователя - добавляем новый метод GetLogsByUserId в репозиторий) - не нужно накручивать репозиторий новыми методами, достаточно сделать так: var unit = UnitOfWork.Repository<UserLog>().GetQuery(e => e.UserId == targetUserId) или var unit = UnitOfWork.Repository<UserLog>().AsQuaryableQuery(e => e.UserId == targetUserId) (методы GetQuery и AsQuaryableQuery - это методы у сущности UnitOfWork, которые возвращают IEnumerable или IQueryable соответственно);
  • если методы SaveChanges/Commit реализованы в UnitOfWork (и UnitOfWork управляет транзакциями БД), то у вас решается проблема консистентности данных - UnitOfWork либо подтверждает все изменения (Commit), либо откатывает в случае ошибок (Rollback);
Или мне спецальный класс завести в которомом сразу будут все репозитории(как dbContext прям)?

Правильно мыслите - этот специальный класс и есть UnitOfWork. Он "прям как dbContext", только таким образом вы абстрагируетесь от всяких EF, NHibernate и прочих ORM. В итоге вашей бизнес-логике по барабану, какую ORM вы используете.

Если мне нужен в 1 контроллере/сервисе не 1 репозиторий? мне по 1 их подключать? может мне 15 надо.

Достаточно подключить 1 UnitOfWork (и лучше это делать через внедрение зависимостей - DI - DependencyInjection с использованием IoC-контейнера, например, Autofac, Ninject, Unity. Autofac лично мне нравится больше хотя бы потому, что у него самая адекватная и понятная документация, чем у Ninject и уж тем более Unity [нет, это не тема для холивара "кто лучше: Autofac/Ninject/Unity/... - это всё выводы из моего личного опыта]).

И главный вопрос. Нужно ли вообще применять этот паттерн если ты не используешь юнит тестирование, либо не используешь его для покрытия 100% всего кода а только отдельные хелперы? Ведь для перехода на другую бд достаточно будет сменить провайдер если используешь ef, и если вы используете орм врядли вы полностью от неё откажетесь. Зачем писать кучу кода "наперед" с принципом "а вот вдруг мы поменяем бд/ос", да это скорей всего уменьшит изменения кода по сравнению с полной переписыванием проекта. Но если ты уверен что ос и бд не поменяется?

В любом enterprise-проекте надо думать наперед, даже если это проект "маленький интернет-магазинчик". Потому что через год этот маленький интернет-магазинчик может превратиться в нормальный такой бизнес с очень высокими оборотами и прибылью, и тогда, если у Вас не будет хорошей абстрагированной архитектуры, если у вас не будет модульности, то любая новая задача будет реализовываться со все большим количеством костылей, и любая новая задача будет приводить к неустойчивости системы и повышению степени регресса (проверено!). А вот если вы как архитектор вложите при разработке в вашу систему модульность и абстрагирование компонентов, то вы тем самым уже упростите себе жизнь в будущем, а это означает, что через какое-то время любая задача по изменению системы будет решаться быстрее, надежнее, а значит, вы сможете брать как разработчик за это больше денег (потому что стоимость новых изменений будет гораздо ниже). А если вы еще и юнит-тестирование будете использовать при разработке, то тем самым вы снижаете риск регресса в поведении вашей системы, что опять же, снижает стоимость разработки лично для вас. Поверьте, когда система имеет хотя бы модульность и абстрагирование компонентов друг от друга (т.е. какой-либо компонент зависит от абстракции - в виде интерфейса или абстрактного класса, а не от реализации - в виде конкретного класса), то работа с такой системой будет доставлять удовольствие.

СУБД менять - это не такая распространенная практика, а вот от ORM (и от EF в частности) отказываться в пользу производительности - это реальный кейс с ростом нагрузки.

Толстый Лорри - абсолютно с Вами согласен по этому поводу.

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

Михаил еще также указал в комментарии, в каких случаях репозиторий можно не использовать. Добавлю: в не-enterprise-решениях. Например, вы делаете какой-нибудь простенький сайт для себя. Или для друга. Или просто для изучения новой технологии.
Ответ написан
Пригласить эксперта
Ответы на вопрос 4
@Free_ze
Пишу комментарии в комментарии, а не в ответы
Желательно почитать по поводу работы EF и IQuerable в частности. С помощью этого интерфейса и LINQ вы сможете формировать запросы (которые рендерятся в нативный SQL), а не просто фильтровать коллекции.
private IQuerable<Student> GetLogs()
{
    return context.Logs;
}   
public IEnumerable<Student> GetLogsForStudent(int id, DateTime from, DateTime to)
{
    return GetLogs().Where(x => x.Id == id
                                && x.Date >= from
                                && x.Date <= to)
                    .ToList();
}


Все используют lazy loading?

Зависит от количества данных и критичности по скорости.

Если мне нужен в 1 контроллере/сервисе не 1 репозиторий? мне по 1 их подключать?

Да, по одному. А можно в сервис или UnitOfWork объединять.

Изачем он для юнит тестов если можно спокойно moqнуть весь dbcontext?

dbcontext мокать нет смысла хотя бы потому, что он гвоздями к EF прибит. СУБД менять - это не такая распространенная практика, а вот от ORM (и от EF в частности) отказываться в пользу производительности - это реальный кейс с ростом нагрузки.
Ответ написан
Комментировать
Думаю если небольшой, новый проект в котором все данные лежат в базе, которая создается из EF Code First, то использовать репозиторий особой нужды нет. Но например если сегодня я беру данные из базы, а завтра планирую получать от api или какой нибудь NoSQL к которой нет ORM ? А может сущности, которыми оперирует моё приложение ну совсем не похожи на те что лежат в базе или "собираются" из нескольких источников ?
Не надо рассматривать репозиторий как обертку над EF.

Теперь пара замечаний:

вот так
public IEnumerable<Student> GetLogs()
{
      return context.Logs;
}


будет возвращен IQueryable и

GetLogs().Where(...)

будет ограничивать выборку с сервера.

Я бы с удовольствием посмотрел на страницу, где выводятся ВСЕ логи всех админов.
Но если она все же реально нужна я бы формировал её на клиенте так: получил AJAX запросом всех админов для каждого из них параллельно или последовательно запросил топ 100 мессаджей из логов (при необходимости следующие 100). Как следствие отталкиваться надо от UX тогда не будет желания писать репозиторий, который возвращает ВСЕ записи.

ЗЫ Пока ORM обеспечивает 100% данных и хватает мощности DB сервера все окей и без репозиториев, но при высокой нагрузке архитектура в корне другая и JOINы там выполняют отнюдь не DB сервера.
Ответ написан
Комментировать
@kttotto
пофиг на чем писать
В принципе пост выше ответил, но я тоже добавлю.

Если Вы каждый запрос будете закрывать ToList(), то действительно, на каждый такое действие будет запрос в бд. Если используете LINQ, даже если будет несколько строк кода, сформируется один запрос к бд. И репозитории чаще оборачивают в один сервис, централизованный доступ к данным и работают уже с ним.

Users.Find(id).Logs.Where(х => x.Date == date) или
Logs.Where(x => x.UserId == userId && x.Date == date)

В любом случае будет один запрос.

И под каждый сложный запрос (часто востребованный) добавляют метод в интерфейс.

Надо-не надо, это личное дело, если проектом только один человек занимается. Если его надо поддерживать, развивать, то приходится думать о гибкости. И не всегда меняют провайдера, могут уйти в чистый ado.net, если orm перестает тянуть какие то запросы. Поэтому без репозитория тут проблемно.
Ответ написан
Комментировать
@Sing303
1. Писать свои методы до бесконечности (Таких методов на самом деле будет не много)
2. Тоже писать свой метод, помещать в зависимости от того, из какой таблицы выборка (В вашем случае logs)
3. Подключать по 1, если вам надо 15, то подключать 15 по одному, но тут скорее всего у вас явное нарушение принципа единственной ответственности. Проблем быть не должно, есть IoC контейнеры, фасад, если нужно.
4. Lazy loading используют только там, где не критична производительность, во всех остальных случаях его отключают и получают все данные сразу
5. Этот паттерн в первую очередь применяется для устранения дублирования кода и инкапсуляции, что является базой в ООП. Если нужен "чистый" и гибкий код, нет ничего сложного в реализации и поддержки репозитория, это очень простой паттерн, а если вам кажется сложным, значит вы его не поняли.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы