Repository

disclaimer: статья делает обзор конкретной реализации паттерна Repository, подробно рассматривая методы и особенности в рамках Incoding Framework. Для лучшего погружения ознакомьтесь с  Repository by Fowler, CQRS vs NLayer

UPD: исходные коды статьи, доступны на GitHub

Что мы получаем ?

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

  • Абстракция над базой данных

примечание: ORM и так является абстракцией над базой данных и Repository будет избыточен, но со временем я пришел к выводу, что лучше всегда иметь прослойку между сторонними компонентами, такими как ORMIoC и другими.

  • Унифицированный интерфейс для провайдеров ( Nhibernate , Entity Framework  и т.д.)

примечание: первый пункт от части покрывает второй, но все же это разные задачи. 

А может без него ?

DataContext на View linq to sql использовал название DataContext для  класса доступа к базе данных ), своеобразный “мем” при разработке web сайтов, который  находится на одном уровне с GOD object и другими анти-паттернами. На самом деле обращение не обязательно должно быть на View, потому что вызывая DataContext  из Controller или Service, проблема остается. Какие трудности влечет за собой использования Data Context без Repository:

  • Привязка к конкретной реализации ORM ( Linq to sql = DataContext, Nhibernate = ISession )

примечание: с одной стороны замена ORM на поздних этапах проекта, кажется спорным и сложным шагом, но на личном опыте убедились ( замена Linq to Sql на Nhibernate ), что это порой единственный выход для решения некоторых задач. 

  • Очень много низкоуровневых методов

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

  • Сложно контролировать места где идет работа с базой данных (Incoding Framework предоставляет доступ к Repository только в Query и Command )

примечание: основной трудностью будет поддержка UnitOfWork для обеспечения корректной работы транзакций

  • Трудности написания модульных тестов

примечание: из-за того, что код работает с объектами 3rd-party, то появляется сложности при создании Mockup

 

CRUD

Repository имеет методы для выполнения основных задач связанных с созданием ( create ), чтением ( read ), обновлением ( update ) и удалением ( delete ):

Create, Update, Delete

  • Save – сохраняет объект в базу данных

  •  Delete – удаляет объект по типу и Id

  • SaveOrUpdate –  сохраняет или обновляет объект в базе данных

примечание: метод SaveOrUpdate можно не использовать, потому что многие ORM ( Nhibernate, Entity Framework ) поддерживают tracking ( слежение ) состояние объекта, но если провайдер  не имеет такой возможности, то нужно всегда вызывать SaveOrUpdate

Read

  • GetById – возвращает объект по типу и Id

примечание: параметр Id это Object, причина отсутствия конкретного типа, в том чтобы добиться максимального гибкого ( Id может быть string, int, long, guid )  решения.

примечание: если Id null или объект не найден, то возвратом будет null

  • LoadById – возвращает объект по типу и Id

примечание: метод LoadById работает точно также как и GetById, с той разницей, что Load при обращение попытается найти объекты в Cache, а Get всегда обращается к базе данных

  •  Query – возвращает набор ( IQueryable ) объектов на основе спецификаций (  where, order, fetch, paginated )

примечание: руководство по работе с where, order, fetch спецификациями запроса рассмотрены ниже.

  • Paginated – возвращает paginated result на основе спецификаций (  where, order, fetch, paginated )

примечание: IncPaginatedResult  – это объект, который был разработан для обеспечения удобной работы с результатами, которые надо отображать постранично. Практика показала, что для построения постраничных данных, нужно знать общие ( TotalCounts ) количество элементов без учета страниц и  элементы ( Items ).

  • Total Counts  – общие количество элементов в базе данных с учетом where
  • Items – элементы находящиеся в диапазоне текущей странице

примечание: алгоритм Paginated Specification будет рассмотрен ниже

Спецификация

В C# есть LINQ, который позволяет строить план запросов, а выполнять ( транслировать ) их уже через определенный провайдер. Например если мы выбрали Nhibernate, как ORM для нашего приложения, то можно использовать IQueryable NHibernate provider, для того, чтобы транслировать условия ( where , order, fetch , paginated ) в SQL.

Чем же плох LINQ ?

