Поделиться через


Проектирование уровня сохраняемости инфраструктуры

Совет

Это содержимое является фрагментом из электронной книги, архитектуры микрослужб .NET для контейнерных приложений .NET, доступных в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Компоненты сохраняемости данных предоставляют доступ к данным, размещенным в границах микрослужбы (то есть в границах базы данных микрослужбы). Они содержат фактическую реализацию компонентов, таких как репозитории и классы единиц работы, например пользовательские объекты Entity Framework (EF) DbContext. EF DbContext реализует как репозиторий, так и шаблоны единиц работы.

Шаблон репозитория

Шаблон репозитория — это шаблон конструктора на основе домена, предназначенный для сохранения проблем вне модели домена системы. Одна или несколько абстракций сохраняемости — интерфейсы — определены в модели домена, и эти абстракции имеют реализации в виде адаптеров для сохраняемости, определенных в другом месте приложения.

Реализации репозитория — это классы, которые инкапсулируют логику, необходимую для доступа к источникам данных. Они централизуют общие функциональные возможности доступа к данным, обеспечивая лучшую поддержку и разбиение инфраструктуры или технологии, используемой для доступа к базам данных из модели домена. Если вы используете объектно-реляционное сопоставление (ORM), например Entity Framework, код, который необходимо реализовать, упрощается благодаря LINQ и строгой типизации. Это позволяет сосредоточиться на логике сохраняемости данных, а не на вспомогательных функциях для доступа к данным.

Шаблон репозитория представляет собой хорошо задокументированный способ работы с источниками данных. В книге Шаблоны архитектуры корпоративных приложений Мартин Фаулер описывает репозиторий следующим образом.

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

Определение одного репозитория для каждого агрегата

Для каждого агрегата или корня агрегации следует создать только один класс репозитория. Вы можете использовать универсальные службы C# для уменьшения общего числа конкретных классов, которые необходимо поддерживать (как показано далее в этой главе). В микрослужбе на основе шаблонов предметно-ориентированного проектирования единственный канал, который следует использовать для обновления базы данных, — репозитории. Это вызвано тем, что у репозитория есть связь "один к одному" с корнем агрегации, который управляет инвариантностью агрегата и согласованностью транзакций. Отправлять запросы к базе данных также можно через другие каналы (например, с использованием подхода CQRS), так как запросы не изменяют состояния базы данных. Однако область транзакций (то есть обновления) всегда должна контролироваться репозиториями и корнями агрегации.

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

Как уже отмечалось, если вы используете шаблон архитектуры CQS/CQRS, то начальные запросы будут выполнены с помощью сторонних запросов модели предметной области, которые выполняются с помощью простых инструкций SQL с использованием Dapper. Этот подход является гораздо более гибким по сравнению с репозиториями, так как вы можете отправлять запросы к любым необходимым таблицам и объединять эти таблицы, и эти запросы не ограничены правилами агрегатов. Эти данные перейдут на уровень представления или в клиентское приложение.

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

Важно еще раз подчеркнуть, что следует определять только один репозиторий для одного корня агрегации, как показано на рис. 7-17. Чтобы корень агрегации поддерживал транзакционную согласованность между всеми объектами в агрегате, не следует создавать репозиторий для каждой таблицы в базе данных.

Diagram showing relationships of domain and other infrastructure.

Рис. 7-17. Связь между репозиториями, агрегатами и таблицами базы данных

На приведенной выше схеме показаны связи между уровнями домена и инфраструктуры: агрегат покупателя зависит от интерфейса IBuyerRepository и Order Aggregate зависит от интерфейсов IOrderRepository, эти интерфейсы реализуются на уровне инфраструктуры соответствующими репозиториями, которые зависят от UnitOfWork, также реализованных там, что обращается к таблицам на уровне данных.

Использование только одного корня агрегации для одного репозитория

При создании архитектуры репозитория может оказаться полезным правило, которое позволяет создать только один репозиторий для одного корня агрегации. Вы можете создать универсальный или базовый репозиторий, ограничивающий типы объектов, с которым он работает, интерфейсом метки IAggregateRoot.

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

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
    public class OrderRepository : IOrderRepository
    {
      // ...
    }
}

Каждый интерфейс репозитория реализует общий интерфейс IRepository:

public interface IOrderRepository : IRepository<Order>
{
    Order Add(Order order);
    // ...
}

Тем не менее более эффективный способ с помощью кода внедрить соглашение о том, что каждый репозиторий связан с одним корнем агрегации, — реализовать общий тип репозитория. Таким образом, вы явным образом используете репозиторий для нацеливания на определенный агрегат. Это можно сделать, реализовав общий репозиторий в базовом интерфейсе IRepository, как показано в следующем коде:

public interface IRepository<T> where T : IAggregateRoot
{
    //....
}

Шаблон репозитория упрощает тестирование логики приложения

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

