はじめに: モジュラーモノリスのデータベース
データベース設計は、モジュラー モノリスにおいて最も重要なアーキテクチャ上の決定の 1 つです。 各サービスが独自のデータベースを持つマイクロサービスとは異なり、モジュラーモノリスでは モジュールは同じプロセスを共有し、場合によっては同じ物理データベースを共有します。課題は を維持する データの所有権 データベースを活用しながらモジュールごとに モジュール間の ACID トランザクションなどの共有。
この記事では、次の 2 つの主なアプローチについて説明します。 共有スキーマ e モジュールの図、それぞれのトレードオフがあります。管理のパターンがわかります トランザクション、最終的な整合性、およびスキームから段階的に移行するための戦略 別々のスキームに共有されます。
この記事で学べること
- 共有スキーマとモジュールごとのスキーマ: 利点と欠点
- データの所有権: 国境の完全性を維持するためのルール
- プロセス内の ACID トランザクションと結果整合性
- 長時間実行トランザクションの Saga パターン
- 一貫性のための代替モデルとしてのイベント ソーシング
- データキャプチャを変更してモジュール間でデータを同期する
- 移行戦略: 共有から分離へ
- パフォーマンス: クロスコンテキストクエリの最適化
共有スキーマ: 1 つのデータベース、個別の論理スキーマ
アプローチでは 共有スキーマ、すべてのモジュールが同じデータベースを共有します 物理的かつ潜在的に同じパターン。ただし、各モジュールには独自のテーブルがあり、 プレフィックスまたは専用の PostgreSQL スキーマ。データへのアクセスはアプリケーション レベルで規制されています。 各モジュールは、独自のリポジトリを通じてのみ独自のテーブルにアクセスします。
共有スキーマの利点
- ACIDトランザクション: 単一のトランザクションでモジュール間の一貫性を確保できます。
- 操作の簡素化: 管理、監視、バックアップを行う単一のデータベース
- クロスモジュールクエリ: 必要に応じて JOIN を使用して可能 (レポート、分析用)
- 簡単な移行: 追加のインフラストラクチャは必要ありません
共有スキーマの欠点
- データ層の結合: 他のモジュールのテーブルに直接アクセスするリスク
- 制限されたスケーリング: データベースは単一のスケーリング ポイントです
- 進化スキーム: 移行はすべてのモジュールに影響を与える可能性があります
-- Shared schema con prefissi per modulo
-- Ogni modulo ha il proprio prefisso nelle tabelle
-- Modulo Order
CREATE TABLE order_orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL, -- riferimento, non FK esterna
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10,2),
total_currency VARCHAR(3),
created_at TIMESTAMP NOT NULL
);
CREATE TABLE order_items (
id UUID PRIMARY KEY,
order_id UUID REFERENCES order_orders(id),
product_id UUID NOT NULL, -- riferimento, non FK esterna
quantity INT NOT NULL,
unit_price DECIMAL(10,2)
);
-- Modulo Catalog
CREATE TABLE catalog_products (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
category_id UUID,
is_available BOOLEAN DEFAULT true
);
-- Modulo User
CREATE TABLE user_accounts (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(100),
created_at TIMESTAMP NOT NULL
);
-- NOTA: order_orders.user_id NON ha una FK verso user_accounts
-- Questo e intenzionale: i moduli comunicano via API, non via FK
データベースの黄金律
決して作成しないでください 異なるモジュールのテーブル間の外部キー。モジュール間の参照 ID (UUID) のみを使用します。モジュール間の参照の一貫性はアプリケーション レベルで管理されます。 データベースレベルではありません。これが抽出を可能にする妥協案です モジュールをマイクロサービスとして。
モジュールの図: 完全な分離
アプローチでは モジュールの図、各モジュールには独自の PostgreSQL スキーマがあります (または独自の論理データベース)。これにより、データ層での完全な分離が実現しますが、 モジュール間の一貫性を管理するための特定のパターン。
// Configurazione multi-schema in Spring Boot
// Ogni modulo ha il proprio schema PostgreSQL
// application.yml
// spring:
// datasource:
// url: jdbc:postgresql://localhost:5432/ecommerce
// Modulo Order: accede solo allo schema 'orders'
@Configuration
class OrderDatabaseConfig {
@Bean
public LocalContainerEntityManagerFactoryBean orderEntityManager(
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.ecommerce.order.internal");
Map<String, Object> props = new HashMap<>();
props.put(
"hibernate.default_schema", "orders"
);
em.setJpaPropertyMap(props);
return em;
}
}
// Modulo Catalog: accede solo allo schema 'catalog'
@Configuration
class CatalogDatabaseConfig {
@Bean
public LocalContainerEntityManagerFactoryBean catalogEntityManager(
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.ecommerce.catalog.internal");
Map<String, Object> props = new HashMap<>();
props.put(
"hibernate.default_schema", "catalog"
);
em.setJpaPropertyMap(props);
return em;
}
}
トランザクション管理
モジュラーモノリスでは、トランザクションに応じてさまざまなレベルでトランザクションを管理できます。 一貫性のニーズ:
モジュール内 ACID トランザクション
単一モジュール内では、ACID トランザクションは正常に動作します。シングル
注釈 @Transactional 原子性、一貫性、絶縁性、耐久性を保証します
すべてのモジュール操作に適用されます。
モジュール間の結果整合性
異なるモジュール間で一貫性が管理されます。 ドメインイベント。 モジュールはトランザクションを完了すると、イベントを発行します。他のモジュールが反応する 個別のトランザクションでイベントに参加し、保証する 最終的な整合性.
// Pattern: Transactional Outbox per garantire
// la pubblicazione affidabile degli eventi
@Entity
@Table(name = "order_outbox_events")
public class OutboxEvent {
@Id
private UUID id;
private String eventType;
private String payload; // JSON serializzato
private Instant createdAt;
private boolean processed;
}
@Service
class OrderServiceImpl implements OrderModuleApi {
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
// 1. Salva l'ordine
Order order = Order.create(cmd);
orderRepository.save(order);
// 2. Salva l'evento nella outbox table
// NELLA STESSA TRANSAZIONE dell'ordine
OutboxEvent event = new OutboxEvent(
UUID.randomUUID(),
"OrderCreated",
JsonUtil.toJson(new OrderCreatedEvent(
order.getId(), order.getUserId()
)),
Instant.now(),
false
);
outboxRepository.save(event);
return order.toDto();
// La transazione include sia l'ordine che l'evento
}
}
// Scheduler che processa gli eventi dalla outbox
@Scheduled(fixedDelay = 1000)
public void processOutboxEvents() {
List<OutboxEvent> events = outboxRepo.findUnprocessed();
for (OutboxEvent event : events) {
eventPublisher.publish(event.toEvent());
event.markProcessed();
outboxRepo.save(event);
}
}
佐賀パターン: 長時間実行トランザクション
Il サーガパターン 複数のモジュールにまたがるトランザクションをシーケンスとして処理します それぞれ独自のローカルトランザクションの 補償 失敗した場合。 ステップが失敗した場合、補償アクションによって前のステップが取り消されます。
// Saga: Creazione Ordine con compensazione
// Orchestration-based Saga
@Service
class CreateOrderSaga {
private final OrderModuleApi orderModule;
private final PaymentModuleApi paymentModule;
private final InventoryModuleApi inventoryModule;
public OrderDto execute(CreateOrderCommand cmd) {
OrderDto order = null;
PaymentDto payment = null;
try {
// Step 1: Crea ordine (stato PENDING)
order = orderModule.createOrder(cmd);
// Step 2: Riserva inventario
inventoryModule.reserveStock(
order.id(), order.items()
);
// Step 3: Processa pagamento
payment = paymentModule.processPayment(
order.userId(), order.total()
);
// Step 4: Conferma ordine
orderModule.confirmOrder(order.id());
return order;
} catch (PaymentFailedException e) {
// Compensazione: rilascia inventario
inventoryModule.releaseStock(order.id());
// Compensazione: annulla ordine
orderModule.cancelOrder(order.id());
throw new OrderCreationFailedException(e);
} catch (InsufficientStockException e) {
// Compensazione: annulla ordine
orderModule.cancelOrder(order.id());
throw new OrderCreationFailedException(e);
}
}
}
イベントソーシング: 代替モデル
L'イベントソーシング エンティティの状態が次から再構築されるパターンです。 by the sequence of events that changed it.現在の状態を保存するのではなく、自分自身を保存します。 すべてのイベント。このアプローチでは次のことが可能になります。
- 完全な監査証跡: すべての変更はイベントとして記録されます
- 国家の再建: いつでも状態を再構築できる可能性
- 自然な統合: イベントはモジュール間の通信にすでに利用可能です
- 複雑: クエリを効率的に行うには追加のパターン (CQRS、プロジェクション、スナップショット) が必要です
// Event Sourcing: l'ordine e ricostruito dagli eventi
public class OrderAggregate {
private UUID id;
private OrderStatus status;
private List<OrderItem> items;
private Money total;
// Ricostruisci lo stato dagli eventi
public static OrderAggregate rebuild(List<DomainEvent> events) {
OrderAggregate order = new OrderAggregate();
for (DomainEvent event : events) {
order.apply(event);
}
return order;
}
private void apply(DomainEvent event) {
if (event instanceof OrderCreated e) {
this.id = e.orderId();
this.status = OrderStatus.CREATED;
this.items = e.items();
} else if (event instanceof ItemAdded e) {
this.items.add(e.item());
this.recalculateTotal();
} else if (event instanceof OrderConfirmed e) {
this.status = OrderStatus.CONFIRMED;
} else if (event instanceof OrderCancelled e) {
this.status = OrderStatus.CANCELLED;
}
}
}
// Event Store: salva la sequenza di eventi
@Repository
class EventStore {
void append(UUID aggregateId, DomainEvent event);
List<DomainEvent> loadEvents(UUID aggregateId);
}
変更データキャプチャ (CDC)
Il 変更データキャプチャ データベースの変更をキャプチャする技術と、 イベントとして他のコンシューマに伝播します。のようなツール デベジウム 彼らはそれを読んだ データベースのトランザクション ログを作成し、INSERT、UPDATE、DELETE ごとにイベントを生成します。
モジュラーモノリスのコンテキストでは、CDC は次の場合に役立ちます。
- マテリアライズド・ビューを同期する モジュール間を直接結合せずに
- 読み取りモデルの投影をフィードする 効率的なモジュール間クエリのため
- 移行の準備をする: モジュールがマイクロサービスとしてチェックアウトされると、CDC は移行中にデータを同期できます
パフォーマンス: クロスコンテキストクエリ
モジュラー モノリスの課題の 1 つは、複数のモジュールからデータを要求するクエリを管理することです。 主な戦略は次のとおりです。
1. API構成
コンシューマは複数のモジュールの API を呼び出し、結果をメモリ内に作成します。シンプルだけど 大量のデータの場合は非効率になる可能性があります。
2. モデル/マテリアライズドビューの読み取り
レポートモジュールは次のことを維持します。 マテリアライズドビュー データを集約する さらに多くのモジュール。ビューはドメイン イベントを通じて更新されます。読み取りクエリは高速です データはすでに事前に集計されているためです。
3. CQRS (コマンドクエリ責任分離)
書き込みモデル (トランザクション用に最適化された) を読み取りモデル (最適化された) から分離します。 クエリごと)。コマンドはモジュール API を介して渡されます。クエリアクセスプロジェクション 非正規化はイベント経由で更新されます。
// CQRS: Read Model per dashboard ordini
// Aggiornato tramite eventi, denormalizzato per query veloci
@Entity
@Table(name = "reporting_order_summary")
public class OrderSummaryView {
@Id
private UUID orderId;
private String customerName; // dal modulo User
private String customerEmail; // dal modulo User
private int itemCount; // dal modulo Order
private BigDecimal totalAmount; // dal modulo Order
private String paymentStatus; // dal modulo Payment
private Instant createdAt;
}
// Handler che aggiorna il read model reagendo agli eventi
@EventListener
class OrderSummaryProjection {
void on(OrderCreatedEvent e) {
OrderSummaryView view = new OrderSummaryView();
view.setOrderId(e.orderId());
view.setItemCount(e.itemCount());
view.setTotalAmount(e.total());
view.setCreatedAt(e.timestamp());
summaryRepo.save(view);
}
void on(PaymentCompletedEvent e) {
OrderSummaryView view = summaryRepo.findById(e.orderId());
view.setPaymentStatus("COMPLETED");
summaryRepo.save(view);
}
}
移行戦略: 共有から分離へ
共有スキーマから始めて、将来的にスキーマを分離することにした場合は、次の 1 つのアプローチがあります。 増分的かつ安全:
- プレフィックスを追加する: モジュールごとのプレフィックスを使用してテーブルの名前を変更します (
order_,catalog_) - クロスモジュール FK を削除する: モジュール間の外部キーを ID による参照に置き換えます
- 別々のパターンを作成する: テーブルを専用の PostgreSQL スキーマに移動します
- 接続を更新する: 独自のスキーマにアクセスするように各モジュールを構成します
- 確認する: 統合テストを実行して、すべてが機能することを確認します。
次の記事
次の記事では、 コミュニケーションパターン モジュール間: 同期呼び出し、非同期メッセージ、インプロセス イベント バス、および完全な CQRS パターン。 Spring Boot で各パターンを実装する方法と、いつどちらかを選択するかを見ていきます。