Спецификации являются оболочкой LINQ выражений в отдельные классы, которые дают следующие преимущества:

  • Повторно использовать в разных Query

примечание: весомое преимущество, потому что при написание крупных проектов, трудно поддерживать “разбросанные” по коду LINQ выражения. 

  • Подмена на mock-up  объекты в тестах Query

примечание: тестирования спецификаций будет отдельно

  • Инкапсуляция дополнительной логики

Sample



 примечание: дополнительная логика, также актуальная и для Fetch и Order спецификаций

А может есть решения ?

Если не использовать specification, то можно применять C# extensions

примечание: данное решение обладает минусом, потому  что не получится протестировать Query без выполнения кода Extesnions.

Where

Реализация

Все спецификации для фильтрации наследуются от базового класса Specification<TEntity> ( TEntity – это тип объекта Query ), который имеет абстрактный метод IsSatisfiedBy.

примечание:  вместо null при пустом значении code можно вернуть выражение product = > true, но тогда в результирующем SQL запросе будут лишении условия, которые не будут нести логического нагрузки.

Использование

примечание: specification можно складывать через And, Or или Not

Linq аналог

Order

Реализация

Все спецификации для сортировки наследуются от базового класса OrderSpecification<TEntity> ( TEntity – это тип объекта Query ), который имеет абстрактный метод SortedBy.

  • Order(r= >r.Name,OrderType.Ascending)  – сортировка Name по возрастанию

примечание: OrderBy(r= >r.Name) сокращенный вариант

  • Order(r= >r.Name,OrderType.Descending)  – сортировка Name по убыванию

примечание: OrderByDescending(r= >r.Name) сокращенный вариант 

Использование

Linq аналог

Paginated

Реализация

PaginatedSpecification это уже готовый класс, так что наследников делать не надо, а создаем новый экземпляр через  конструктор ( CurrentPage и PageSize )

  •  Current Page – указывает какую страницу выбирать
  •  Page Size – указывает сколько элементов  на странице

Использование

примечание: если в  базе данных содержится 50 записей, то вернутся записи с 10 по 20 

Linq аналог

Fetch

Реализация

Все спецификации для выборки наследуются от базового класса FetchSpecification<TEntity> ( TEntity – это тип объекта Query ), который имеет абстрактный метод FetchedBy.

  •  Join   – подгружает  элемент по связи One to Many

примечание: можно получать доступ к полям выбранного элемента 

  • JoinMany – подгружает  элементы по связи Many to  One / Many to Many

примечание: можно получить доступ к полям каждого выбранного элемента

Linq аналог

Каждый ORM имеет свой способ выборки элементов

Актуальность

  • Fetch нужен когда сценарий Query возвращает объекты базы данных, а не подготовленную “плоскую” модель, но после прихода MVD, данный сценарий практический не востребован.
  • Основное применение Fetch это возможность ускорить Query за счет выборки всех данных за один запрос, но как показала практика, лучшим решением будет построение OLAP системы ( пример реализации можно посмотреть в Browsio )

примечание: тема OLAP  сложная и хорошо освещена в интернете, но в ближайшие время появится статья с обзором решений в рамках Incoding Framework

Особенности

Для эффективного применения Repository в Incoding Framework, надо учитывать следующие моменты:

  • Repository работает в контексте Unit of Work и поэтому транзакция не будет закрыта пока весь  код Command успешно не выполнится.

  • Query работает с Repository в режиме ReadUncommited

примечание: это говорит о том, что нельзя произвести сохранения или изменения в базе данных

  • Соединение с базой данных доступно только в контексте Command и Query

Пример



Опасность представляет  gap.Type.Name, потому что  мы обращаемся к дочерней таблице, что заставляет ORM ( если не выключено LazyLoad или добавлен  соответствующий для поля Fetch ) делать запрос в базу данных, для того, чтобы подгрузить элемент, поэтому если мы вернем List<Gap> на Controller и там обратимся к поле gap.Type.Name, то получим exception.

Vlad Kopachinsky

I am a Senior developer at Incoding Software in Russian Federation. I created the Incoding Framework for rapid development

More Posts - Website - Twitter - Facebook - LinkedIn

Leave a Reply