maestrow / repository.md
Паттерн Репозиторий стал популярным благодаря DDD (Domain Driven Design). В противоположность к Database Driven Design в DDD разработка начинается с проектирования бизнес логики, принимая во внимание только особенности предметной области и игнорируя все, что связано с особенностями базы данных или других способов хранения данных. Способ хранения бизнес объектов реализуется во вторую очередь.
Применение данного паттерна не предполагает создание только одного объекта репозитория во всем приложении. Хорошей практикой считается создание отдельных репозиториев для каждого бизнес-объекта или контекста, например: OrdersRepository, UsersRepository, AdminRepository.
Generic Repository это антипаттерн
Возможно у вас возник вопрос: Зачем использовать репозиторий, если я использую ORM?
Действительно, ORM позволяет:
Однако может быть масса случаев, когда хранение данных представляет собой нечто более сложное или специфичное, чем просто ORM. И тогда такой слой данных инкапсулируется с помощью паттерна репозиторий:
Преимущества паттерна Репозиторий
Репозиторий и DAL (Data Access Layer, Persistence Layer)
Источник: Серия статей by Mike Mogosanu из его блога https://blog.sapiensworks.com:
На момент написания этой заметки статьи Mike размещались в категории Repository, затем он переделал свой блог и эти статьи попали в категорию Best Practces, что охватывает и другие вопросы, выходящие за рамки темы Паттерн Репозиторий. Поэтому привожу здесь ссылку на версию статей 2014 года.
Core Data + Repository pattern. Детали реализации
Немного про репозиторий
Таким образом, к его преимуществам можно отнести:
отсутствие зависимостей от реализации репозитория. Под капотом может быть все, что угодно: коллекция в оперативной памяти, UserDefaults, KeyChain, Core Data, Realm, URLCache, отдельный файл в tmp и т.п.;
разделение зон ответственности. Репозиторий выступает прослойкой между бизнес-логикой и способом хранения данных, отделяя одно от другого;
формирование единого, более структурированного подхода в работе с данными.
В конечном итоге, все это благоприятно сказывается на скорости разработки, возможностях масштабирования и тестируемости проектов.
Ближе к деталям
Рассмотрим самый неблагоприятный сценарий использования Core Data.
1. Core Data как один большой репозиторий с NSManagamentObject
Идея оперировать NSManagedObject в качестве доменного объекта самая простая, но не самая удачная. При таком подходе перед нами встают сразу несколько проблем:
Используя единый репозиторий для всех Data Provider, он будет разрастаться с появлением новых доменных объектов;
В худшем случае, логика работы с объектами начнет пересекаться между собой и это может превратиться в один большой непредсказуемый magic.
2. Core Data + DB Client
Первое, что приходит на ум, для решения проблем из предыдущего примера, это вынести логику работы с объектами в отдельный класс (назовем его DB Client), тогда наш Repository будет только сохранять и доставать объекты из хранилища, в то время, как вся логика по работе с объектами ляжет в DB Client. На выходе должно получиться что-то такое:
Рисунок 2
Обе схемы решают проблему №1. (Core Data ограничивается DB Client и Repository), и частично могут решить проблему №2 и №3 на небольших проектах, но не исключают их полностью. Продолжая мысль дальше, возможно придти к следующей схеме:
Рисунок 3
Core Data можно ограничить только репозиторием. DB Client конвертирует доменные объекты в NSManagedObject и обратно;
Repository больше не единый и он не разрастается;
Логика работы с данными более структурирована и консолидирована
Подготовка к реализации
В первую очередь, таким я вижу репозиторий:
Доменный объект, которым оперирует репозиторий;
Возможность подписаться на отслеживание изменений в репозитории;
Сохранение объектов в репозиторий;
Сохранение объектов с возможностью очистки старых данных в рамках одного контекста;
Загрузка данных из репозитория;
Удаление объектов из репозитория;
Удаление всех данных из репозитория.
Возможно, ваш набор требований к репозиторияю будет отличаться, но концептуально ситуацию это не изменит.
К сожалению, возможность работать с репозиторием через AccessableRepository отсутствует, о чем свидетельствует ошибка на рисунке 4:
Рисунок 4
В таком случае, хорошо подходит Generic-реализация репозитория, которая выглядит следующим образом:
NSObject нужен для взаимодействия с NSFetchResultController;
FatalError играет роль предохранителя, чтобы всяк сюда входящий не использовал то, что не реализовано;
Данное решение позволяет не привязываться к конкретной реализации, а также обойти предыдущую проблему:
Рисунок 5
Для работы с выборкой объектов потребуются объект с двумя свойствами:
Контекст, с которым работает main Queue, необходим для использования NSFetchedResultsController;
Требуется для выполнения операций с данными в фоновом потоке. Можно заменить на newBackgroundContext(). Про различия в работе этих двух методов можно прочитать тут.
Также, потребуются объекты, которые будут осуществлять конвертацию (мапинг) доменных моделей в объекты репозитория (NSManagedObject) и обратно:
Позволяет конвертировать NSManagedObject в доменную модель;
Позволяет обновить NSManagedObject с помощью доменной модели;
Когда-то, я использовал доменный объект, в качестве инициализатора NSManagedObject. С одной стороны, это было удобно, с другой стороны накладывало ряд ограничений. Например, когда использовались связи между объектами и один NSManagedObject создавал несколько других NSManagedObject. Такой подход размывал зоны ответственности и негативно сказывался на общей логике работы с данными.
Во время работы с репозиторием потребуется обрабатвать ошибки, для этого достаточно enum:
Реализация
Для данного примера, подойдет простая реализация DBContextProvider (без каких-либо дополнительных параметров):
Раньше такой подход избавлял от утечек памяти;
Основная составляющая репозитория выглядит следующим образом:
Cвойство, которое будет использовать при работе с NSFetchRequest;
Чтобы не порождать однотипный код для работы с контекстом, потребуется вспомогательный метод:
Сохранение изменений в persistent store;
Если изменения объектов отсутствуют, в completion-блок передается соответствующая ошибка.
Сохранение объектов реализовано следующим образом:
Используется при необходимости удалить объекты, перед сохранением новых (в рамках текущего контекста);
Выполняется выгрузка объектов, которые существуют в репозитории, для их дальнейшего изменения;
Если объект с нужным entityAccessorKey отсутствует, создается новый экземпляр NSManagedObject;
Выполнение мапинга свойств из доменного объекта в NSManagedObject;
Применение выполненных изменений.
Важно: Возможно вас смутил п.2., данное решение оптимально на небольших наборах данных. Я выполнял замеры (ExampleCase3 в демо-проекте) на 10 000 записей, iPhone 6s Plus IOS 12.4.1 и получил следующие результаты:
время записи/перезаписи данных от 0,9 до 1.8 сек, cкачок потребления оперативной памяти в пике до 33 мб;
если убрать код в п2 и оставить только вставку новых объектов, то время записи/перезаписи данных +- 20 сек, cкачок потребления оперативной памяти в пике до 50 мб.
Для больших наборов данных я бы рекомендовал разделять их на части, использовать batchUpdate и batchDelete, а начиная с IOS 13 появился batchInsert.
Таким образом, реализация методов save cводится к вызову метода saveIn:
Методы present, delete, eraseAllData завязаны на работе с NSFetchRequest. В их реализации нет ничего особенного, поэтому не вижу смысла заострять на них внимание:
Выборка объектов и их обработка;
Вовзрат результата операции.
Для реализации возможности отслеживания изменений данных в реальном времени, потребуется FetchedResultsController. Для его конфигурации используется следующий метод:
Формирование запроса, на основании которого будут отслеживаться изменения;
Создание экземпляра класса NSFetchedResultsController;
performFetch() позволяет выполнить запрос и получить данные, не дожидаясь изменений в базе. Например, это может быть полезно при реализации Ofline First;
Изменение свойства searchedData, в свою очередь уведомляет подписчиков (если такие имеются) об изменении.
Заключение
На этом этапе реализация всех основных методов для работы с репозиторием подходит к концу. Для меня основными преимуществами данного подхода стало следующее:
логика работы репозитория с Core Data стала везде единая;
для добавления новых объектов в репозиторий, достаточно создать только EntityMapper (новое Entity требуется создать в любом случае). Вся логика по мапингу свойств также собрана в одном месте;
Data слой стал более структурированным. Теперь можно точно гарантировать, что репозиторий не выполняет 100500 запросов в методе сохранения, чтобы проставить связи между объектами;
репозиторий легко можно подменить, например для тестов, или для отладки.
Спасибо за внимание! Легкого кодинга, поменьше багов, побольше фич!
Spring Data JPA
В статье опишу использование Spring Data.
Spring Data — дополнительный удобный механизм для взаимодействия с сущностями базы данных, организации их в репозитории, извлечение данных, изменение, в каких то случаях для этого будет достаточно объявить интерфейс и метод в нем, без имплементации.
1. Spring Repository
Основное понятие в Spring Data — это репозиторий. Это несколько интерфейсов которые используют JPA Entity для взаимодействия с ней. Так например интерфейс
public interface CrudRepository extends Repository
обеспечивает основные операции по поиску, сохранения, удалению данных (CRUD операции)
Есть и другие абстракции, например PagingAndSortingRepository.
Т.е. если того перечня что предоставляет интерфейс достаточно для взаимодействия с сущностью, то можно прямо расширить базовый интерфейс для своей сущности, дополнить его своими методами запросов и выполнять операции. Сейчас я покажу коротко те шаги что нужны для самого простого случая (не отвлекаясь пока на конфигурации, ORM, базу данных).
1. Создаем сущность
2. Наследоваться от одного из интерфейсов Spring Data, например от CrudRepository
3. Использовать в клиенте (сервисе) новый интерфейс для операций с данными
Здесь я воспользовался готовым методом findById. Т.е. вот так легко и быстро, без имплементации, получим готовый перечень операций из CrudRepository:
Понятно что этого перечня, скорее всего не хватит для взаимодействия с сущностью, и тут можно расширить свой интерфейс дополнительными методами запросов.
2. Методы запросов из имени метода
Запросы к сущности можно строить прямо из имени метода. Для этого используется механизм префиксов find…By, read…By, query…By, count…By, и get…By, далее от префикса метода начинает разбор остальной части. Вводное предложение может содержать дополнительные выражения, например, Distinct. Далее первый By действует как разделитель, чтобы указать начало фактических критериев. Можно определить условия для свойств сущностей и объединить их с помощью And и Or. Примеры
В документации определен весь перечень, и правила написания метода. В качестве результата могут быть сущность T, Optional, List, Stream. В среде разработки, например в Idea, есть подсказка для написания методов запросов.
Достаточно только определить подобным образом метод, без имплементации и Spring подготовит запрос к сущности.
3. Конфигурация и настройка
Весь проект доступен на github
github DemoSpringData
Здесь лишь коснусь некоторых особенностей.
В context.xml определенны бины transactionManager, dataSource и entityManagerFactory. Важно указать в нем также
путь где определены репозитории.
EntityManagerFactory настроен на работу с Hibernate ORM, а он в свою очередь с БД Oracle XE, тут возможны и другие варианты, в context.xml все это видно. В pom файле есть все зависимости.
4. Специальная обработка параметров
В методах запросов, в их параметрах можно использовать специальные параметры Pageable, Sort, а также ограничения Top и First.
5. Пользовательские реализации для репозитория
Предположим что в репозиторие нужен метод, который не получается описать именем метода, тогда можно реализовать с помощью своего интерфейса и класса его имплементирующего. В примере ниже добавлю в репозиторий метод получения сотрудников с максимальной оплатой труда.
Имплементирую интерфейс. С помощью HQL (SQL) получаю сотрудников с максимальной оплатой, возможны и другие реализации.
А также расширяю Crud Repository Employees еще и CustomizedEmployees.
Здесь есть одна важная особенность. Класс имплементирующий интерфейс, должен заканчиваться (postfix) на Impl, или в конфигурации надо поставить свой postfix
Проверяем работу этого метода через репозиторий
Другой случай, когда надо изменить поведение уже существующего метода в интерфейсе Spring, например delete в CrudRepository, мне надо что бы вместо удаления из БД, выставлялся признак удаления. Техника точно такая же. Ниже пример:
Теперь если в employeesCrudRepository вызвать delete, то объект будет только помечен как удаленный.
6. Пользовательский Базовый Репозиторий
В предыдущем примере я показал как переопределить delete в Crud репозитории сущности, но если это надо делать для всех сущностей проекта, делать для каждой свой интерфейс как то не очень. тогда в Spring data можно настроить свой базовый репозиторий. Для этого:
Объявляется интерфейс и в нем метод для переопределения (или общий для всех сущностей проекта). Тут я еще для всех своих сущностей ввел свой интерфейс BaseEntity (это не обязательно), для удобства вызова общих методов, его методы совпадают с методами сущности.
В конфигурации надо указать этот базовый репозиторий, он будет общий для всех репозиториев проекта
Теперь Employees Repository (и др.) надо расширять от BaseRepository и уже его использовать в клиенте.
Проверяю работу EmployeesBaseRepository
Теперь также как и ранее, объект будет помечен как удаленный, и это будет выполняться для всех сущностей, которые расширяют интерфейс BaseRepository. В примере был применен метод поиска — Query by Example (QBE), я не буду здесь его описывать, из примера видно что он делает, просто и удобно.
7. Методы запросов — Query
Ранее я писал, что если нужен специфичный метод или его реализация, которую нельзя описать через имя метода, то это можно сделать через некоторый Customized интерфейс ( CustomizedEmployees) и сделать реализацию вычисления. А можно пойти другим путем, через указание запроса (HQL или SQL), как вычислить данную функцию.
Для моего примера c getEmployeesMaxSalary, этот вариант реализации даже проще. Я еще усложню его входным параметром salary. Т.е. достаточно объявить в интерфейсе метод и запрос вычисления.
Упомяну лишь еще, что запросы могут быть и модифицирующие, для этого к ним добавляется еще аннотация @Modifying
Так например в моем гипотетическом примере, когда мне надо для всех сущностей иметь признак “удален», я сделаю базовый интерфейс с методом получения списка объектов с признаком «удален» или «активный»
Далее все репозитории для сущностей можно расширять от него. Интерфейсы которые не являются репозиториями, но находятся в «base-package» папке конфигурации, надо аннотировать @NoRepositoryBean.
Теперь когда будет выполняться запрос, в тело запроса будет подставлено имя сущности T для конкретного репозитория который будет расширять ParentEntityRepository, в данном случае Employees.
Шаблоны DAO против репозитория
Поймите разницу между шаблонами DAO и репозиторием на примере Java.
1. Обзор
Часто реализации репозитория и DAO считаются взаимозаменяемыми, особенно в приложениях, ориентированных на данные. Это создает путаницу в их различиях.
В этой статье мы обсудим различия между шаблонами DAO и репозиториями.
2. Шаблон ДАО
Поэтому во многих случаях наши DAO соответствуют таблицам базы данных, позволяя более простой способ отправки/извлечения данных из хранилища, скрывая уродливые запросы.
Давайте рассмотрим простую реализацию шаблона DAO.
2.1. Пользователь
Во-первых, давайте создадим базовый класс User domain:
2.2. UserDao
2.3. UserDaoImpl
3. Шаблон репозитория
Согласно книге Эрика Эванса Domain-Driven Design , репозиторий ” – это механизм для инкапсуляции поведения хранения, поиска и поиска, который эмулирует коллекцию объектов.”
Другими словами, репозиторий также имеет дело с данными и скрывает запросы, аналогичные DAO. Однако он находится на более высоком уровне, ближе к бизнес-логике приложения.
Следовательно, репозиторий может использовать DAO для извлечения данных из базы данных и заполнения объекта домена. Или он может подготовить данные из объекта домена и отправить их в систему хранения, используя DAO для сохранения.
3.1. Информация о пользователе
Во-первых, давайте создадим интерфейс UserRepository :
3.2. UserRepositoryImpl
Здесь мы использовали UserDaoImpl для отправки/извлечения данных из базы данных.
До сих пор мы можем сказать, что реализации DAO и репозитория выглядят очень похожими, потому что класс User является анемичным доменом. Кроме того, репозиторий-это просто еще один слой поверх уровня доступа к данным (DAO).
4. Шаблон Репозитория С Несколькими DAO
Чтобы четко понять последнее утверждение, давайте расширим наш User домен для обработки бизнес-прецедента.
Представьте, что мы хотим подготовить профиль пользователя в социальных сетях, объединив его твиты в Twitter, сообщения в Facebook и многое другое.
4.1. Твит
Во-первых, мы создадим класс Tweet с несколькими свойствами, которые содержат информацию о твите:
4.2. TweetDao и TweetDaoImpl
Здесь мы вызовем API Twitter, чтобы получить все твиты пользователя, используя его электронную почту.
Таким образом, в этом случае DAO предоставляет механизм доступа к данным с использованием сторонних API.
4.3. Расширение домена Пользователя
4.4. UserRepositoryImpl
Здесь UserRepositoryImpl извлекает данные пользователя с помощью UserDaoImpl и твиты пользователя с помощью TweetDaoImpl.
Аналогичным образом, мы можем расширить ваш Пользователь домен, чтобы сохранить список сообщений Facebook.
5. Сравнение двух моделей
Теперь, когда мы рассмотрели нюансы шаблонов DAO и репозитория, давайте обобщим их различия:
Кроме того, если у нас есть анемичный домен, репозиторий будет просто DAO.
Кроме того, шаблон репозитория поощряет доменный дизайн, обеспечивая легкое понимание структуры данных и для нетехнических членов команды|/.
6. Заключение
В этой статье мы исследовали различия между шаблонами DAO и репозиториями.
Во-первых, мы рассмотрели базовую реализацию шаблона DAO. Затем мы увидели аналогичную реализацию с использованием шаблона репозитория.
Наконец, мы рассмотрели репозиторий, использующий несколько DAO, расширяющий возможности домена для решения бизнес-задач.
Таким образом, мы можем сделать вывод, что шаблон репозитория доказывает лучший подход, когда приложение переходит от ориентированного на данные к бизнес-ориентированному.
Разбираемся, как работает Spring Data Repository, и создаем свою библиотеку по аналогии
На habr уже была статья о том, как создать библиотеку в стиле Spring Data Repository (рекомендую к чтению), но способы создания объектов довольно сильно отличаются от «классического» подхода, используемого в Spring Boot. В этой статье я постарался быть максимально близким к этому подходу. Такой способ создания бинов (beans) применяется не только в Spring Data, но и, например, в Spring Cloud OpenFeign.
Содержание
Итак, у нас прошли праздники, но мы хотим иметь возможность создавать на лету бины (beans), которые позволили бы нам поздравлять всех, кого мы в них перечислим.
При вызове метода мы хотим получать:
Т.е. мы должны найти все интерфейсы, которые расширяют интерфейс Congratulator или имеют аннотацию @Congratulate
@Enable
Как и любая взрослая библиотека у нас будет аннотация, которая включает наш механизм (как @EnableFeignClients и @EnableJpaRepositories ).
Напишем свою аннотацию
ImportBeanDefinitionRegistrar
Посмотрим, что происходит в ImportBeanDefinitionRegistrar у Spring Cloud Feign:
В Spring Cloud OpenFeign сначала создаются бины конфигурации, затем выполняется поиск кандидатов и для каждого кандидата создается Factory.
В Spring Data подход аналогичный, но так как Spring Data состоит из множества модулей, то основные моменты разнесены по разным классам (см. например org.springframework.data.repository.config.RepositoryBeanDefinitionBuilder#build )
Можно заметить, что сначала создаются Factory, а не сами bean. Это происходит потому, что мы не можем в BeanDefinitionHolder описать, как должен работать наш bean.
Сделаем по аналогии наш класс (полный код класса можно посмотреть здесь)
ResourceLoaderAware и EnvironmentAware используется для получения объектов класса ResourceLoader и Environment соответственно. При создании экземпляра CongratulatorsRegistrar Spring вызовет соответствующие set-методы.
Чтобы найти требуемые нам интерфейсы, используется следующий код:
Что, если мы хотим иметь возможность получать наши beans по имени, например, так
это тоже возможно, так как во время создания Factory в качестве alias было передано имя bean ( AnnotationBeanNameGenerator.INSTANCE.generateBeanName(candidateComponent, registry) )
FactoryBean
Теперь займемся Factory.
Стандартный интерфейс FactoryBean имеет 2 метода, которые нужно имплементировать
Заметим, что есть возможность указать, является ли объект, который будет создаваться, Singleton или нет.
Есть абстрактный класс ( AbstractFactoryBean ), который расширяет интерфейс дополнительной логикой (например, поддержка destroy-методов). Он так же имеет 2 абстрактных метода
Второй метод требует вернуть уже сам объект, а для этого нужно его создать. Для этого есть много способов. Здесь представлен один из них.
Сначала создадим обработчик для каждого метода:
Теперь при старте контекста Spring создает бины (beans) на основе наших интерфейсов.



