Разграничиваем Данные в Модульном Монолите. Окончание
Начало
Настройка схем EF и нескольких DbContext
- Используйте отдельные DbContext на каждый модуль. Установка схемы по умолчанию также влияет на последовательности и миграции.
- Укажите строку подключения для каждого модуля, используя роль модуля. Даже если модули используют одну БД, различные роли гарантируют, что неправильно настроенный контекст не сможет получить доступ к другой схеме.
- Настройте таблицу истории миграций в каждом контексте.
Пошаговая инструкция
Допустим, у нас два модуля (Orders и Shipping), и мы хотим установить границы между ними.
1. Создадим схемы и роли, предоставив каждой роли привилегии только на её схему:
-- Схема и роль Orders
CREATE ROLE orders_role LOGIN PASSWORD 'orders_secret';
CREATE SCHEMA orders AUTHORIZATION orders_role;
GRANT USAGE ON SCHEMA orders TO orders_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA orders TO orders_role;
ALTER ROLE orders_role SET search_path = orders;
Аналогично для роли Shipping.
2. Добавим строки подключения:
{
"ConnectionStrings": {
"Orders": "Host=…;Database=…;Username=orders_role;Password=orders_secret",
"Shipping": "…"
}
}3. Определим контексты для каждого модуля. Для Orders:
public class OrdersDbContext : DbContext
{
public DbSet<Order>
Orders { get; set; } = default!;
…
protected override void
OnModelCreating(ModelBuilder mb)
{
// схема по умолчанию
mb.HasDefaultSchema("orders");
// либо явно таблиц
mb.Entity<Order>().ToTable("orders");
…
base.OnModelCreating(mb);
}
}
4. Регистрируем каждый контекст со своей строкой подключения и указываем таблицу миграций:
builder.Services
.AddDbContext<OrdersDbContext>(opts =>
opts.UseNpgsql(
builder.Configuration.GetConnectionString("Orders"),
o => o.MigrationsHistoryTable("__EFMigrationsHistory", "orders")));
5. Управляем миграциями отдельно. При создании миграции указываем контекст:
dotnet ef migrations add InitialOrders --context OrdersDbContext --output-dir Data/Migrations/Orders
EF Core сгенерирует классы миграций, которые создадут таблицы в указанной схеме (благодаря HasDefaultSchema). Не забудьте применить миграции в правильном порядке при развёртывании. Вы можете автоматизировать этот процесс, используя средство выполнения миграций, итеративно перебирающее контексты.
Сквозные запросы
Даже в модульной системе иногда требуется экран, охватывающий несколько модулей, например, страница истории заказов, отображающая данные о заказе и доставке. Не поддавайтесь искушению использовать JOIN между схемами. Полезны два подхода:
1. Выделенная модель чтения. Один модуль владеет моделью представления и подписывается на события других. Этот шаблон хорошо работает, когда модули могут быть позже извлечены в микросервисы.
2. Представления в БД. Поскольку модули используют общую БД, мы можем создать в общедоступной схеме представление, доступное только для чтения, которое объединяет соответствующие таблицы. Мы предоставляем право SELECT для представления специальной роли или модулю. Это представление действует как управляемое общедоступное API. Потребители запрашивают представление, но не могут напрямую получить доступ к базовым таблицам. Однако, если впоследствии вы разделите БД, представление придётся заменить вызовом сервиса.
CREATE VIEW public.order_summary AS
SELECT o.id, o.total, s.status
FROM orders.orders o
JOIN shipping.shipments s ON s.order_id = o.id;
-- даём права чтения роли отчётов
GRANT SELECT ON public.order_summary TO reporting_role;
Это позволит создавать отчёты, не нарушая границ данных.
Источник: https://www.milanjovanovic.tech/blog/how-to-keep-your-data-boundaries-intact-in-a-modular-monolith