Как отмечалось в предыдущем разделе, рекомендуется определить и разместить интерфейсы репозитория на уровне модели предметной области, чтобы уровень приложения (например, микрослужба веб-API) напрямую не зависел от уровня инфраструктуры, на котором реализованы фактические классы репозитория. Сделав это и используя внедрение зависимостей в контроллерах веб-API, вы можете реализовать макеты репозиториев, которые возвращают псевдоданные вместо данных из базы данных. Этот подход с разделением позволяет создавать и запускать модульные тесты, которые нацелены на логику приложения и не требуют подключения к базе данных.

Подключение к базе данных может завершиться неудачно, а главное — выполнение нескольких сотен тестов для базы данных не рекомендуется по двум причинам. Во-первых, это может занять много времени из-за большого количества тестов. Во-вторых, записи базы данных могут изменяться и влиять на результаты тестов, особенно если тесты выполняются параллельно, чтобы они не были согласованы. Модульные тесты обычно могут выполняться параллельно; тесты интеграции могут не поддерживать параллельное выполнение в зависимости от их реализации. Тесты, которые выполняются над базой данных, — это не модульные тесты, а тесты интеграции. У вас должно быть много модульных тестов, которые быстро выполняются, и меньшее количество тестов интеграции, которые выполняются над базами данных.

С точки зрения проблемы разделения для модульных тестов, ваша логика работает над объектами предметной области в памяти. Предполагается, что класс репозитория доставил эти объекты. После того как логика изменяет объекты предметной области, предполагается, что класс репозитория правильно сохраняет их. Важно создавать модульные тесты для модели предметной области и ее логики. Корни агрегации представляют собой главные границы согласованности в предметно-ориентированном проектировании.

Репозитории, реализованные в eShopOnContainers, зависят от реализации класса DbContext в EF Core для шаблонов репозитория и единицы работы с использованием средства отслеживания изменений, которое помогает избежать дублирования функций.

Разница между шаблоном репозитория и устаревшим шаблоном класса для доступа к данным

Типичный объект DAL напрямую выполняет операции доступа к данным и сохраняемости в хранилище, часто на уровне одной таблицы и строки. Простые операции CRUD, реализованные с набором классов DAL, часто не поддерживают транзакции (хотя это не всегда так). Большинство подходов к классу DAL используют минимальное использование абстракций, что приводит к жесткой связи между классами уровня бизнес-логики (BLL), которые вызывают объекты DAL.

При использовании репозитория сведения о реализации сохраняемости инкапсулируются от модели домена. Использование абстракции обеспечивает простоту расширения поведения с помощью шаблонов, таких как декораторы или прокси-серверы. Например, перекрестные проблемы, такие как кэширование, ведение журнала и обработка ошибок, могут применяться с помощью этих шаблонов, а не жестко закодированных в самом коде доступа к данным. Кроме того, это просто для поддержки нескольких адаптеров репозитория, которые могут использоваться в разных средах, от локальной разработки до общих промежуточных сред до рабочей среды.

Реализация единицы работы

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

Эти несколько операций сохраняемости выполняются позднее в одном действии, когда из кода на уровне приложения поступает команда на их выполнение. Решение о применении изменений в памяти к фактическому хранилищу базы данных обычно основано на шаблоне единицы работы. В EF шаблон единицы работы реализуется и DbContext выполняется при вызове SaveChanges.

Во многих случаях этот шаблон, или способ применения операций к хранилищам, может повысить производительность приложения и снизить риск возникновения несоответствий. Кроме того, он снижает блокировку транзакций в таблицах базы данных, потому что все необходимые операции фиксируются как часть одной транзакции. Это более эффективно по сравнению с выполнением многих изолированных операций в базе данных. Таким образом, в выбранных ORM можно оптимизировать выполнение запросов к базам данных путем группирования нескольких действий обновления в одной и той же транзакции в отличие от выполнения нескольких отдельных мелких транзакций.

Шаблон единицы работы можно реализовать с помощью шаблона репозитория или без нее.

Репозитории не должны быть обязательными

Пользовательские репозитории удобны по перечисленным выше причинам, и именно: этот подход используется для упорядочивания микрослужб в eShopOnContainers. Однако этот подход необязателен в предметно-ориентированном проектировании и даже при разработке в .NET в целом.

Например, Джимми Богард в качестве прямого отзыва на это руководство отметил следующее:

Пожалуй, это мой самый большой отзыв. Я не большой поклонник репозиториев, главным образом, потому, что они скрывают важные детали механизма сохраняемости. Именно поэтому я пользуюсь MediatR для команд. Я могу использовать все возможности уровня сохраняемости и отправить все это поведение предметной области в корни агрегации. Обычно я не хочу создавать макеты репозиториев, так как этот тест интеграции нужно проводить на реальной системе. Переход на CQRS означал, что у нас действительно больше не было потребности в репозиториях.

Репозитории могут оказаться полезными, но они не являются критически важными для структуры DDDD таким образом, как шаблон агрегата и многофункциональная модель домена. Поэтому вы сами должны решить, стоит ли использовать шаблон репозитория в каждом конкретном случае.

Дополнительные ресурсы

Шаблон репозитория

Шаблон единицы работы