Суть шаблона подробно описана в статьях P of EAA Catalog: Repository и Domain-Driven Design Community: Repository. Шаблон Repository представляет собой фасад для доступа к данным. Repository реализует механизм для хранения, извлечения и поиска объектов в источнике данных.
Сейчас в большинстве случаев шаблон Repository используется для скрытия конкретной ORM (NHibernate, Entity Framework, Linq2Sql, LLBLGen и т.п.) от остальной части приложения. Наравне с этим он может служить для обёртки любого другого источника данных: сервиса, файловой системы, Active Directory и т.п.
В идеале наша система сохраняет и выбирает доменные объекты из Repository так, как будто это коллекция объектов в памяти. Это позволяет нам полностью игнорировать специфику хранения и выборки объектов.
Проблемы при использовании шаблона Repository
Как любой другой шаблон проектирования, Repository имеет множество интерпретаций и реализаций. Вообще, когда речь идет о проектировании, то сложно отделить абсолютно правильное решение от полностью неправильного. Обычно говорят, что всё зависит от ситуации. С другой стороны, в блогах и статьях попадаются решения, в которых авторы нарушают принципы проектирования или предлагают реализации с явными проблемами. Я собрал такие способы использования шаблона Repository, для того чтобы обратить внимание на возможные проблемы их применения.
Проблема: Repository, как фабрика запросов
Объект IQueryable в .NET дает больше возможностей сделать ошибки в проектировании. К примеру, довольно популярным является такая реализация шаблона Repository:
interface IRepository<TEntity> { IQueryable<TEntity> Query(); // ... } class MyOrmAccountRepository : IRepository<Account> { IQueryable<Account> Query() { // здесь вызов оборачиваемой ORM } // ... }
Похоже, что такая реализация является довольно популярной: Хабр: LINQ to SQL: паттерн Repository, GotDotNet: NHibernate: паттерн Repository, Microsoft Blog: Using Repository Pattern with Entity Framework, MSDN: Persistence Patterns.
К чему может привести передача объекта IQueryable из объекта Repository? Рассмотрим ситуацию выборки пользователей:
var accountRepository = new AccountRepository(); List<Account> activeAccounts = accountRepository.Query() .Where(x => x.IsDeleted == false) .ToList();
Потом ещё где-нибудь в коде:
var accountRepository = new AccountRepository(); var activeAdmins = accountRepository.Query() .Where(x => x.Role == AccountRole.Admin) .Where(x => x.IsDeleted == false) .ToList();
Здесь мы подразумеваем, что в текущих понятиях предметной области активными пользователями являются те, которые не удалены. Причем эти условия x.IsDeleted == false дублируются в разных частях бизнес-логики. Что будет, когда добавится ещё один флаг или состояние у пользователей. Например, флаг IsArchive будет делать пользователей неактивными. Во все выборки пользователей мы должны будем добавить еще одно условие x.IsArchive == false:
var activeAdmins = repository.Query() .Where(x => x.Role == AccountRole.Admin) .Where(x => x.IsDeleted == false && x.IsArchive == false) .ToList();
Теперь понятно, что мы даем пользователям нашего интерфейса IRepository делать запросы какие угодно, где угодно, с дублированием логики. Посмотрим в определение шаблона Repository и станет понятно, что с этой проблемой он был призван бороться: «...adding this layer helps minimize duplicate query logic».
Решение №1
Одним из решений может быть создание специфического метода у объекта Repository, который будет иметь смысловое название. Вот реализация для выборки активных пользователей:
public class AccountRepository { public IEnumerable<Account> GetActiveAccounts() { return ormDataContext.Query<Account>() .Where(x => x.IsDeleted == false && x.IsArchive == false) .ToList(); } }
В такой реализации выборка активных пользователей вне AccountRepository выглядит так:
var accountRepository = new AccountRepository();
var activeAccounts = accountRepository.GetActiveAccount();
Мы локализовали логику определения активного пользователя только в методе GetActiveAccounts. Теперь при добавлении или удалении флагов у объекта Account изменится только метод в AccountRepository.
Решение №2
Можно использовать шаблон Specification вместе c шаблоном Query Object. Второй шаблон уже реализован в .NET с помощью дерева запросов на LINQ.
Реализация для выборки активных пользователей:
public class ActiveAccountSpecification : ISpecification { public Expression<Func<Account, bool>> IsSatisfiedBy() { return x => x.IsDeleted == false && x.IsArchive == false; } } public class AccountRepository { public IEnumerable<Account> Find(ISpecification specification) { return ormDataContext.Query<Account>() .Where(specification.IsSatisfiedBy()) .ToList(); } } // использование спецификации var accountRepository = new AccountRepository(); accountRepository.Find(new ActiveAccountSpecification());
Условия объектов Specification можно накладывать друг на друга. Хороший пример есть в Wikipedia: Specification pattern.
Также, эту тему недавно поднимал menozz в гугл-группе DotNetConf: Многокриториальный поиск и паттерн Specification. Там есть несколько примеров кода.
Проблема: Выход специфики DAL за рамки Repository
Эта проблема относится в основном к ORM, которые генерируют код по существующей структуре БД.
Шаблон Repository в основном используется, как обертка над ORM. Он скрывает специфику построения запросов с помощью ORM, инициализации объектов и т.п. Например, вы получили из AccountRepository объект Account. После этого ставите точку и обращаетесь к сгенерированному полю:
var accountRepository = new AccountRepository(); Account account = accountRepository.GetById(1); // свойство IsChanged сгенерировано ORM if (account.IsChanged) { // ...
Получается, что с помощью Repository мы скрывали источник данных для нашего приложения. В итоге получилось, что используем специфику доступа к данным вне Repository. Более подробно эта проблема описана в статье LLBLGen vs. NHibernate.
Проблема: Один Repository на всех
Попытка создать один универсальный Repository для всех доменных сущностей всегда оборачивается нарушением принципа единственности ответственности и разрастанием этого Repository. Зачастую такие универсальные решения нарушают принцип открытости/закрытости (пример с Repository).
Решение
Нужно разделить реализацию универсального Repository на более маленькие и специфические.
Проблема: Создание Repository для каждого объекта домена
Отдельный объект Repository нужно делать только для корня агрегации, к которому нам нужен непосредственный доступ. Более подробно в статье Domain-Driven Design: aggregation root.
Проблема: Ссылка на Repository из доменного объекта
Ещё одна популярная ошибка, когда в доменном объекте мы можем составлять запросы к БД. Например:
public class Manager { private IEnumerable<customer> customers; public IEnumerable<customer> Customers { get { return customers ?? (customers = new CustomerRepository().Query().Where(x => x.Manager == this).ToList()); } } }
В такое реализации мы наделяем доменный объект Manager дополнительными обязанностями:
- создание CustomerRepository;
- составление запроса для получения всех клиентов данного менеджера.
Это нарушает как минимум принцип единственности ответственности и инверсии зависимости. Как результат получившаяся система будет менее стабильна.
Решение
Очевидно, что создание запроса нужно вынести в ManagerRepository.
Проблема: использование шаблона Repository без шаблона Unit Of Work
Пример:
Account account = accoutRepository.GetById(1);
Product product = new Product();
accoutRepository.Save(product);
account.AddProduct(product);
accoutRepository.Save(account);
Что будет, если запрос на первом Save() выполнится, а на втором нет? Удалять из БД созданный объект product? Этот код демонстрирует то, что мы можем привести БД в несогласованное состояние.
Решение
Шаблон Unit Of Work реализован практически всеми ORM. Работа с данными должна проходить внутри единицы работы. Более подробно с примерами кода описано в Совершенный код №2. Пример реализации Unit Of Work с NHibernate.
Проблема: Связывание Repository и Unit of Work
Один из вариантов связывания Repository с Unit of Work заключается в том, что мы передаем во все объекты Repository ссылку на созданную Unit of Work. Пример реализации в статье Совместное использование Repository с Unit Of Work:
using (IUnitOfWork uow = new MyDataContext()) { ICustomerRepository cr = new EFCustomerRepository(uof); IOrderRepository or = new EFOrderRepository(uof); // CRUD действия с объектами // Завершаем транзакцию. uow.Save(); }
Возникает несколько проблем:
- Каждый Repository получает возможность управлять общей единицей работы. Например, EFCustomerRepository может в любой момент вызвать uow.Save(). Это может привести к ошибкам в логике и нарушает принцип SRP
- Как мы будем создавать ICustomerRepository с помощью инжекции зависимости, если ему в конструктор требуется экземпляр Unit of Work?
- Единица работы может создаваться в контроллере, а Repository в сервисе. Тогда придется передавать ссылку на текущий Unit of Work из контроллера в сервис, из сервиса в Repository
Я бы не рекомендовал напрямую связывать Unit of Work и Repository. Решение проблемы связывания приведено в статье Совершенный код №2. Пример реализации Unit Of Work с NHibernate.
Проблема: Смешивание Repository и Specification
Как я уже говорил, шаблон Repository часто пытаются превратить в нечто универсальное. Вот ещё один пример в статье RepositoryUnit: удобный шаблонный репозиторий.
Всегда нужно стремиться к разделению обязанностей в классах. Чем меньше ваши классы, чем меньше они связаны друг с другом, тем проще вам изменять, тестировать и понимать проект.
Проблема: Смешивание Repository и Unit of Work
Пример ещё одного смешивания. На этот раз Repository и UoW. Пример в статье Entity Framework 4 POCO, Repository and Specification Pattern.
Лучше будет оставлять контроль за транзакцией клиенту объекта Repository, делать этот контроль внешним по отношению к Repository.
Недостаток шаблона Repository
С ростом приложения нам требуется всё больше разных запросов. Объект Repository может вырасти до десятков публичных функций. Нечёткая ответственность делает такой Repository нарушителем SRP.
Ссылки
Domain-Driven Design Community: Repository
Фабрика запросов удобно делается при помощи named scope http://ryandaigle.com/articles/2008/3/24/what-s-new-in-edge-rails-has-finder-functionality . Но тут уже нужен как бы не репозиторий, а свой QueryBuilder для определенных сущностей
ОтветитьУдалитьАлександр, спасибо за статью.
ОтветитьУдалитьА разве проверка активности пользователя - это задача репозитория?
Я выношу подобные проверки или в домен
Account.IsActive()
{
return !this.IsDeleted;
}
или в сервис
AccountService.GetActiveAccounts()
{
return Repository.Query().Where(.Where(x => x.IsDeleted == false);
}
@Константин
ОтветитьУдалитьВ .NET есть аналог? Будет интересно глянуть на код.
@ankstoo
ОтветитьУдалитьВ формате account.IsActive - это доменная логика, но для запроса не подходит.
А если в другом сервисе надо будет выбрать активных пользователей? :) Кажется опять будет дублирование логики.
public class ActiveAccountSpecification : ISpecification
ОтветитьУдалитьВ какой сборке/неймспейсе лежит этот интерфейс? Поверхностное гугление результатов как-то не дало :-(
Плюс - а зачем ты делаешь ToList() в примере со спецификацией?
ОтветитьУдалить@zerkms
ОтветитьУдалить> В какой сборке/неймспейсе лежит этот интерфейс?
Тут нет каких-то специальных ограничений и зависит от того, что будет в реализации спецификации. Если ты будешь использовать в запросах специфику ORM, то в сборке, где используется специфика ORM.
> а зачем ты делаешь ToList() в примере со спецификацией?
Если не сделать ToList, то метод Find будет возвращать IQueryable, а не готовый список.
@Александр Бындю:
ОтветитьУдалитьА, я просто думал это готовый интерфейс в дотнете есть такой.
> Если не сделать ToList, то метод Find будет возвращать IQueryable, а не готовый список.
Угу, спутал IQueryable и IEnumerable ( public IEnumerable).
Как быть с проблемой метода Save в репозитории? Встречал мнение, что это плохая практика. Например, вот http://richarddingwall.name/2009/10/22/repositories-dont-have-save-methods/
ОтветитьУдалить@sombre hombre
ОтветитьУдалитьСпасибо за ссылку.
Здесь Save - это не совсем Save, а скорее Persist.
Вы сами, что думаете по этому вопросу? Предложения?
Честно говоря вся статья - фейерический бред.
ОтветитьУдалить>Решение №1. Одним из решений может быть создание специфического метода у объекта Repository, который будет иметь смысловое название.
То есть под каждую выборку в программе делать метод? А если выборка настраивается пользователем? Тупо фильтры в таблице.
Еще фаулером был описан основной принцип Repository - он получает Query Object на вход и дает материализованную коллекцию на выходе.
>Решение №2.
Тупо не скомпилируется.
Не очень понятно наложение фильтра делать внутри repository и еще оборачивать этот фильтр в объект, кода можно прямо в контроллере наложить этот фильтр на запрос.
Гораздо лучший вариант - сделать generic repository, примерно как тут: http://gandjustas.blogspot.com/2009/01/generic-unity.html
Фильтры вроде IsDeleted == false очень удобно делать Extension-методами для IQueryable. Причем T вполне может быть и интерфейсом, как описывал тут: http://gandjustas.blogspot.com/2010/05/iqueryable-generics.html
>Проблема: Ссылка на Repository из доменного объекта
>Решение:Очевидно, что создание запроса нужно вынести в ManagerRepository.
А если БЛ в классе Manager таки требуется обращаться к customers?
@gandjustas
ОтветитьУдалить> То есть под каждую выборку в программе делать метод?
Нет
> А если выборка настраивается пользователем?
Сделать Specification с параметром или метод Repository с параметром.
> можно прямо в контроллере наложить этот фильтр на запрос
Покажите код, будет интересно.
> Гораздо лучший вариант - сделать generic repository, примерно как тут
Вот как раз такого способа я рекомендую избегать. В статье описана причина.
> Фильтры вроде IsDeleted == false очень удобно делать Extension-методами для IQueryable
В чем удобство?
P.S. Да, я вас помню, вы изобретали свои принципы проектирования http://gandjustas.blogspot.com/2010/11/layered-architecture.html :)
> Здесь Save - это не совсем Save, а скорее Persist.
ОтветитьУдалитьА что, в таком случае, должен делать тот Save, который совсем Save? У меня нет опыта работы с ORM, кроме EF.
> Вы сами, что думаете по этому вопросу? Предложения?
Использовать unit of work?
@sombre hombre
ОтветитьУдалитьДа, хороший вопрос :)
Похоже этот Save(Persit) нужен только для новых объектов, которых еще нет в БД. Изменения объектов, которые мы взяли с помощью ORM, отслеживаются самой ORM.
Получается это скорее AddOrUpdate.
> Использовать unit of work?
Мы используем UoW. В работе это оказалось очень удобно. Вы можете скачать исходный код для NHibernate по ссылке http://alex-byndyu-presentations.googlecode.com/svn/trunk/UnitOfWork%20NHiberante/
>> То есть под каждую выборку в программе делать метод?
ОтветитьУдалить>Нет
А как тогда делать? Для примера параметры сортировки и некоторых фильтров приходят с UI, как делать Repository в данном случае?
>> можно прямо в контроллере наложить этот фильтр на запрос
>Покажите код, будет интересно.
repository.tems().Where(filterET)
>> Гораздо лучший вариант - сделать generic repository, примерно как тут
>Вот как раз такого способа я рекомендую избегать. В статье описана причина.
Написана причина избегать одного репозитария на всех, но не generic репозитария. Кстати generic репозитарий не имеет тенденции к раздуванию и препятствует плохим дизайнерским решением с протаскиванием специфики БЛ в репозитарий.
>> Фильтры вроде IsDeleted == false очень удобно делать Extension-методами для IQueryable
>В чем удобство?
Код выглядит стройнее и нету растаскивания логики по куче классов (repository, specification итп)
Например
var activeAdmins =
accountRepository.Items()
.IsAdmin()
.NotDeleted();
Причем NotDeleted вполне можно сделать generic.
Кстати что насчет БЛ в manager, которая работает со связанной коллекцией Customers&
@gandjustas
ОтветитьУдалить> Для примера параметры сортировки и некоторых фильтров приходят с UI, как делать Repository в данном случае?
Собрать из входящих параметров спецификацию и передать в Repository.
> repository.tems().Where(filterET)
А если подобная логика понадобится в другом месте? Ведь Where(filterET) имеет какую-то смысловую нагрузку.
> Написана причина избегать одного репозитария на всех, но не generic репозитария
У вас проблема в том, что IRepository возвращает объект запроса IQueryable Items(), а не в том, что репозиторий обобщенный. Описано в "Проблема: Repository, как фабрика запросов".
> Причем NotDeleted вполне можно сделать generic
Да, симпатично выглядит.
> А если БЛ в классе Manager таки требуется обращаться к customers?
Если Manager выбран из БД и разрешен LazyLoad, выполнится запрос в пределах UoW. Если LazyLoad запрещен, то будет ошибка.
Если Manager надо коллекцию Customer, то лучше ее загрузить при выборке этого объекта Manager.
>Собрать из входящих параметров спецификацию и передать в Repository.
ОтветитьУдалитьА зачем тут вообще спецификация? Совершенно спокойно можно собрать ET и прицепить к IQueryable, не плодя классы спецификаций и не модифицируя репозитарий.
>А если подобная логика понадобится в другом месте? Ведь Where(filterET) имеет какую-то смысловую нагрузку.
Ну тогда в Extension-метод это вполне можно вынести.
>> Написана причина избегать одного репозитария на всех, но не generic репозитария
>У вас проблема в том, что IRepository возвращает объект запроса IQueryable Items(), а не в том, что репозиторий обобщенный. Описано в "Проблема: Repository, как фабрика запросов".
У меня проблем нет.
>> Причем NotDeleted вполне можно сделать generic
>Да, симпатично выглядит.
И это при том что Repository у меня возвращает IQueryable.
Так что проблем нет.
>> А если БЛ в классе Manager таки требуется обращаться к customers?
>Если Manager выбран из БД и разрешен LazyLoad, выполнится запрос в пределах UoW. Если LazyLoad запрещен, то будет ошибка.
LL часто приводит к SELECT N+1 проблеме.
>Если Manager надо коллекцию Customer, то лучше ее загрузить при выборке этого объекта Manager.
А как тогда тестировать логику, если она зависит от внешних событий - была ли загружена или нет связанная коллекция?
Репозитарий-то легко заменить на Mock, а вот связность по последовательности действий этому не поддается.
Мне кажется в IsSatisfiedBy забыт параметр 'x' и соответственно последующий вызов Where должен выглядеть как Where(specification.IsSatisfiedBy, без скобок. А в этом ключе лучше назваеть ее IsSatisfied, раз параметр у нас implicit.
ОтветитьУдалить@nesteruk
ОтветитьУдалитьСпасибо, Дмитрий! Поправил.
Там еще тип возвращаемого значения не тот)
ОтветитьУдалить@hazzik
ОтветитьУдалитьТочно :) Поправил.
Спецификации - это хорошо, но как их применять к связанным доменным объектам? Вводить по спецификации на каждую связь?
ОтветитьУдалитьВыбрать задачи для всех активных кастомеров например:
TasksRepository.Find(new TaskByCustomerSpecification(new ActiveCustomerSpecification())...
и это всего одна связь в запросе.
>>Если Manager надо коллекцию Customer, то лучше ее загрузить при выборке этого объекта Manager.
Если убирать IQueryable и оставлять ToList в Find, то как потом выбрать Task.Customer.Manager, не получив N+1?
Заводить отдельные медоты FindTasksAndCustomers(), FindTasksAndCustomersAndManagersAndUsersOrderByUserRegistrationDate()?
>> Я бы не рекомендовал напрямую связывать Unit of Work и Repository.
В статье "совершенный код" у UoW и Repository разделены только интерфейсы. Реализация IRepository зависит от внутренних деталей NHibernateUnitOfWork, и наоборот? При этом зависит не явно, а в виде предположений. Это разве не нарушение "Depend upon Abstractions. Do not depend upon concretions"?
Т.е. интерфейсы IRepository и IUnitOfWork и их реализации вроде не связаны, но для их совместной работы нужно выполнить много неявных условий:
- Любая реализация IRepository должена внутри работать с базой исключительно через ISession. Любая попытка использовать mock вместо IRepository поломает взаимодействие с UoW. Что не облегчает тестирование.
- IRepository может вызывать только определенные методы ISession (по аналогии с "Каждый Repository получает возможность управлять общей единицей работы."). Та же возможность вызвать Save/SubmitChanges, то же "нарушение SRP".
- Любой объект с интерфейсом IRepository должен получить общий экзепляр ISession (через ISessionProvider) с текущим объектом IUnitOfWork.
Ну и опять же, SRP. Если реализация IRepository отвечает исключительно за представление данных в виде коллекций объектов, то за что тогда отвечает реализация ISession? Что помешает использовать ISession напрямую (например, если кодить будет тот же человек, что в "проблемах" в "EFCustomerRepository может в любой момент вызвать uow.Save()")?
@Pasha
ОтветитьУдалить> Выбрать задачи для всех активных кастомеров
В чем проблема?
Как вы реализуете тоже самое без спецификаций?
> Если убирать IQueryable и оставлять ToList в Find, то как потом выбрать Task.Customer.Manager, не получив N+1?
Нужно подгружать нужные коллекции сразу при выборке объекта. В NHibernate можно использовать метод Expand(). Разве есть другие способы?
Вообще такая длинная цепочка Task.Customer.Manager уже должна вызвать подозрение, потому что она потенциально будет делать N+1.
> Реализация IRepository зависит от внутренних деталей NHibernateUnitOfWork
Не должна зависеть. Можете показать код, где есть зависимость?
> IRepository может вызывать только определенные методы ISession (по аналогии с "Каждый Repository получает возможность управлять общей единицей работы.")
Да, есть такая возможность. Этому может помочь оборачивание ISession, чтобы не было явно взаимодействия.
Остальные пункты про ISession тоже справедливы. Спасибо, что обратили на это внимание! На самом деле ISession оставили в RepositoryBase только для того, чтобы делать хитрые запросы с помощь Creteria http://ayende.com/Blog/archive/2009/05/19/nhibernate-queries-examples.aspx
Думаю для версии NH3 эту связь можно будет убрать. Сейчас для Repository вы можете просто написать адаптер для ISession и убрать из него функции Save, Flush и т.п.
> Как вы реализуете тоже самое без спецификаций?
ОтветитьУдалитьвозвращаем IQueryable, не не оригинальный, а свой. С добавленными по умолчанию фильтрами.
Например,
Repository.All() - все кастомеры, которых имеет право видеть текущий юзер.
Repository.All() - все активные пользователи.
Repository.All().IncludeDeleted().IgnorePromissions() - выбор всех кастомеров, включая удаленных, даже тех, которых текущий пользователь видеть не должен.
Приложение multi-tenant, так что еще и фильтр по аккаунту привешивается.
При вызове трансформим дерево и собираем IQueryable для оригинального провайдера. Заодно проверяем, что оригинальный провайдер не сделает N+1.
За IQueryable есть один аргумент - из него можно сделать Select, и вытянуть из базы, например, только CustomerId-CustomerName. А со спецификациями такое не пройдет.
> Нужно подгружать нужные коллекции сразу при выборке объекта. В NHibernate можно использовать метод Expand(). Разве есть другие способы?
А в L2S нет метода Expand. Т.е. возможность сделать Expand - это явное раскрытие деталей доступа к данным, причем очень специфическое.
В нашем репозитории поверх l2s (tm) решается .LoadWith(t => t.Customer).LoadWith(c => c.Manager).
>Вообще такая длинная цепочка Task.Customer.Manager уже должна вызвать подозрение, потому что она потенциально будет делать N+1.
Не делает. Обычный join всего на 3 сущности. Таких более чем достаточно в любом приложении.
> Не должна зависеть. Можете показать код, где есть зависимость?
BaseRepository подразумевает, что IUnitOfWork использует тот же объект ISession, и что он использует именно методы для работы с транзакциями именно ISession. Т.е. BaseRepository совместим только с IUnitOfWork, являющимся оберткой над ISession. И наоборот.
Попробуйте написать другую реализацию IRepository, хранящую данные, например, в DataSet в памяти, не затронув при этом реализацию IUnitOfWork. Хотя бы без поддержки linq, с обычным crud. Если получится - значит зависимости нет.
Если получится - значит зависимость не только есть, но еще и неявная, что совсем нехорошо. Потому что переписывая реализацию IRepository разработчик должен как-то узнать, что где-то в другом месте есть такая реализация IUnitOfWork, которую надо тоже соответственно переписать. Лучше уж оставить обычную ссылку на IUoW, и вставлять ее через IoC.
> Да, есть такая возможность. Этому может помочь оборачивание ISession, чтобы не было явно взаимодействия.
Сейчас реализация интерфейсов из статьи - это и есть обертки, реализующие IUoW и IRepository. Добавление еще одного уровня оберток ничего не даст.
@Pasha
ОтветитьУдалить> возвращаем IQueryable, не не оригинальный, а свой
А что мешает сделать так:
Сервис:
query1 = Repository.All()
.IncludeDeleted();
Контроллер:
query2 = query1.Where(x => x.IsDeleted);
Модель:
query2.Where(x => x.IsArchive).ToList();
Получится, что запрос строится на всех уровнях приложения и выполняется в моделе. Не думаю, что хороший вариант локализации запросов.
> Заодно проверяем, что оригинальный провайдер не сделает N+1.
Как вы это проверяете?
> В нашем репозитории поверх l2s (tm) решается .LoadWith(t => t.Customer)
Опять же ваши LoadWith могут быть где угодно в коде. При таком подходе надо, чтобы команда была очень маленькой, а проект небольшим (один-два месяца работы), т.к. инфрастуктура позволяет делать неправильные вещи.
> Не делает. Обычный join всего на 3 сущности. Таких более чем достаточно в любом приложении.
Когда речь идет от небольшой базе в пару гигабайт, то "обычный join всего на 3 сущности" возможно нормальное явление. Лучше все-таки сразу подгружать такие связанные коллекции.
> BaseRepository подразумевает, что IUnitOfWork использует тот же объект ISession
Это так только для конкретных реализаций NHibernateUnitOfWork и BaseRepository (тоже NHibernate). Обе реализации лежат в Infrastructure.NHiberante. Сами интерфейсы не обязывают использовать общую ISession и вообще про ISession ничего не знаю.
А это дает нам возможность написать другой BaseRepository и другую реализацию UoW, связать их по-другому. Все, что надо будет сделать - поменять байдинги IoC на старте приложения.
> Получится, что запрос строится на всех уровнях приложения и выполняется в моделе. Не думаю, что хороший вариант локализации запросов.
ОтветитьУдалитьА по-моему отличный вариант, так как ближе UI имеется больше деталей о запросе, который нужен, чем в репозитарии. Создание "спецификаций" и передача их в репозитарий ничем не лучше, а только хуже, так как делает тоже самое, но более многословно.
> Когда речь идет от небольшой базе в пару гигабайт, то "обычный join всего на 3 сущности" возможно нормальное явление. Лучше все-таки сразу подгружать такие связанные коллекции.
Что значит "лучше"? Запрос с join + индексы в базе + правильная проекция - самый быстрый вариант работы. Если пытаться грузить связанные коллекции для множества сущнсотей то будет именно SELECT N+1
>>делает тоже самое, но более многословно.
ОтветитьУдалитьМожет, и более многословно, но ведь и более формализованно, с уменьшением повторяющейся логики и т. д. Спецификация по сравнению с протаскиванием IQueryable привносит все то, что ценно в большом проекте и не имеет большого значения в малом (по-моему малые проекты - миф :) ).
>Может, и более многословно, но ведь и более формализованно, с уменьшением повторяющейся логики и т. д.
ОтветитьУдалитьСчегобы? Мы можем передавать туда-сюда сам ET (а можем и не передавать и сразу применять) вместо того чтобы передавать туда-сюда спецификации.
>Спецификация по сравнению с протаскиванием IQueryable привносит все то, что ценно в большом проекте и не имеет большого значения в малом.
Это что например? А то звучит как лозунг.
Можете привести ЛЮБОЙ пример со спецификациями поверх IQueryable, его можно будет упростить выкинут классы спецификаций, передавая ET или используя Extension.
@gandjustas
ОтветитьУдалить> А по-моему отличный вариант, так как ближе UI имеется больше деталей о запросе, который нужен, чем в репозитарии
Странная логика, но если она работает в ваших проектах, то оставайтесь с ней.
> Если пытаться грузить связанные коллекции для множества сущнсотей то будет именно SELECT N+1
Связные коллекции можно подгрузить сразу с помощью left join, как раз это делает метод Expand и LoadWith у @Pasha
gandjustas, что есть ET в ваших ответах? На ум приходит только Entity Type, но как-то не вяжется.
ОтветитьУдалить@Александр Бындю
ОтветитьУдалить>> А по-моему отличный вариант, так как ближе UI имеется больше деталей о запросе, который нужен, чем в репозитарии
>Странная логика, но если она работает в ваших проектах, то оставайтесь с ней.
Это правда жизни. О сортировке и фильтрации и проекции PL знает гораздо больше, чем BL или DAL. Надо эти данные как-то из PL в DAL передавать. Query Object именно для этого и был придуман.
@Isda
ОтветитьУдалить>что есть ET в ваших ответах?
Expression Tree вообще-то
@gandjustas
ОтветитьУдалить> Это правда жизни
:)
> А что мешает сделать так:
ОтветитьУдалитьа что мешает сделать так же при использовании спецификаций? Вызываемые методы - это методы IEnumerable. Только запросы будут к L2O, что потенциально медленее. Более того, разработчики именно так и будут делать, протому что так проще. Если не бить постоянно по рукам.
В случае c IQueryable можно тупо запретить возвращать из сервисов сам IQueryable. Например, потребовать в интерфейсах сервисов только POCO/списки POCO. И запретить любой код в моделях. Проконтролировать на code review или инструментами такой вариант гораздо проще. IMHO :)
Цель замены - исключить дублирование того кода, который пишется в 99% запросов. Остальное - привешивается как Extestion methods. Которые, кстати, уже именные (Rob Conery Filters and Pipes)
Момент непосредственной выборки данных в любом случае определяет сервис. IQueryable - это тот же QueryObject, как и ISpecification. Отличается только имя метода для выполнения запроса.
>> Заодно проверяем, что оригинальный провайдер не сделает N+1.
> Как вы это проверяете?
после трансформации нашего IQueryable и перед выполнением берем оригинальный провайдер, вызываем provider.Compile(expression) и проверяем SubQueries.Count. Во время прогонки автоматических тестов, ес-но. В живом окружении - подменяем IQueryExecutor на базовый, без проверки. Минус (или плюс) - в полной реализации около 15-ти интерфейсов, если считать всякие IMappingSourceManager, IInterceptorFactory и поддержку проекций. И разделение ответственности между классами намного строже.
> Опять же ваши LoadWith могут быть где угодно в коде. При таком подходе надо, чтобы команда была очень маленькой, а проект небольшим (один-два месяца работы), т.к. инфрастуктура позволяет делать неправильные вещи.
С какой стати возможность в сервисе указать DAL-у загружаемые связи - это неправильные вещи? IMHO, именно невозможность предугадать результаты вызова метода - это непрозрачность, и как раз совсем неправильная вещь.
Про дропдауны я уже выше написал. Получается, что для заполнения дропдауна будут вытянуты:
1. вообще все данные кастомеров, попавших под спецификацию.
2. все данные managers.
3. данные Manager.User (зачем нам manager-ы без данных пользователей).
4. все managers.Customers (т.е. скорее всего вообще все кастомеры)
Итого - 100-200 строк на мелкого размера базе. Возможно, вы на мелких проектах можете себе такое позволить. У нас, на средних/крупных, отсутствие контроля за загрузкой связей - это практически самоубийство.
> Когда речь идет от небольшой базе в пару гигабайт, то "обычный join всего на 3 сущности" возможно нормальное явление. Лучше все-таки сразу подгружать такие связанные коллекции.
ОтветитьУдалитьПолбазы в память без возможности контроля - это правильная вещь? нуну. И опять же, SRP - не обязанность репозитория принимать решение о загрузке сущностей.
> Это так только для конкретных реализаций NHibernateUnitOfWork и BaseRepository (тоже NHibernate). Обе реализации лежат в Infrastructure.NHiberante.
> Сами интерфейсы не обязывают использовать общую ISession и вообще про ISession ничего не знаю.
Это и называется "нарушение D их SOLID". Когда реализация какого-то куска зависит не от абстакции (интерфейса), а от конктерности (другой реализации, того, где она лежит, сакральных знаний о ее работе). Сами интерфейсы, ес-но, никак D нарушить не могут.
Это так для любых реализаций Repository/UoW, в которых нет ссылки из реализации репозитория на IUow или из реализации UoW на IRepository.
Я не говорю что это плохо, или что реализация плохая (IMHO, отличная), но если это делалось ради того, чтобы запретить случайный вызов Save из репозитория - то нужно было использовать Interface Segregation (из того же solId) - отдавать репозиторию не IUoW, а базовый для IUoW интерфейс, без метода Commit/SubmitChanges.
Кстати, есть еще более стандартные варианты создания/передачи ссылки на IUoW. Например, использования TransactionScope как контейнера, и регистрации в нем IUoW как транзакционного ресурса. Что убирает еще один аргумент против зависимости реализации репозитория от IUoW.
@Idsa
> Спецификация по сравнению с протаскиванием IQueryable привносит все то, что ценно в большом проекте и не имеет большого значения в малом.
И ты, Брут... Возможность написать 10 строк кода ради простой проекции с фильтром и сортировкой? Это действительно ценно?
Спасибо, отличная статья, если рассматривать ее как Q/A. Меня вполне устраивают предложенные в статье решения. Просто немного напрягает перекос агрументов и решений из SOLID в Solid. И перекос примеров в сторону NHibernate. По опыту знаю, что попытка "просто реализовать" точно такие же интерфейсы IUoW/IRepository под L2S/POCO EF выявит кучу проблем. Начиная с утечек соединений в L2S и заканчивая невозможностью использовать одного и того же контекста EF для выборки и удаления POCO. Не говоря уже про то, что при любой реализации разработчие сервисов привяжется к недокументированным особенностям (Identity mapping, Lazy Load, возможности сделать .Lock(), поддержке циклических связей, поддержке синхронизации, разрешению конфликтов...).
PS вылез за лимит в 4к, значит пора прекращать :) еще раз спасибо за статью :)
Мне кажется, из нашего холиварчика можно выделить 3 варианта реализации:
ОтветитьУдалить1. Выполнить запрос в репозитории. При необходимости гибкость достигается засчет спецификаций (позиция Александра)
2. Возвращать из репозиториев IQueryable, а уже в сервисах (я бы назвал их репозиториями второго рода или database-сервисами) выполнять запрос (позиция Павла).
3. Протаскивать IQueryable вплоть до Frontend (позиция gandjustas).
Для меня неприемлема лишь третья позиция: на мой взгляд, это путь в никуда. Хотя надо отдать должное gandjustas: использование методов расширений позволяет минимизировать побочные эффекты этого подхода. Тем не менее, я убежден, что IQueryable не должен доходить до Frontend, и я готов нажать ради этого несколько десятков "лишних" клавиш (это реверанс в сторону спецификаций).
Если сравнивать первые два варианта, то они для меня одинаково хороши. Например, в текущем проекте я начинал с первого варианта, но потом, когда стало появляться очень много непростых запросов (разрулить спецификациями которые было непросто), стал в некоторых местах использовать вариант Павла. Но еще раз отмечу, что при этом во Frontend от сервисов уходит IEnumerable (IList, если быть более точным).
@Pasha
ОтветитьУдалитьСпасибо за ценные замечания.
@Idsa
ОтветитьУдалитьДа, лично для меня дискуссия получилась продуктивной. Всем спасибо.
> Хотя надо отдать должное gandjustas: использование методов расширений позволяет минимизировать побочные эффекты этого подхода
Можно еще лучше сделать этот Fluent запрос. Можно возвращать не IQueryable, а свой IQueryObject, который будет лежать в слое доступа к данным. Ссылку на этот слой не давать тем, кто выше репозитория (на крайний случай сервиса). Получится такая Fluent-спецификация, но более ограниченная, чем простой объект IQueryable.
@Александр,
ОтветитьУдалитьДа, для меня тоже. Спасибо за статью: в моем проекте репозитории как раз переживают кризис и требуют рефакторинга - статья и обсуждение в комментариях оказались очень кстати.
Насчет кастомного IQueryable. Идея мне нравится, только, насколько я понимаю, это улучшение варианта Павла (именно у него я увидел очень ценную для меня мысль ограничения IQueryable в рамках сервиса), а не gandjustas.
Я тоже думал об ограничении области видимости репозитория... но ограничился идеей сделать репозитории internal, а все методы дублировать в сервисах. Сначала мне этот подход не очень понравился (именно протаскиванием методов репозитория), но потом, когда на глазах почти каждый метод репозитория стал требовать сервиса (пейджинг, кэширование и т. д.), идея перестала выглядеть в моих глазах совсем глупой.
Пожалуй, идея с кастомным IQueryable смотрится лучше, потому что репозитории остаются публичными, и мы оставляем за собой право при необходимости использовать их методы (те, которые не возвращают IQueryable) вне сервисов. Правда, придется разбить интерфейс репозитория на IRepository и IQueryableRepository, второй из которых может быть использован извне.
@Idsa
ОтветитьУдалитьНа самом деле в новом проекте мы уже отказались от репозиториев. Но я не мог обойти их стороной, т.к. они "мостик" для понимания следующей парадигмы. Об это в следующий раз :)
@Александр
ОтветитьУдалитьЗаинтриговал :)
@Idsa
ОтветитьУдалитьНичего сверхъестественного - это CQSR
@Isda.
ОтветитьУдалитьЧто есть Frontend? Я IQueryable тащу до контроллера\презентера\viewmodel. Иногда до "слоя сервисов", который кешированием результатов занимается.
>Тем не менее, я убежден, что IQueryable не должен доходить до Frontend, и я готов нажать ради этого несколько десятков "лишних" клавиш (это реверанс в сторону спецификаций).
Все равно не понял цели этого действа... Лишь бы было?
@Александр,
ОтветитьУдалить>Можно возвращать не IQueryable, а свой IQueryObject, который будет лежать в слое доступа к данным.
А в чем преимущество и как будет IQueryObject выглядеть?
>> Что есть Frontend? Я IQueryable тащу до контроллера\презентера\viewmodel. Иногда до "слоя сервисов", который кешированием результатов занимается.
ОтветитьУдалитьКак раз это под Frontend я и имел в виду.
>> Все равно не понял цели этого действа... Лишь бы было?
У меня созрел наводящий вопрос. Правильно ли я понимаю, что этот "финт" с прокидыванием IQueryable в контроллер будет работать только с долгоживущим контекстом (в случае EF) или сессией (в случае nHibernate)? То есть в этом случае я не могу создать контекст, вернуть IQueryable, убить контекст:
using (var ctx = new DbContext())
{
return ctx.Items;
}
, а потом уже выполнить IQueryable? Или же потом я смогу выполнить IQueryable, создав другой контекст/сессию?
@gandjustas
ОтветитьУдалить> А в чем преимущество и как будет IQueryObject выглядеть?
К нему можно будет писать только те запросы, которые определены разработчиком. Т.е. нельзя будет написать:
query
.IsDeleted()
.IsNotArchive()
.Where(x => x.Name == "ok")
.ToList();
Первые два пройдут, а Where нельзя будет делать. Получится что-то типа настройки маппингов в NHibernate. Это позволит избежать дублирования запросов.
В идеале, я бы не стал разрешать использование настойки запроса дальше слоя доступа к данным, тем более в котроллерах и моделе. Кроме того, что логика построения запроса будет находится "везде", я думаю, что это затруднит написание модульных тестов на код и усложнит рефакторинг.
Еще бы добавил к ответу Александра, что подобный подход напоминает времена, когда код доступа к базе данных писался очень близко ко View (например, в codebehind). Конечно, протаскивание IQueryable - не то же самое, что и выполнение голого SQL в контроллере, но, согласитесь, аналогия прослеживается.
ОтветитьУдалить@gandjustas
ОтветитьУдалитьЯ понял чего не хватает :)
Вот у вас есть веб-проект. В нем есть репозиторий.
1) Пришел запрос на клиента
2) Создается контроллер
3) Возможно он создает сервис, который берет из репозитория IQueryable
4) Дальше по вашим словам контроллер тоже берет IQueryable и добавляет в него свои функции
5) Дальше модель, которую передают во View тоже добавляет свои функции.
После всех добавлений мы получаем запрос, который можно назвать, например, "Выборка всех зарегистрированных пользователей, у которых есть оплаченный доступ".
Не знаю, как у вас, а у нас в любом проекте веб-часть является только долей от остального проекта. Т.е. есть еще веб-сервиси, консольные утилиты, win-приложения и т.п. Им тоже нужен доступ к БД и тоже надо сформировать запрос "Выборка всех зарегистрированных пользователей, у которых есть оплаченный доступ".
Но этот запрос ведь уже есть в веб-проекте, правда он размазан по нескольким слоям. Видимо в этом случае придется в каждой отдельной части приложения заново создавать запросы типа "Выборка всех зарегистрированных пользователей, у которых есть оплаченный доступ".
С другой стороны, если бы мы ограничили ответственность по формирования запросов в каком-то отдельном слое (например, в объектах Repository), то не составило бы труда пользоваться их функциями снова и снова в разных частях приложения.
@Александр,
ОтветитьУдалить>К нему можно будет писать только те запросы, которые определены разработчиком.
А в чем профит?
>Это позволит избежать дублирования запросов
А зачем дублировать запросы?
Верхние слои будут обращаться к нижним, получая у них IQueryable с некоторым предикатами. Никакого дублирования не будет. В этом и суть расслоения в бизнес системе.
>В идеале, я бы не стал разрешать использование настойки запроса дальше слоя доступа к данным, тем более в котроллерах и моделе. Кроме того, что логика построения запроса будет находится "везде".
Логика и так находится везде будет. Потому что верхние слои лучше знаю о том какой запрос к базе нужен, в отличие от нижних. Проблема в том как эти знания передать из верхнего слоя в нижний, не создавая при этом связности. Можно плодить "костыли" в виде спецификаций, а можно использовать IQueryable.
ИМХО у своих костылей нету никаких преимуществ перед готовым IQueryable. Обратного пока никому не удалось доказать, кроме того все эти спецификации внутри и будут использовать IQeryable.
>... я думаю, что это затруднит написание модульных тестов на код и усложнит рефакторинг.
Как раз тестирование с IQueryable сильно упрощается. Потому что есть метод AsQueryable и не надо писать нетривиальные моки.
@Isda?
ОтветитьУдалить>Конечно, протаскивание IQueryable - не то же самое, что и выполнение голого SQL в контроллере, но, согласитесь, аналогия прослеживается.
Аналогия прослеживается в том что с IQueryable мы не пытаемся спрятаться от запросов. Не секрет что в любом бизнес-приложении быстродействие упирается чаще всего в работу с БД. Возможности писать запросы с IQueryable, а не магию с политиками загрузки связанных объектов, позволяют писать более эффективный и при этом надежный код.
Огромная разница IQueryable с SQL в том что последний исключительно текстовый и не очень хорошо декомпозируется. Linq же декомпозируется отлично.
@Александр
ОтветитьУдалить>Вот у вас есть веб-проект. В нем есть репозиторий.
>1) Пришел запрос на клиента
>2) Создается контроллер
>3) Возможно он создает сервис, который берет из >репозитория IQueryable
>4) Дальше по вашим словам контроллер тоже берет >IQueryable и добавляет в него свои функции
>5) Дальше модель, которую передают во View тоже >добавляет свои функции.
Как-то слишком круто...
Обычно происходит так:
1)Котроллер обращается к сервису БЛ: "дай мне всех online-пользователей" (это метод класса БЛ)
2)БЛ обращается к репозитарию: "дай мне всех пользователей" (это метод обобщенного класса Repository)
3)БЛ на полученный IQueryable накладывает предикат, который получается только Online пользователей и возвращает в контроллер
4)Контроллер накладывает свои предикаты при необходимости (например по имени) и проекцию, выбирает только логин и id пользователя например
5)Контроллер материализует результат запроса, формирует объект View Model и передает на вьюху для генерации результата.
>Не знаю, как у вас, а у нас в любом проекте веб-часть является только долей от остального проекта. Т.е. есть еще веб-сервиси, консольные утилиты, win-приложения и т.п. Им тоже нужен доступ к БД и тоже надо сформировать запрос "Выборка всех зарегистрированных пользователей, у которых есть оплаченный доступ".
Они все шарят BL, меняется только PL.
BL при этом отлично тестируется.
@Idsa,
ОтветитьУдалить>У меня созрел наводящий вопрос. Правильно ли я >понимаю, что этот "финт" с прокидыванием >IQueryable в контроллер будет работать только с >долгоживущим контекстом (в случае EF) или сессией >(в случае nHibernate)?
Что значит "долгоживущим"? Более одного скоупа одного метода - да, но разницы нет, потому что до запроса соединение не открывается.
У меня обычно один конекст на запрос в случае веба. В декстопе it depends.
@gandjustas
ОтветитьУдалить> 4)Контроллер накладывает свои предикаты при необходимости (например по имени) и проекцию, выбирает только логин и id пользователя например
Как эту логику выборки использовать в других частях системы?
> 5)Контроллер материализует результат запроса, формирует объект View Model и передает на вьюху для генерации результата.
Это значит, что в контроллере делается ToList()?
gandjustas переживает за УИ пользователей. И доля правды в его словах есть. Во многих системах нехватает просто получить активных пользователей, их еще надо фильтровать, сортировать, группировать, еще желательно делать проекции чтобы не грузить все данные обьекта. Все это делает пользователь. И все должно быть очень гибко. Более того, оно все очень датацентрик и идентично для каждой сущьности. Короче IQueryable для чтения/кверинья просто идеален.
ОтветитьУдалитьЧерез репозитрарий IQueryable пропихивать достаточно глупо. Хош не хош, а грузится весь обьект. Селект+Н к месту и не к место и так далее. Еще и ЮнитОфВорк не к месту, тож немного производительности тратит. Это всетаки чтение.
Короче, лично у нас для чтения/кверинья с УИ пользуется IQueryable со всеми плюшками, а для бизнес логики (читай изменения) Репозитарий без всяких заворотов типа кверенья. Такой себе CQS.
П.С. gandjustas как всегда на страже анемистов ;)
@Pasha
ОтветитьУдалить>> Это так только для конкретных реализаций NHibernateUnitOfWork и BaseRepository (тоже NHibernate). Обе реализации лежат в Infrastructure.NHiberante.
>> Сами интерфейсы не обязывают использовать общую ISession и вообще про ISession ничего не знаю.
>Это и называется "нарушение D их SOLID". Когда реализация какого-то куска зависит не от абстакции (интерфейса), а от конктерности (другой реализации, того, где она лежит, сакральных знаний о ее работе). Сами интерфейсы, ес-но, никак D нарушить не могут.
Вы что-нибудь о coheison/coupling слышали?
Внутри модуля должна быть высокая связность их компонент (coheison), а между модулями должна быть слабая связанность (coupling). Поэтому очевидно, что, NHUnitOfWork и NHRepository должны использоваться вместе и только вместе, должны быть объявлены в одном модуле. По вашей же логике, какой-нибудь криворукий джуниор будет пытаться использовать NHRepository+EFUnitOfWork - и как вы себе это представляете?
Какой смысл мочить отдельно Repository и отдельно UnitOfWork? Так, извините не бывает.
Еще, вы наверное не слышали про принципы "уменьшай неопределенность" и "абстрагируйтесь от низкоуровневых API", в данном случае IQueryable - это слишком абстрактный низкоуровневый API.
В нагрузку - нужно писать код так, чтобы у его пользователей было как можно меньше шансов сделать что-либо неправильно.
ОтветитьУдалитьСпасибо за классную статью и вообще за замечательный блог!
ОтветитьУдалитьПожелание не по теме:
Ваш блог был бы еще более замечательным, если бы Вы чуть больше ценили время читателей, фильтруя бред некоторых комментаторов. Яркий пример - gandjustas...
Не знаю как Вам, но вступать в дискуссию с человеком, чей ответ не содержит ни капли смысла и к тому же начинается со слов типа 'Феерический бред' по-моему обломно. Тем более вы уже раньше были знакомы.
Извините за оффтоп...
baser, я не Александр, и не мне принимать решение, но у gandjustas просто такой стиль общения. И у него бывают очень даже неплохие идеи.
ОтветитьУдалить@barser
ОтветитьУдалить> Спасибо за классную статью и вообще за замечательный блог!
Спасибо!
> вступать в дискуссию с человеком, чей ответ не содержит ни капли смысла и к тому же начинается со слов типа 'Феерический бред' по-моему обломно
Его идеи могут разделять те, кто очень плохо знаком с темой, поэтому лучше на них развернуто отвечать.
Тема уже "обсалывалась" у товарища Ayende: http://ayende.com/Blog/archive/2009/04/17/repository-is-the-new-singleton.aspx
ОтветитьУдалитьи вот
http://ayende.com/Blog/archive/2007/03/09/Querying-is-a-business-concern.aspx
Я согласен с ним во всём и с gandjustas в том, что дополнительные абстракции над тем, как формировать спецификации для IQueryable ничего не дают.
В целом считаю наиболее разумным подходом это прямое использование репозитория для очень простых запросов. Для сложных же предпочитаю, как рекомендует Аенде, создавать отдельные классы, представляющие из себя этот самый запрос, критерий, спецификацию для поиска (называют их по разному).
Таким образом решается проблема "реюза" этих запросов в разных частях системы.
Ну и в довесок, подтверждая эти идеи - цитата от самого великого "Client objects construct query specifications declaratively and submit them to Repository for satisfaction."
Это и даёт нам подобную реализацию:
Repository.Find(userByName);
а недавно, прочитал замечательный пост
http://fragmental.tw/2010/12/23/how-to-write-a-repository/
что тоже интересно.
Опа, а куда делся мой комментарий?
ОтветитьУдалить@Александр
ОтветитьУдалить>> 4)Контроллер накладывает свои предикаты при необходимости (например по имени) и проекцию, выбирает только логин и id пользователя например
>Как эту логику выборки использовать в других частях системы?
Функции, которые принимают IQueryable и возвращают IQueryable, могут быть экстеншенами, могут быть методами классов БЛ\Service Layer.
>> 5)Контроллер материализует результат запроса, формирует объект View Model и передает на вьюху для генерации результата.
>Это значит, что в контроллере делается ToList()?
Чаще всего именно так.
@hazzik,
ОтветитьУдалитьКакой смысл мочить отдельно Repository и отдельно UnitOfWork? Так, извините не бывает
Смотрю я на DataSet и DataAdapter и вижу что бывает.
Это просто почти все ORM прибивают гвоздями UoW к контексту (по сути соединению с БД), хотя это делать совершенно необязательно.
Вот EF встал на путь истинный - у него есть SelfTracking Entities, но там UoW прибит к данным. А хорошо бы иметь отдельно объекты, соединение и UoW.
@hazzik,
ОтветитьУдалить>Вы что-нибудь о coheison/coupling слышали...
>Еще, вы наверное не слышали про принципы "уменьшай неопределенность" и "абстрагируйтесь от низкоуровневых API"
Не надо повторять лозунги, надо понимать к чему приводит выполнение тех или иных принципов, желательно формализованно, в цифрах.
Например Low Copuling я понимаю, действительно чем ниже свзяность, тем проще тестировать, тем проще найти ошибку и не допустить новой.
А вот про High Cohesion непонятно, даже однозначного перевода нет. Чаще всего это понимается как "согласованность". То есть некоторый модуль выполняет согласованные (то есть похожие, однотипные) задачи.
Что касается неопределенности и низкоуровневости АПИ, то тут вообще можно до бесконечности копья ломать.
Однако практика - критерий истины. Напишите код, который оборачивает IQueryable, чтобы он был удобнее IQueryable.
Кстати у вас вряд ли выйдет. Потому что IQueryable - монада и с ней удобно работать пока находишься в монаде (методы Select, SelectMany, Where оставляют на в монаде). А как только выходишь, то появляются сложности с протаскиванием контекста, который автоматом протаскивается в самой монаде.
Ну или вам придется создать еще одну монаду, только чем это будет лучше IQueryable - непонятно.
Саша, ты можешь восстановить мой коммент?
ОтветитьУдалить>Проблема: использование шаблона Repository без шаблона Unit Of Work
ОтветитьУдалитьМне кажется это как раз его прямое предназначение. Сам паттерн Repository придуман исключительно для работы с отсоединенными сущностями, что возможно только для простых графов и в таком виде уже никому не нужен. Поэтому никакие серьезные orm(Session, DataContext...) его не поддерживают, а другие серьезные frameworkи пытаются его скрестить с unit of work. В unit of work есть методы "MarkAsCreated" и "MarkAsRemoved" и соответственно метод типа "вот вам квери билдер на T" или "давайте я выполню ваш query object от T".
Репозиторий получил популярность за простоту "хлоп-хлоп": stored procedure arghh.. метод c n-параметрами готов, хороший статический класс n-методами, а как же lazy-loading и "все или ничего", тестируемость там и т.п., черт надо его к коннекту/транзак. привязать, ой к unit of work... так static убираем.. От лукавого, кто это придумал, где такое писано??? ))
Т.е. получается, что репозиторий это на самом деле набор простых предопределённых query - object' ов-методов, которые должны выдаваться unit of work'м, и способных лишь параметризовываться длинной простыней параметров(аллилуйя С# 4.0) и может быть контекстом. А каким образом отдавать query builder или в каком принимать query object, как IQueryable(LINQ тъфу на него, как запросить по protected полю, например, или как объяснить человеку, которые пишет UI- что нельзя использовать в предикатах where регулярные выражения, да у меня еще тут вот этот флаг на базу не замаплен, сорри, смотри маппинг, плиз, когда пишешь запросы) или что-то другое более рассчитанное на доступ к сущностям в базе(данным), а так же наборы этих методов(из репозиториев), решать, мне кажется, должен интерфейс над единицей работы.
А вообще, Майк прав, мне кажется, если не пытаться разделять модели чтения(ui forms/reports/data export/odata whatever) и записи(доменной модели), то спорить бесполезно, в этом случае все уже зависит от типа, масштаба проекта конечно и его евангелистов.
Все сказанное выше мой личный феерический бред, т.е. ИМХО.
Да и еще давно хотел сказать, заменять ORM(и хотеть иметь такую возможность) на середине проекта - точно феерический бред.
>>Вот EF встал на путь истинный - у него есть SelfTracking Entities, но там UoW прибит к данным. А хорошо бы иметь отдельно объекты, соединение и UoW.
ОтветитьУдалитьХоть сам и использую в текущем проекте Self-tracking entities, назвать их истинным путем язык не поворачивается. Они довольно прикольные тем, что все внутри себя трекят и делают операцию Save/AddOrUpdate сказочно простой. Но есть два косяка:
1. Концептуальный: Они не POCO, совсем не POCO. Думаю, нет смысла описывать следствия
2. Технический: они реализованы не самым лучшим образом. Во-первых, заточены под вокрфлоу WCF, и нормально трекятся только после десериализации (есть воркэраунд). Во-вторых, не сохраняются original values (что очень неудобно для логирования) - есть воркэраунд, но он помогает не во всех случаях.
Ну и на msdn да на stackoverflow советуют использовать POCO, а к STE прибегать только при использовании WCF. Да и сами разработчики EF не позиционируют его как "путь истинный"
@Restuta
ОтветитьУдалитьИногда комментарии появляются с опозданием. Не знаю с чем связано.
Комментарий от @Restuta:
Тема уже "обсалывалась" у товарища Ayende: http://ayende.com/Blog/archive/2009/04/17/repository-is-the-new-singleton.aspx
и вот
http://ayende.com/Blog/archive/2007/03/09/Querying-is-a-business-concern.aspx
Я согласен с ним во всём и с gandjustas в том, что дополнительные абстракции над тем, как формировать спецификации для IQueryable ничего не дают.
В целом считаю наиболее разумным подходом это прямое использование репозитория для очень простых запросов. Для сложных же предпочитаю, как рекомендует Аенде, создавать отдельные классы, представляющие из себя этот самый запрос, критерий, спецификацию для поиска (называют их по разному).
Таким образом решается проблема "реюза" этих запросов в разных частях системы.
Ну и в довесок, подтверждая эти идеи - цитата от самого великого "Client objects construct query specifications declaratively and submit them to Repository for satisfaction."
Это и даёт нам подобную реализацию:
Repository.Find(userByName);
а недавно, прочитал замечательный пост
http://fragmental.tw/2010/12/23/how-to-write-a-repository/
что тоже интересно.
@gandjustas
ОтветитьУдалитьКонечно, вместо того, что б разобраться к чему это все, легче обвинить в выкрикивании лозунгов. Я кричу только те лозунги, которые спасали мне жизнь не один раз.
Про high cohesion - вообще какая разница как оно переводиться? Я к тому же написал оригинальное английское название. Обычно под high cohesion понимают высокую *функциональную* связность.
http://ru.wikipedia.org/wiki/GRASP (на англ: http://en.wikipedia.org/wiki/GRASP_(object-oriented_design))
А тут http://en.wikipedia.org/wiki/Cohesion_(computer_science) можно прочитать какая бывает связность, какая из них лучше и почему должно быть.
>Проблема: использование шаблона Repository без шаблона Unit Of Work
ОтветитьУдалитьМне кажется это как раз его прямое предназначение. Сам паттерн Repository придуман исключительно для работы с отсоединенными сущностями, что возможно только для простых графов и в таком виде уже никому не нужен. Поэтому никакие серьезные orm(Session, DataContext...) его не поддерживают, а другие серьезные frameworkи пытаются его скрестить с unit of work. В unit of work есть методы "MarkAsCreated" и "MarkAsRemoved" и соответственно метод типа "вот вам квери билдер на T" или "давайте я выполню ваш query object от T".
Репозиторий получил популярность за простоту "хлоп-хлоп": stored procedure arghh.. метод c n-параметрами готов, хороший статический класс n-методами, а как же lazy-loading и "все или ничего", тестируемость там и т.п., черт надо его к коннекту/транзак. привязать, ой к unit of work... так static убираем.. От лукавого, кто это придумал, где такое писано??? ))
Т.е. получается, что репозиторий это на самом деле набор простых предопределённых query - object' ов-методов, которые должны выдаваться unit of work'м, и способных лишь параметризовываться длинной простыней параметров(аллилуйя С# 4.0) и может быть контекстом. А каким образом отдавать query builder или в каком принимать query object, как IQueryable(LINQ тъфу на него, как запросить по protected полю, например, или как объяснить человеку, которые пишет UI- что нельзя использовать в предикатах where регулярные выражения, да у меня еще тут вот этот флаг на базу не замаплен, сорри, смотри маппинг, плиз, когда пишешь запросы) или что-то другое более рассчитанное на доступ к сущностям в базе(данным), а так же наборы этих методов(из репозиториев), решать, мне кажется, должен интерфейс над единицей работы.
А вообще, Майк прав, мне кажется, если не пытаться разделять модели чтения(ui forms/reports/data export/odata whatever) и записи(доменной модели), то спорить бесполезно, в этом случае все уже зависит от типа, масштаба проекта конечно и его евангелистов.
Все сказанное выше мой личный феерический бред, т.е. ИМХО.
Да и еще хотел сказать: заменять ORM(и хотеть закладывать такую возможность) на середине проекта - точно феерический бред.
@Александр
ОтветитьУдалить>Ну и в довесок, подтверждая эти идеи - цитата от самого великого "Client objects construct query specifications declaratively and submit them to Repository for satisfaction."
>Это и даёт нам подобную реализацию:
>Repository.Find(userByName);
Только вот проблема как получать этот userByName. Ведь реально будет userByNameWithPojectionPagingAndSorting. Причем слой PL знает какой нужен Paging, Sorting и Projection, а BL - каких пользователей надо выбирать.
Короче как это дело удобно композировать между собой.
>Проблема: использование шаблона Repository без шаблона Unit Of Work
ОтветитьУдалитьМне кажется это как раз его прямое предназначение. Сам паттерн Repository придуман исключительно для работы с отсоединенными сущностями, что возможно только для простых графов и в таком виде уже никому не нужен. Поэтому никакие серьезные orm(Session, DataContext...) его не поддерживают, а другие серьезные frameworkи пытаются его скрестить с unit of work. В unit of work есть методы "MarkAsCreated" и "MarkAsRemoved" и соответственно метод типа "вот вам квери билдер на T" или "давайте я выполню ваш query object от T".
Репозиторий получил популярность за простоту "хлоп-хлоп": stored procedure arghh.. метод c n-параметрами готов, хороший статический класс n-методами, а как же lazy-loading и "все или ничего", тестируемость там и т.п., черт надо его к коннекту/транзак. привязать, ой к unit of work... так static убираем.. От лукавого, кто это придумал, где такое писано??? ))
Т.е. получается, что репозиторий это на самом деле набор простых предопределённых query - object' ов-методов, которые должны выдаваться unit of work'м, и способных лишь параметризовываться длинной простыней параметров(аллилуйя С# 4.0) и может быть контекстом. А каким образом отдавать query builder или в каком принимать query object, как IQueryable(LINQ тъфу на него, как запросить по protected полю, например, или как объяснить человеку, которые пишет UI- что нельзя использовать в предикатах where регулярные выражения, да у меня еще тут вот этот флаг на базу не замаплен, сорри, смотри маппинг, плиз, когда пишешь запросы) или что-то другое более рассчитанное на доступ к сущностям в базе(данным), а так же наборы этих методов(из репозиториев), решать, мне кажется, должен интерфейс над единицей работы.
ОтветитьУдалитьА вообще, Майк прав, мне кажется, если не пытаться разделять модели чтения(ui forms/reports/data export/odata whatever) и записи(доменной модели), то спорить бесполезно, в этом случае все уже зависит от типа, масштаба проекта конечно и его евангелистов.
Да и еще давно хотел сказать, заменять ORM(и хотеть иметь такую возможность) на середине проекта - феерический бред (c) you know who.
Т.е. получается, что репозиторий это на самом деле набор простых предопределённых query - object' ов-методов, которые должны выдаваться unit of work'м, и способных лишь параметризовываться длинной простыней параметров(аллилуйя С# 4.0) и может быть контекстом. А каким образом отдавать query builder или в каком принимать query object, как IQueryable(LINQ тъфу на него, как запросить по protected полю, например, или как объяснить человеку, которые пишет UI- что нельзя использовать в предикатах where регулярные выражения, да у меня еще тут вот этот флаг на базу не замаплен, сорри, смотри маппинг, плиз, когда пишешь запросы) или что-то другое более рассчитанное на доступ к сущностям в базе(данным), а так же наборы этих методов(из репозиториев), решать, мне кажется, должен интерфейс над единицей работы.
ОтветитьУдалитьА вообще, Майк прав, мне кажется, если не пытаться разделять модели чтения(ui forms/reports/data export/odata whatever) и записи(доменной модели), то спорить бесполезно, в этом случае все уже зависит от типа, масштаба проекта конечно и его евангелистов.
ОтветитьУдалитьДа и еще давно хотел сказать, заменять ORM(и хотеть иметь такую возможность) на середине проекта - феерический бред (c) you know who.
Сорри, все за раз, не получается опубликовать.
@Nikita Govorov
ОтветитьУдалить> Да и еще давно хотел сказать, заменять ORM(и хотеть иметь такую возможность) на середине проекта - феерический бред (c) you know who
Мы год назад это сделали. Для проекта была выбрана ORM LLBLGen. Проект начал активно развиваться и через год стало очевидно, что эта ORM не справляется. Пришлось переходить на другую ORM. Так что нет ничего постоянного.
@Александр
ОтветитьУдалитьОк, неправильно выразился, абстрагироваться от самого же орм в слое доступа к данным, аргументируя это тем, что в любой момент можно быстро ее заменить. Возможно, я читал такое утверждение не в вашем блоге.
Да, так же забыл поблагодарить Вас за статью, пусть ваше понимание Repository c моей точки зрения спорное, но так как, оно такое у многих в .net-community, то это достаточно полезный контент для рунета(тем более с комментариями). Спасибо.
ОтветитьУдалитьПротаскивание IQueryable через все слои и наращивание его функциями LINQ, тоже самое, что передача и наращивание строки SQL-запроса. В обоих случаях доступ к источнику данных не локализован.
ОтветитьУдалитьСогласен, выставление IQueryable у unit of work неправильно. Опять же надо сказать it depends, я стараюсь так не делать. Однако признаюсь есть такие методы, в заброшенной dll(о которых мало кто знает): [EditorBrowsable(EditorBrowsableState.Never)]
ОтветитьУдалитьpublic static IQueryable AllOfEx(this UnitOfWork unit) where TEntity : IQueryableEntity
{
return Cast(unit).Query();
}
private static ISession Cast(INativeUnitOf unitOf)
{
return unitOf.Native as ISession;
}
Выставление типа-IQueryable в reading модели, например через odata - совершенно нормальное явления.
@Nikita Govorov
ОтветитьУдалить"совершенно нормальное явление", которое может привести к очень неприятным последствиям :)
Ладно, я понял вашу точку зрения. Спасибо!
Этот комментарий был удален автором.
ОтветитьУдалитьУ товарища Ayende в конце одного поста была замечательная ссылка:
ОтветитьУдалитьhttp://davybrion.com/blog/2009/04/educate-developers-instead-of-protecting-them/
«Спор» с gandjustas очень похож на это противоречие: 1. (protect) предусмотреть все неправильные ходы своих программистов, чтобы даже неопытный человек не смог сделать ничего «неправильного», либо 2. допустить небольшой «шаг влево, шаг вправо» без обязательного расстрела, чтобы более дисциплинированные и продвинутые (результат educate) не были скованы требованиями и предписаниями, которые на 99% выполняются и без этого, а остальной 1% может быть очень даже полезным с точки зрения развития и появления новых идей.
О, а мне так лень было искать эту ссылку. Полностью поддерживаю Алексея Волкова.
ОтветитьУдалить@Алексей Волков @Restuta
ОтветитьУдалитьКак это связано с проектированием доступа к данным?
@Александр,
ОтветитьУдалить>Протаскивание IQueryable через все слои и наращивание его функциями LINQ, тоже самое, что передача и наращивание строки SQL-запроса. В обоих случаях доступ к источнику данных не локализован.
И че? Разве это насколько плохо что надо ухудшать быстродействие и писать больше кода?
@hazzik:
ОтветитьУдалить> Какой смысл мочить отдельно Repository и отдельно UnitOfWork? Так, извините не бывает.
Совершенно верно, никакого. И ссылаться на UoW из репозитория (или наоборот) можно вполне спокойно. Заменить обязательную неявную связь на явную ссылку. Именно это я и пытался донести до автора поста. Я понимаю, что мои посты вы читали наискосок. Краткое содержание предыдущих серий:
- я: в статье написано что связвать нельзя, а в примере от автора - неяваная связь есть. почему бы не связать явно.
- автор: есть, но неявная связь лучше!
- я: нет, хуже, но черт с ней.
- вы, менторским тоном: паша, связь это нормально! и лучше, чем без связи! читай википедию!
> Еще, вы наверное не слышали про принципы "уменьшай неопределенность" и "абстрагируйтесь от низкоуровневых API", в данном случае IQueryable - это слишком абстрактный низкоуровневый API.
Как понимать "слишком абстрактный"? :) Я вот в ответ напишу "наборот, он абстрактный и высокоуровневый!". Очень полезное обсуждение получится.
@Александр
>Как это связано с проектированием доступа к данным?
самым прямым образом. пару проектов назад пришел к нам свежий разработчик, добавил в UI ссылку на System.Data и нафигачил SQL напрямую в Page_Load. да, студия умеет ловить и такие ситуации, но проще и выгодее обучить человека - рассказать ему о секрете "запрос выполняется когда вызывашь ToList(), думай когда пишешь код", чем жертвовать гибкостью и производительностью системы. От ситуации "у нас нельзя выбрать список имен кастомеров, только кастомеров целиком" до "у нас ORM не справляется" один шаг. ;)
Нужно срочно прикручивать плюсование к комментам =)
ОтветитьУдалить@Александр Бындю
ОтветитьУдалитьСвязано напрямую, т.к. зачастую слой доступа к данным, да и другие слои, проектируются превентивно. Чтобы менее талантливые разработчики не наделали ерунды. Это в свою очередь приводит к дополнительному уровню абстракции "защита от дурака", который никак не способствует гибкости проектируемого "слоя".
@Restuta
ОтветитьУдалить> Чтобы менее талантливые разработчики не наделали ерунды
Мы локализуем формирование запросов в одном месте, чтобы не было дублирования. Вот единственная причина использовать Repository. И эта же причина, чтобы не гонять IQueryable по всему приложению.
@Pasha
ОтветитьУдалить>*"у нас нельзя выбрать список имен кастомеров, только кастомеров целиком"* до "у нас ORM не справляется" один шаг. ;)
В статье про это ни слова. Выбирать можно хоть что, но главное инкапсулировать это в какую-нибудь изолированную сущность, например в запрос, обязательно спрятанный за интерфейсом: абстрактным (IQuery) или конкретным (ICustomersRepository.GetCustomerNames)
>в статье написано что связвать нельзя, а в примере от автора - неяваная связь есть. почему бы не связать явно.
В статье описано, что нужно абстрагироваться от апи ОРМ, и не более. Но также тут написано, что при этом возникает проблема - как связать UoW и рапозиторий. Решается эта проблема через неявную связь.
@hazzik
ОтветитьУдалить> и не более
и более:
>>Я бы не рекомендовал напрямую связывать Unit of Work и Repository
а я бы рекомендовал.
> Решается эта проблема через неявную связь.
Капитан, не надо мне объяснять содержимое статьи.
Я прекрасно понимаю, как это проблема решается в статье. Ее можно было решить через явную связь. Неявность - непрозрачность и зло, IMHO.
> В статье про это ни слова. Выбирать можно хоть что, но главное инкапсулировать это в какую-нибудь изолированную сущность, например в запрос, обязательно спрятанный за интерфейсом: абстрактным (IQuery) или конкретным (ICustomersRepository.GetCustomerNames)
Что должен возвращать этот GetCustomerNames? Список чего? Главное инкапсулировать, обязательно спрятать, не дать поменять. А вы задавали себе вопрос - зачем? Какую такую мегапроблему эта "инкапсуляция" решит? Гарантирую, из результата .Select(c=>c.Name) даже самый тупой разработчик ничего, кроме имен, не выберет. Инкапсулировано по самое небалуйся.
Кто это должен добавлять GetCustomerNames? Тот самый разработчик, которого нужно ограждать от сложностей и ошибок? Да он ни в жизнь не полезет ниже BL. Конкретный пример - у нас запрос на выборку иерархического списка данных задач (со всеми колонками) был успешно "повторно использован" для заполнения почти всех выпадающих списков в системе. Потому что использовать существующий запрос в вашем подходе легче (O(1)), чем залезть в репозиторий и дописать там еще один GetXXX с поддержкой всех спецификаций (O(n)). А лень - одно из достоинств разработчиков.
@Alexandr
@hazzik
> Мы локализуем формирование запросов в одном месте, чтобы не было дублирования
Репозиторий не предназначен для "изоляции от деталей доступа к данным" - эта фраза относится к DataMapper в описании паттерна репозиторий. И не предназначен для составления запросов.
Репозиторий:
- представляет данные в виде массивов объектов.
- позволяет выполнять над ними запросы. Не фиксированные запросы, а спецификации запросов в каком-то виде.
- позволяет добавлять/удалять объекты в эти виртуальные массивы
Формирование запросов к массивам объектов - это ответственность уровня BL. Вот, дословно, из описания паттерна:
Client objects construct query specifications declaratively and submit them to Repository for satisfaction.
Как только вы начинаете писать методы GetCustomerByName() - вы реализуете уже не репозиторий, а какую-то "библиотеку запросов".
Любой провайдер XXXContext в EF/L2S:
- представляет данные в виде массивов объектов
- позволяет выполнять над ними запросы. запросы специфицируются в виде Expressions.
- позволяет добавлять/удалять объекты
Единственное, ради чего стоит оборачивать готовую реализацию репозитория (провайдер/ISession) в свою собственную - это улушение поддержки IOC/DI.
Да, в Фаулере нет паттерна для IQueryable - так книжке уже 9 лет. Нужно же осозновать, что в то время не было тех же лямбда-выражений. В то время логика построения запросов была достаточно сложной. А сейчас этой логики вообще нет.
Без обид, но обсуждение сейчас идет по принципу "пророк не смотрел телевизор, и мы не будем. И всем запретим!". Фаулер 9 лет назад представить не мог, что можно будет получить массив объектов, применить к нему фильтры, отсортировать, сгруппировать, вызвать take(3) - и магическим образом выбрать 3 экземпляра объектов. Это на два порядка превышало возможности мейнстрим-языков.
Я пока не вижу ни одного агрумента против протаскивания IQuetyable на уровень BL, кроме "мы сделали из репозитория god-object-библиотеку-запросов, и это позволяет избежать дублирования". По поводу защиты от дураков - проще и дешевле дураков увольнять. Или не нанимать.
@Pasha
ОтветитьУдалитьЕще раз. В Repository локализуются запросы к БД. Если объект IQueryable передавать из Repository, от запросы типа x => x.Name и т.п. будут строится в любом месте проекта. Например, в слое BL может быть пару методов типа repository.Where(x => x.Name). Т.к. эта логика будет в паре мест, то это является дублированием логики построения запроса поиска по имени. Если потом надо будет изменить этот запрос на repository.Where(x => x.Name.ToLower()), то менять придется в двух местах.
@Александр
ОтветитьУдалить>Мы локализуем формирование запросов в одном месте, чтобы не было дублирования. Вот единственная причина использовать Repository. И эта же причина, чтобы не гонять IQueryable по всему приложению.
С запросами в одном месте дублирования будет гораздо больше. Например код пейджинга будет в каждом запросе в репозитарии, тогда как его можно вынести в отдельную абстракцию PagedList для представления.
А если не делать дублирования, но не выставлять IQueryable, то получим просаживание производительности или раздувание кода репозитария.
Еще раз повторюсь: все запросы в приложении в принципе не могут быть локализованы в одном месте, просто потому что разные части приложения обладают разной информацией о том какая выборка нужна. Поэтому и был придуман паттерн QueryObject. IQueryable - отличная реализация Query Object ибо Linq (монада). Придумывать свой Query Object неэффективно с точки зрения трудозатрат.
@gandjustas
ОтветитьУдалить> С запросами в одном месте дублирования будет гораздо больше
Нет. Откуда ему взяться?
Еще раз. Я говорю про логику выборки, т.е. бизнес-правила. Пример описан в части "Проблема: Repository, как фабрика запросов". Там выбираются активные пользователи. Понятие "активные пользователи" является бизнес-правилом. Поэтому его реализацию надо локализовать в одном месте. Я предложил сделать это в Repository.
Т.е. когда придется заказчик и поменяет это бизнес-правило, у нас будет одно место, где надо будет исправить понятие "активные пользователи".
Если же давать объект IQueryable хоть кому, то это бизнес-правило может быть описано в нескольких частях приложения (см. в примере).
> А если не делать дублирования, но не выставлять IQueryable, то получим просаживание производительности или раздувание кода репозитария.
Не понимаю, с чего вы это взяли. Просаживание производительности может быть только при составлении неоптимальных запросов или выборках N+1. Все эти проблемы относятся не к Repository, а к оптимизации приложения. Кстати оптимизацию проще делать, если все запросы формируются в одном месте, например, в Repository.
> все запросы в приложении в принципе не могут быть локализованы в одном месте
Могут, мы их локализовали в объектах Repository.
@Саша Бындю
ОтветитьУдалитьНу зачем ты так с @Pasha (дальше просто Паша)? Умные вещи говорит человек. Я бы с радостью с ним вместе поработал.
Тем более поступаешь как троль, уж прости за обвинение, но Паша привёл несколько весомых, логичных аргументов, а ты их игнорируешь и "ещё раз повторяешь".
Вобщем, прощая Паше некоторое преувеличение, видимо писал в запале, мысли выражены отлично, я подписываюсь под ними же.
Саша, чтобы решить проблему, что запросы x => x.Name; дублируются по проекту, можно инкапсулировать _логику построения_ этих запросов в отдельный тип-запрос. Назвать его например GetCustomerNamesQuery и использовать примерно так:
GetCustomerNamesQuery customerNames = new ...;
repository.Get(customerNames);
Решает обе проблемы.
>Там выбираются активные пользователи. Понятие "активные пользователи" является бизнес-правилом. Поэтому его реализацию надо локализовать в одном месте. Я предложил сделать это в Repository.
ОтветитьУдалитьВот в этом и спор, достичь цели "одно место для изменений" можно и описанным мной и Пашей и @gandjustas способомами, не нарушая ответственности репозитория.
>Если же давать объект IQueryable хоть кому, то это бизнес-правило может быть описано в нескольких частях приложения (см. в примере).
Может не значит будет, у тебя же XP команда, делай код ревью, не нужно защищаться от этого на уровне архитектуры.
@Restuta
ОтветитьУдалить> Саша, чтобы решить проблему, что запросы x => x.Name; дублируются по проекту, можно инкапсулировать _логику построения_ этих запросов в отдельный тип-запрос.
Да, это один способов - CQSR. Repository тоже может это сделать. С другой стороны, если давать IQueryable всему приложению, то кто знает, где потом искать формирование запроса.
> Может не значит будет, у тебя же XP команда, делай код ревью, не нужно защищаться от этого на уровне архитектуры.
То, что команда работает в стиле XP не означает, что весь код пишется идеально. Лучше всего на уровне архитектуры не давать (самим себе) делать ошибки.
Когда проект идет год, то в голове надо удерживать множество вещей. Совсем не хочется к этим вещам прибавлять то, что надо следить за тем, где IQueryable делает ToList(). Мы стараемся облегчать себе работу, а не надеяться на то, что все буду думать о каждой мелочи.
>То, что команда работает в стиле XP не означает, что весь код пишется идеально. Лучше всего на уровне архитектуры не давать (самим себе) делать ошибки.
ОтветитьУдалитьЭто как и любое утверждение верно не всегда и вот в данном случае это _может быть_ бОльшей платой. Скорее всего это работает для вашего проекта, в виду специфики, но шаринг IQueryable с умом отлично работает для нас. Просто когда в репозиториях(не хочу называть это репозиториями даже) появится тонна методов GetByXXX, в долгосрочной перспективе это головная боль. Если такие методы можно пересчитать по пальцам и нет описанных ребятами "сложных выборок с блекджеком и шлюхами", то я думаю это наилучший, потому что наиболее простой, выход.
P.S. Но репозиториями я эти сущьности бы не называл =) Для меня это просто дополнительный слой в бизнес логике, который стоит фасадом перед репозиторием и инкапсулирует построение запросов их выполнение.
@Restuta
ОтветитьУдалитьСогласен, что когда Repository начинают разрастаться, то больше подходит концепция CQSR.
Хочу заметить, что в CQSR нет даже намека на то, чтобы из xxxQuery передать объект IQueryable. Всё по тем же причинам.
Не ну это понятно, там уже есть место где _создаются запросы_. Всё относительно =)
ОтветитьУдалитьБывают просто _частные_ случаи, когда он нужен, это тот же пейджинг и фильрация. В таких вещах лишние абстракции только мешают, но опять же это можно организовать по-нормальному. Всё зависит от задачи и по-этому нельзя скзать "так репозиторий писать правильно". Да я понимаю, что ты и не пытался, но многие восприняли именно так, хотя большинство было право в контексте _собственных_, приведённых примеров.
Тут как женщины, кому какие попадались...
CQRS.
ОтветитьУдалитьНикита буквоед =)
ОтветитьУдалить@Restuta
ОтветитьУдалитьУгу :)
Вообще, в следующей статье я напишу про то, как мы перешли от Repository к CQRS.
@Nikita Govorov
Спасибо. Конечно имелось ввиду - Command and Query Responsibility Segregation (CQRS)
Подумай о том, чтобы прикрутить вменяемый дискашшн. Думаю флуда будет не меньше.
ОтветитьУдалить@Restuta
ОтветитьУдалитьЭто движок Blogger, так что будет тоже самое.
Вообще, я не против разных мыслей и того, что все делятся опытом, даже если я не согласен с некоторыми идеями. Наоборот, ради этого я и пишу статьи.
@Александр
ОтветитьУдалитьЯ же написал. банальный пример дублирования кода при постраничной разбивке результатов.
Но проблема не столько в дублировании, сколько в "площади изменений". Например был код с определенной проекцией в репозитарии. И не было дублирования, использовался он в 3 местах, а вот в четвертом месте нам понадобилось иметь слегка другую проекцию, получилось что создали еще метод в репозитарии, который содержит те же предикаты, но другую проекцию... Так рано или поздно получит на 20 представлений 10 запросов с одинаковыми предикатами, но разными проекциями... и тут нам понадобилось поменять предикат...
Можно сказать что можно устроить реюз внутри Resoitory, таки образом Repository превратится в god-object, который содержит 80% логики приложения.
Еще раз мысль. Само по себе дублирование не плохо, а вот повышенная "площадь изменений" - ужасно, god-object тоже.
А если у меня в двух контроллерах одинаковая проекция задается, то я вполне могу её в один экстеншн засунуть и никаких проблем не будет.
@Александр
ОтветитьУдалить>Еще раз. Я говорю про логику выборки, т.е. бизнес-правила. Пример описан в части "Проблема: Repository, как фабрика запросов". Там выбираются активные пользователи. Понятие "активные пользователи" является бизнес-правилом. Поэтому его реализацию надо локализовать в одном месте. Я предложил сделать это в Repository.
А я предлагаю это в BL сделать, там ему и место.
>Если же давать объект IQueryable хоть кому, то это бизнес-правило может быть описано в нескольких частях приложения (см. в примере).
При любой архитектуре код можно написать неправильно. У тебя в любом случае будет код, который достает всех пользователей и разработчик может попытаться написать получение активных пользователей поверх него. И никак ты от этого не защитишься, можно только научить разработчиков как писать правильно.
>Могут, мы их локализовали в объектах Repository.
Ценой того что повысили площадь изменений, создали потенциал раздувания код репозитария и фактически превратили его в God-Object
@Александр
ОтветитьУдалить>С другой стороны, если давать IQueryable всему приложению, то кто знает, где потом искать формирование запроса.
А а пройтись по стеку вызовов это проблема?
>Совсем не хочется к этим вещам прибавлять то, что надо следить за тем, где IQueryable делает ToList()
Не надо за этим следить, за этим будет следить компилятор. Если ты возвращаешь IQueryable из метода, то потребуются дополнительные приседания чтобы материализовать коллекцию и вернуть IQueryable, подобные действия можно рассматривать как вредительство проекту.
А еще можно IOrderedQueryable возвращать, вообще потрясающая вещь.
> Согласен, что когда Repository начинают разрастаться, то больше подходит концепция CQSR.
ОтветитьУдалитьЕсли репозиторий разрастается - то значит у вас не репозиторий, а библиотека запросов. Которая занимается составлением спецификаций запросов (вызовом методов вроде .Where) и их выполенением (.ToList()). Хотя у того же Фаулера, черным по белому:
Client objects construct query specifications declaratively and submit them to Repository for satisfaction.
Сделать поправку на то, что у Фаулера нет понятия IEnumerable - и можно ваш "репозиторий" переименовывать в "AllApplicationQueries".
> Еще раз. В Repository локализуются запросы к БД.... то менять придется в двух местах.
Еще раз. Оригинальный паттерн Репозиторий не предполагает "локализицию запросов" и тем более он не обращается к БД (а к уровню Data Mapping). Он как-то позволяет выполнять запросы к "массивам объектов" вместо запросов к базе данных.
То, что именно у вас что-то локализуется - это особенность вашей реализации. И разрастание - это прямое следствие такой особенности. Очевидно же, с таким подходом "репозиторий" станет абсолютно неюзабельным даже на мелком приложении. Он всосет в себя вообще все используемые комбинации фильтров, сортировок, группировок и наборов полей.
ToLower - это никак не особенность механизма построения запроса базе данных. Это часть формального описания запроса, то самое, которое репозиторий принимает как параметр. Механизм построения во всех рассмотренных системах уже реализован и запрятан под IQueryable.
Кстати, добавление .ToLower в репозиторий - это очень вероятный single line fix перед релизом. который "ничего не поломает" :)
IMHO, вероятность того, что через год новый разработчик будет знать о моменте выполнения запроса IQueryable - 99.99%, если у вас есть собеседования при приеме на работу.
а вероятность того, что через год, когда проект отдадут другой команде на саппорт, кто-то корректно впишет 1234-й метод в репозиторий - 0.00001%
Представь, что я пришел в команду через год, когда все начинавшие проект ушли.
- я: а почему вы наверх IQueryable не вытягиваете
- не знаем, так принято
- я: рассказываю анекдот про обезъян и "так принято" и вытягиваю IQueryable.
Просто об архитектуре рассуждается так, как будто вы из бетона строите, на века. А на самом деле первый же залетевший дятел^WПаша...
Топик, на самом деле, очень холиварный. Например:
UoW - TransactionScope с RowLevel блокировкой и локхинтами.
Репозиторий с IQueryable - без.
Репозиторий с GetXXXOrderByYYYTopZZZ - CRUD и спецификации/lambda.
Выборка данных сущности целиком (мы можем себе это позволить!) - проекции (только имена!)
Энтерпрайз и пусть хоть N+1 - у нас лимит в секунду на запрос.
@любой_челоек_упоминавший_сервисы_или_poco - http://www.martinfowler.com/bliki/AnemicDomainModel.html
Александр, а как вы вообще оцениваете применимость репозитория (в длине/толщине проекта/попугаях)?
---тут было длинное сообщение, которе сожрал движок комментов. или модератор---
ОтветитьУдалить@Александр
вы реализуете не репозиторий. вы реализуете "библиотеку запросов". Перечитайте определение оригинального паттерна и сравните его назначение со своей реализацией. Пересечение - ∅.
по остальному за меня выше ответили, по сравнению с моим постом не хватает только пары анекдотов.
@gandjustas
ОтветитьУдалить> Я же написал. банальный пример дублирования кода при постраничной разбивке результатов.
Этот пример очень хорошо ложится на работу с Repository. Возвращать от будет IPagedList. У нас так сделано. Дублирования нет.
> А если у меня в двух контроллерах одинаковая проекция задается, то я вполне могу её в один экстеншн засунуть и никаких проблем не будет.
Что ты будешь делать, когда эта же проекция будет нужна не в веб-приложении, а, например, в сервисе?
> А я предлагаю это в BL сделать, там ему и место.
Тогда твоя BL превратится в мои Repository. А твой Repository просто в объект, который создается IQueryable.
> У тебя в любом случае будет код, который достает всех пользователей
Это еще одна популярная ошибка, делать метод типа GetAll. Такого метода не должно быть.
> Ценой того что повысили площадь изменений, создали потенциал раздувания код репозитария и фактически превратили его в God-Object
Я это раньше не уточнял, но в проекте, естественно, не один репозиторий живет. Там есть AccountRepository, ProductRepository и т.д. Т.е. нет единого репозитория со всеми методами.
> А а пройтись по стеку вызовов это проблема?
Это шутка такая? :)
Знаешь, когда 10 разработчиков пол года пишут код, то да пройтись по стеку вызовов это проблема. Через пол года уже всего кода и не упомнишь.
@Pasha
ОтветитьУдалитьОк, перечитываем:
"In such systems it can be worthwhile to build another layer of abstraction over the mapping layer where query construction code is concentrated"
В моем случае - это слой с объектами Repository. Что это в случае протаскивания IQueryable по всему приложению?
"In these cases particularly, adding this layer helps minimize duplicate query logic"
Да, потому что вся специфика построения запросов находится в объектах Repository и не видна другим частям приложения. В случае с IQueryable
запрос может строить кто угодно.
"Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection"
Опять да. Мой репозиторий может сохранять в себя объекты, отдавать объекты и удалять объекты. При этом пользователь даже не догадается, что является настоящим источником данных.
В вашем случае объект IQueryable подходит только для хранения в БД. А что если один из репозиториев скрывает файл на жестком диске или данные из ActiveDirectory? Видимо придется реализовывать для них свой IQueryable.
"Client objects construct query specifications declaratively and submit them to Repository for satisfaction"
Прям этот пример у меня описан.
А где в вашем случае формируются спецификации? Как я понял могут в контроллере (в случае веб-приложения), в слое БЛ ну и по большому счету где угодно.
@Pasha
ОтветитьУдалитьНужно прятать слой доступа за интерфейсом, хотя бы потому что, linq поддерживается не всеми ORM. Для примера: у нас был LLBLGen, чей гибкости нам не хватало и мы в считанные часы перешли на использование NHibernate. Но, потом для особо критичных ситуаций, пришлось использовать чистые DataTable + Bulk для записи. И тут-то вся наша инкапсуляция и пригодилась:)
Этот комментарий был удален автором.
ОтветитьУдалитьТакже, допустим вам понадобилось переписать узкий участок с linq на criteria API, или HQL, что вы будете делать? Мы же изменим в *одном месте* реализацию и все, в текущем приложении придется перекомпилировать только сборку xxx.Domain.NHibernate и подкинуть ее в bin приложения.
ОтветитьУдалитьИнтересно, как сторонники выставления IQueryable борются с тем, что выставленный ими контракт не работает полностью(помимо научить всех, уволить лентяев, взять специалистов за 3000). Т.е. вы выставляете интерфейс, который предоставляет много всего, чем пользоваться в случае доступа к данным нельзя. С этим вообще спокойно живется?
ОтветитьУдалить@Александр
ОтветитьУдалить>Этот пример очень хорошо ложится на работу с Repository. Возвращать от будет IPagedList. У нас так сделано. Дублирования нет.
Ну конечно же есть. Как вызывается этот Repository? Ведь надо с UI до Repository через все слои приложения передать параметры сортировки, а код это совершенно одинаковый.
Или Repository вызывается прямо из Controller\Presenter\ViewModel? тогда у него все черты типичного god-object.
>Что ты будешь делать, когда эта же проекция будет нужна не в веб-приложении, а, например, в сервисе?
Они уезжает в service layer и живет там спокойно.
>Тогда твоя BL превратится в мои Repository. А твой Repository просто в объект, который создается IQueryable.
Да мне не важны названия, мне важна суть. А суть заключается в том что на нижнем уровне отдается IQueryable из контекста, а для целей тестирования этот объект вполне легко подменяется.
>Это еще одна популярная ошибка, делать метод типа GetAll. Такого метода не должно быть.
А если нужно выводить список всех пользователей?
Такой метод почти 100% будет присутствовать из-за бизнес-требований.
>Я это раньше не уточнял, но в проекте, естественно, не один репозиторий живет. Там есть AccountRepository, ProductRepository и т.д. Т.е. нет единого репозитория со всеми методами.
god-object не потому что один на все приложение, а потому что содержит всю логику приложения. (читай "злостно нарушает SRP") От того что методы разбиты на два объекта проще жить не становится. Площадь поражения при изменении от этого не уменьшится.
>Знаешь, когда 10 разработчиков пол года пишут код, то да пройтись по стеку вызовов это проблема. Через пол года уже всего кода и не упомнишь.
Пройтись надо там где тормозит, а не везде. Под отладчиком это более чем просто сделать.
А что значит "не работает полностью" ? Он как раз полностью работает ибо позволяет почти любой запрос родить.
ОтветитьУдалить@Nikita Govorov
ОтветитьУдалитьпишем тесты. то, что "нельзя" валит билд.
проблема никуда не девается в случае "непротаскивания" - в спецификации из поста можно тоже вписать любой Expression>. И с этим почему-то всем спокойно живется.
@Александр
Вы не видите разницы между "формированием запросов к коллекциям объектов" и формированием
запросов к data mapping layer. Первое - ответственноть BL (в виде генерации спецификаций или вызовов IQueryable). Второе - ответственность репозитория. Я честно пытался вам эту разницу показать.
Но дело в том, что вы реализовали репозиторий поверх готового репозитория, и "специфики формирования запросов к data mapping layer" у вас просто нет.
И в вашей реализации (с использованием спецификаций) когда в нем быть вообще не должно.
Простой пример - взять вашу реализацию IUoW/IRepository для NHibernate и подсунуть ей реализацию ISession под EF. Или с ISession для L2S. И при этом ваш репозиторий (который как бы содержит и скрывает детали доступа через конкретный ORM - NHibernate) будет вполне так работать. Что практически означает, что он не скрывает в никакой "логики построения запросов к DAL". И что в ваши бодрые ответы "да, прямо про мою реализацию" можно дописать "потому что я все сделал поверх готовой реализации репозитория, и на самом деле все делает она, а единственное предназночение моего репозитория - скрывать ее просто так" :)
Более-менее реальную ситуацию привел @hazzik, но я бы на его месте построил специфический IQueryable поверх датасетов.
>Видимо придется реализовывать для них свой IQueryable.
Давно не заглядывали в список готовых провайдеров? Тогда да, придется реализовывать. Но я бы взял готовый.
И кстати, реализовать свой IQueryable - проще простого. Я сам это делал раза 3-4. В разы проще, чем аккуратно продумать самопальный заменитель IQuery(able).
@Гэнжастэс
ОтветитьУдалитьquery.Where(customer=> customer.Name == "gandjustas" && СustomerExternalSystem.IsActive(customer.Id));
@Паша
ОтветитьУдалитьТестироваться по идее должен тот класс, который отдает контракт. После того как он протестирован, он должен работать. У вас получается вы предоставляете контракт, этим заявляя, делайте все, что хотите, ошибок быть не должно. Однако это не так.
>И кстати, реализовать свой IQueryable - проще простого.
IQueryable - не рассчитан для доступа к данным, он слишком много умеет.
@Nikita Govorov
ОтветитьУдалитьтестирование одного класса - это при чистых юнит-тестах. у нас - скорее интеграцционные/automatic acceptance tests.
Какой смысл тестировать отдельно репозиторий и отдельно бизнес-логику, которая, возможно, выполяне непподерживаемые запросы.
@IQueryable - не рассчитан для доступа к данным, он слишком много умеет.
Он умеет достаточно. Слишком много - понятие относительное. В некоторых системах возможность отфильтровать по любому аттрибуту - это "слишком много", т.к. из-за объемов данных фильтровать можно строго по индексам. А в некоторых - выборка фиксированных сущностей - это "слишком мало", потому что все табличные представления кастомизируемые на UI. И в максимальной конфигурации тянут пол-базы.
Ок, Пусть репозиторий умеет слишком много. Что предлагается взамен?
1. ISpecification - умеет практически столько же, сколько IQueryable. N+1 или СustomerExternalSystem.IsActive - да хоть диск можно отформатирвать.
2. Отдельный метод на каждую комбинацию условий/сортировки/пейджинга + отдельный именованный класс на каждую проекцию. Разрастание и неподдерживаемость уже в течении года.
три варианта - "слишком много", "слишком много", "god object" (или семья богов, каждый из которых покровительствует своей сущности). какой из 3-х? :)
Кстати, что у вас за проекты такие, всего на год? А потом их закрывают? :)
@hazzik
ОтветитьУдалитьНа всякий случай - конкретно ваш пример - вполне убедителен. Но даже в нем можно было обойтись, например, добавлением одного метода в IRepository, под конкретную тормозную ситуацию, и заменой одного экстеншн-метода. Подкинуть 1 или 2 dll - разницы практически никакой.
Лично я ни ни разу не видел замены "ORM с IQueryable" на "ORM без IQueryable". И в обоих вариантах реализации от автора топика такая замена выльется очень долгое переписываение и тестирование. Переход наоборот намного проще и приятнее. :)
@Паша
ОтветитьУдалитьЯ использую вариант возврата своего квери билдера, с возможностью создания своего квери обжекта и одновременно возможностью возврата репозитория, как набора простых предопределенных запросов-методов.
>Кстати, что у вас за проекты такие, всего на год? А потом их закрывают?
Если проект разрабатывает группа разработчиков из n человек один год, и итоге сдает его в эксплуатацию... это нормально... вы это имеете ввиду?
@Nikita Govorov
ОтветитьУдалитьотличный вариант. но он ни за "наделать кучу методов", ни за "вернуть IQueryable". только, IMHO, сейчас найти джуниора с безопасным уровнем знания linq намного проще, чем найти джуниора, который вникнется в QueryBuilder и самописный QueryObject. Особенно если ему в соседнем офисе предложат "новый проект с LINQ и всем таким".
> Если проект разрабатывает группа разработчиков из n человек один год, и итоге сдает его в эксплуатацию... это нормально... вы это имеете ввиду?
Да, нормально, но только в аутсорсе. просто мы занимаемся в основном не разработкой "под заказ". Собственные проекты постоянно дописываются и развиваюся. Не можем позволить себе "сдать и забыть". :)
@Pasha
ОтветитьУдалить>На всякий случай - конкретно ваш пример - вполне убедителен. Но даже в нем можно было обойтись, например, добавлением одного метода в IRepository, под конкретную тормозную ситуацию
Я это и говорил, что нужно прятать логику либо за IQuery/ISpecification или IXXXRepository.GetYYY
>и заменой одного экстеншн-метода. Подкинуть 1 или 2 dll - разницы практически никакой.
Чего-то не разу не видел такой легкой замены метода расширения. Если вы его меняете на другой метод, то тут полезут все зависимости или если вы меняете его реализацию, то налицо нарушение OCP.
При этом, замена одной реализации интерфейса на другой, через контейнер - ничего не нарушает, и, в большинстве случаев не требует даже перекомпиляции приложения.
>Кстати, что у вас за проекты такие, всего на год? А потом их закрывают? :)
Нет, потом они уходят в релиз и не требуют дальнейшей поддержки. Иногда, они возвращаются, но всегда не для исправления багов, а для реализации новых возможностей.
@hazzik
ОтветитьУдалитьЗа ISpecification все не спрячешь - это тот же IQueryable, только в профиль.
UoW у вас реализован? Если реализован, то как вы скрещивали его с теми самыми методами, которые работают с DataTable, а не с основным ORM. Репозиторий и IUoW явно связывали?
И как решается проблема выборки тех же представлений, сортировки, пэйджинга?
> Нет, потом они уходят в релиз и не требуют дальнейшей поддержки.
Софт без багов? :)
>UoW у вас реализован? Если реализован, то как вы скрещивали его с теми самыми методами, которые работают с DataTable
ОтветитьУдалитьВ том проекте, еще не использовали UoW
>За ISpecification все не спрячешь - это тот же IQueryable, только в профиль.
Не тот же. Отличие ISpecification от IQueryable в том, что ISpecification - конкретные реализации, а IQueryable - слишком абстрактный.
>И как решается проблема выборки тех же представлений, сортировки, пэйджинга?
Я вот уже на протяжении 100 комментариев не понимаю о какой проблеме вы говорите:( Никогда не было никаких проблем с сортировкой, пейджингом и т.д - собираются параметры для ISpecification, которая потом выполняется.
Сдается мне, что описанные вами проблемы возникают от того, что вы пытаетесь решать свои задачи слишком абстрактно, увы - но так не бывает. Код сначала пишется, а потом выделяются абстракции, а не наоборот.
>Софт без багов? :)
Приходится - слишком высока плата за ошибки;)
>Отличие ISpecification от IQueryable в том, что ISpecification - конкретные реализации, а IQueryable - слишком абстрактный.
ОтветитьУдалитьПосмотрите рекомендуемый автором пример реализации ISpecification в посте. Ничего в нем конкретного нет. Выдает Expression>. Ваша реализация репозитория может сделать выборку по любому Expression>? :)
Почему все так усиленно считают IQueryable слишком "абстрактным"? Очень урезанный интерфейс. Полно заготовок, в которых буквально 2 метода, обязательны к реализации. На 2 полпопугая абстрактнее спецификации с лямбдами.
> Я вот уже на протяжении 100 комментариев не понимаю о какой проблеме вы говорите:( Никогда не было никаких проблем с сортировкой, пейджингом и т.д - собираются параметры для ISpecification, которая потом выполняется.
Раз не только я говорю - значит проблеа есть.
У вас не аналог реализации ISpecification из статьи), с Expression Tree на выходе. Обычная классическая реализация + QueryObject.
А проблема в том, что реализацию с выражениями очень тяжело полноценно реализовать без готового IQueryable внутри. Попробуйте прикрутить ISpecification из статьи к своему репозиторию. По затратам будет как на 95% свой IQueryable реализовать. :)
Я же не настолько псих, чтобы утверждать что нельзя реализовать классические спецификации и классический Query Object. Верю, что и вы, и @Nikita Govorov вполне успешно это сделали. Но к теме статьи это имеет очень косвенное отношение. У вас буквально только названия паттерна совпадают.
Да, кстати, то что приведено в сообщении это не спецификация - это именованный предикат(точнее expression).
ОтветитьУдалить@Nikita Govorov
ОтветитьУдалитьА что есть спецификация?
Топ гугла.
3 - спецификации на Expression.
3 - возвращающие предикаты
4 - классические спецификации с IsSatisfiedBy(candidate). Как википедии.
И как бы все - спецификации, но первая "слишком абстрактная". А еще две - умеют работать только с уже материализованными объектами, и вообще никакого отношения к построению запросов и выборке данных из strorage не имеют. Разве что вся фильтрация происходит непосредственно в репозитории, но это как-то слишком по индусски.
http://martinfowler.com/apsupp/spec.pdf
ОтветитьУдалитьЯ себе так представляю:
public abstract class Specification : ISpecification
where TDomainObject : IDomainObject
{
public virtual bool IsSatisfiedBy(TDomainObject @object)
{
return SatisfyingElementsFrom(new[] {@object}.AsQueryable()).Any();
}
public abstract IQueryable SatisfyingElementsFrom(IQueryable candidates);
}
То что передается в FindBy репозитория, в терминах репозитория, скорее фильтр.
Парсер съел genetic. (
ОтветитьУдалить@Nikita Govorov,
ОтветитьУдалитьТакой код упадет на первом же запуске. Вы же не собираетесь шипать код, который ни разу не запускался.
@gandjustas
ОтветитьУдалитьА что с ним так?
@Nikita Govorov
ОтветитьУдалитьО чем вопрос?
Ты привел код, который гарантировнно упадет при запуске. Что ты этим доказал, что его можно написать? А кто помешает непутевому разработчику написать аналогичный код внутри "спецификации"? Она же внутри будет к тому же IQueryable обращаться, ибо сейчас ORM такие.
@gandjustas
ОтветитьУдалитьВы наверное меня не поняли, Паша спросил как я понимаю спецификации, я ответил. Я же не пишу, что я буду туда передавать IQueryable с провайдером от NHibernate или EF. IQueryable - c такими провайдерами используется только в пределах слоя доступа к данным.
Простите что вмешиваюсь)
ОтветитьУдалитьНо мне не совсем ясно как можно выбрасывать из репозитария IQueryable в случае использования nHibernate2, например, который LINQ не поддерживает, и "на лету" сменить репозитории, ранее работавшие на linq2sql или EF, мы уже не сможем.
Та же ситуация с использованием каких-то NoRM, где опять же полноценная поддержка LINQ не гарантирована.
По всей видимости подразумевается что ORM меняться не будет? А если будет?
Спасибо.
@Gengzu
ОтветитьУдалитьочевидно, точно так же, как и в авторском варианте со спецификациями. Спросите у @Александра.
Если серьезно - то реализовать свой IQueryable в рамках нужд проекта. Если вы подразумевали под "полноценной поддержкой" возможность выполнить вообще любой запрос - то ее нет ни в одном из существующих провайдеров, кроме Linq To Objects. Неполноценную, с обычной фильтрацией по свойствам - можно нарисовать за пару часов. По затратам и структуре работы - совпадает с переписываеним самописного QueryObject для нового провайдера.
@Gengzu
@Nikita Govorov
@hazzik
А почему вы задаете эти вопросы тем, кто упоминает IQueryable? Чем вам не угодил конктретно этот интерфейс?
Задайте их автору темы. Все ваши вопросы вполне актуальны для варианта со "спецификациями".
- а что, если внутренний провайдер не позволяет фильтровать по Expression>
Варианты "со спецификациями" и "c IQueryable" отличаются только именем метода, который выполняет запрос.
Способ составления запроса - тот же - неконтролируемые лямбды.
Способ ухода от дублирования кода - практически тот же - вынос цепочек лямбд в класс с одним методом (или метод расширения).
Точный момент выполнения запроса определяет BL в обоих случаях.
Почему вы спорите именно со мной, и с gandjustas?
>Способ ухода от дублирования кода - практически тот же - вынос цепочек лямбд в класс с одним методом (или метод расширения).
ОтветитьУдалитьВся разница в том, что методы расширения - методы статического класса, и, следовательно, нарушают OCP. Реализация интерфейса не нарушает OCP
@hazzik
ОтветитьУдалитьвы не на ту реализацию смотрите.
Проблема: Repository, как фабрика запросов
ваша реализация интерфейса - это Решение №1.
а та, про которую я говорю, и которую выше сравнивают с возвратом IQueryable - это Решение №2.
Нет, я говорю про решение N2
ОтветитьУдалитьpublic static class AccountQueryExtensions {
public static IQueriable{Accout} Active(this IQueryable{Account} self) {
return self.Where(x => x.IsDeleted == false && x.IsArchive == false);
}
}
-- нарушает OCP,
А
public class ActiveAccountSpecification : ISpecification{Account}
{
public Expression{Func{Account, bool}} IsSatisfiedBy()
{
return x => x.IsDeleted == false && x.IsArchive == false;
}
}
-- не нарушает OCP, хотя мне такая реализация и не нравится.
>Вся разница в том, что методы расширения - методы статического класса, и, следовательно, нарушают OCP. Реализация интерфейса не нарушает OCP
ОтветитьУдалитьНовое слово в проектировании, чем это статические методы OCP нарушают?
Наверное потому что экстеншены нарушают OCP весь Linq сделан на экстеншенах и отлично расширяем при этом, а классы реализации итераторов даже не являются public. Получается идеальный OCP - изменить не можешь в принципе, расширять - сколько угодно.
@hazzik
ОтветитьУдалитьможет я туплю, но чем нарушает?
реализация чего в первом случае было модифицирована, а во втором - расширена?
@hazzik
ОтветитьУдалитьметоды расширения в принципе не могут модифицировать поведение объекта, только расширить. это инструмент for extension. они даже по английски так и называются.
>Новое слово в проектировании, чем это статические методы OCP нарушают?
ОтветитьУдалитьhttp://blog.byndyu.ru/2009/10/blog-post_14.html
@hazzik
ОтветитьУдалитьКО, вы бы еще на википедию ссылку дали.
в статье по ссылке слово static не встречается.
не надо нам объяснять что такое OCP. объясните, чем, по вашему, его нарушают методы расширений.
В случае с методом расширения, есть несколько путей:
ОтветитьУдалить1. Изменение метода расширения, это уже само по себе нарушение OCP, в следствие нарушения SRP к классу содержащему методы расширения, ведь вы их разместили все в AccountQueryExtensions
2. Написать другой метод расширения, тем самым нарушив OCP по отношению к пользователям этого метода расширения, т.е. нужно обновить всех пользователей с MsSqlFullTextSearchExtensions на OracleFullTextSearchExtensions
С другой стороны, подмена реализации интерфейса вообще не требует ни изменения клиентского кода, ни изменения исходной реализации.
@hazzik
ОтветитьУдалитьВы как-то странно понимаете SRP.
Ьо, что вы выше описали в (1) - это просто локальное изменение. Которое вообще никаким боком ни SRP, ни OCP не затрагивает.
Ответственность у класса как была одна, так и осталась - предоставлять набор фильтров.
До изменения - этот класс представляет набор фильтров.
После изменения - этот класс представляет набор фильтров.
До изменения метод добавлял фильтр на активные аккаунты.
После изменения метод добавляет фильтр на активные аккаунты.
Нарушение SRP - это когда у класса/метода появляется второе назначение. Вы можете назвать второе назначение метода/класса, То, появление которого внезапно нарушило SRP?
Если не можете - то нарушения нет. Если можете - назовите.
(2) - это вообще какая-то нереальная ситуация. вся идея в том, чтобы не привязывать методы расширения к конкретному провайдеру.
@Pasha попробуйте почитать книжки умные, например Robert C. Martin "Agile Software Development: Principles, Patterns, and Practices"
ОтветитьУдалить@Александр, нехорошо молча удалять чужие сообщения. хоть бы след оставили.
ОтветитьУдалить@hazzik
могу посоветовать вам ту же самую книжку.
я понимаю, что вы думаете что я не знаю что responsibility - это на самом деле "причина изменения". но к сожалению, тут в комментах лимит на 4К, и если я буду расписывать вам "у этого метода была лишь одна причина для изменений - изменение набора условий фильтра, определяющий активность аккаунта", то я этот лимит прeвышу почти сразу.
Класс с набором фильтров - вообще не объект, просто конструкция языка, в которую можно положить extensions methods. и это явно проверяется компилятором.
По отношению к нему нельзя нарушить SRP. Как нельзя нарушить принцип SRP по отношению к неймспейсу, например. А если можно - то в случае спецификаций тоже нарушается SRP по отношению к неймспейсу или модулю.
Тут было пожелание вам удачи, я надеюсь вы подписаны на комменты, и получили его по почте.
@Pasha
ОтветитьУдалитьЯ не против, когда вы пишите то, с чем я не согласен, но я против сарказма и оскорблений. Все подобные комментарии будут удаляться.
Для всех несогласных рекомендую почитать Стива МакКоннела, совершенный код. Если не всю книгу, то обязательно 33 главу "Личность"
ОтветитьУдалитьЭта дискуссия одна из наиболее интересных которые я читал последнее время. Хотелось бы побольше такого.
ОтветитьУдалитьА пот теме я поддерживаю товарища gandjustas. Не вижу ничего такого в классической реализации ISpecification чего не было бы в IQuerable. Скорее наоборот. Что такого ISpecification ради чего стоило бы пожертвовать гибкостью IQuerable?
Я вполне принимаю мысль что в определенных условиях эта гибкость является недостатком, но в моей деятельности это не так.
А все проблемы на которые указывают оппоненты, по-моему, достаточно легко решаются различными способами расширения которые применимы к IQuerable.
Где-то год назад я тоже использовал ISpecification, но затем перешел на IQuerable и ни о чем не жалею. Возможно мой проект не настолько сложен чтобы оценить ISpecification... Утверждать не буду.
В любом случае огромное спасибо за этот пост и еще большее спасибо за комментарии. С нетерпением жду следующего поста и его обсуждения.
Александр, будет ли все-таки обещанный пост про использование архитектуры CQRS?
ОтветитьУдалитьБыло бы очень интересно посмотреть на реальный пример применения.
@Дмитрий
ОтветитьУдалитьЯ помню про обещанную статью, но сейчас совсем нет времени.
>>> Также, эту тему недавно поднимал menozz в гугл-группе DotNetConf: Многокриториальный поиск и паттерн Specification. Там есть несколько примеров кода.
ОтветитьУдалитьПримеры перестали работать. Можно ли их восстановить?
В разделе "Проблема связывания Repository и UnitOfWork" опечатка в примере. Локальная переменная единицы работы называется uow, но в конструктор репозитория передается uof
ОтветитьУдалить