次の方法で共有


CQRS パターン

コマンド クエリ責任分離 (CQRS) は、データ ストアの読み取り操作と書き込み操作を別々のデータ モデルに分離する設計パターンです。 このアプローチにより、各モデルを個別に最適化し、アプリケーションのパフォーマンス、スケーラビリティ、セキュリティを向上させることができます。

コンテキストと問題

従来のアーキテクチャでは、読み取り操作と書き込み操作の両方に 1 つのデータ モデルがよく使用されます。 この方法は簡単で、基本的な作成、読み取り、更新、削除 (CRUD) 操作に適しています。

従来の CRUD アーキテクチャを示す図。

アプリケーションが拡大するにつれて、単一のデータ モデルで読み取りと書き込みの操作を最適化することがますます困難になる可能性があります。 多くの場合、読み取り操作と書き込み操作のパフォーマンスとスケーリングの要件は異なります。 従来の CRUD アーキテクチャでは、この非対称性は考慮されないため、次の課題が発生する可能性があります。

  • データの不一致: データの読み取りと書き込みの表現は、多くの場合異なります。 更新時に必要な一部のフィールドは、読み取り操作中に不要になる場合があります。

  • ロックの競合: 同じデータ セットに対する並列操作 ロックの競合を引き起こす可能性があります。

  • パフォーマンスの問題: 従来のアプローチは、データ ストアとデータ アクセス層への負荷、および情報の取得に必要なクエリの複雑さにより、パフォーマンスに悪影響を及ぼす可能性があります。

  • セキュリティの課題: エンティティが読み取り操作と書き込み操作の対象となる場合、セキュリティを管理するのは困難な場合があります。 この重複により、意図しないコンテキストでデータが公開される可能性があります。

これらの責任を組み合わせると、モデルが過度に複雑になる可能性があります。

解決策

CQRS パターンを使用して、書き込み操作または コマンドを読み取り操作またはクエリから分離 します。 コマンドによってデータが更新されます。 クエリはデータを取得します。 CQRS パターンは、コマンドと読み取りを明確に分離する必要があるシナリオで役立ちます。

  • コマンドについて説明します。 コマンドは、低レベルのデータ更新ではなく、特定のビジネス タスクを表す必要があります。 たとえば、ホテル予約アプリでは、"ReservationStatus を予約済みに設定する" の代わりにコマンド "Book hotel room" を使用します。この方法では、ユーザーの意図をより適切にキャプチャし、コマンドをビジネス プロセスに合わせます。 コマンドが正常に実行されるようにするには、ユーザーの操作フローとサーバー側のロジックを調整し、非同期処理を検討することが必要になる場合があります。

    絞り込みの領域 勧告
    クライアント側の検証 明確なエラーを防ぐために、コマンドを送信する前に特定の条件を検証します。 たとえば、会議室がない場合は、[予約] ボタンを無効にして、予約できない理由を説明するわかりやすいわかりやすいメッセージを UI に表示します。 このセットアップにより、不要なサーバー要求が減り、ユーザーにすぐにフィードバックが提供され、エクスペリエンスが向上します。
    サーバー側のロジック エッジ ケースと障害を適切に処理するようにビジネス ロジックを強化します。 たとえば、複数のユーザーが最後に利用可能な部屋を予約しようとするなどの競合状態に対処するには、待機リストにユーザーを追加するか、代替候補を提案することを検討してください。
    非同期処理 コマンドを同期的に 処理するのではなく、キューに配置して非同期的に処理します。
  • クエリについて説明します。 クエリによってデータが変更されることはありません。 代わりに、ドメイン ロジックを使用せずに、必要なデータを便利な形式で提示するデータ転送オブジェクト (DTO) を返します。 この個別の責任の分離により、システムの設計と実装が簡素化されます。

読み取りモデルと書き込みモデルを分離する

読み取りモデルを書き込みモデルから分離すると、データ書き込みとデータ読み取りに関する特定の問題に対処することで、システムの設計と実装が簡素化されます。 この分離により、明確さ、スケーラビリティ、パフォーマンスが向上しますが、トレードオフが生じます。 たとえば、オブジェクト リレーショナル マッピング (O/RM) フレームワークなどのスキャフォールディング ツールでは、データベース スキーマから CQRS コードを自動的に生成できないため、ギャップを埋めるためにカスタム ロジックが必要です。

次のセクションでは、CQRS で読み取りモデルと書き込みモデルの分離を実装するための 2 つの主要なアプローチについて説明します。 各アプローチには、同期や整合性管理など、独自の利点と課題があります。

1 つのデータ ストアでモデルを分離する

このアプローチは CQRS の基本レベルを表します。読み取りモデルと書き込みモデルの両方が 1 つの基になるデータベースを共有しますが、その操作に対して個別のロジックを保持します。 基本的な CQRS アーキテクチャを使用すると、共有データ ストアに依存しながら、読み取りモデルから書き込みモデルを説明できます。

基本的な CQRS アーキテクチャを示す図。

このアプローチでは、読み取りと書き込みの問題を処理するための個別のモデルを定義することで、明確さ、パフォーマンス、スケーラビリティが向上します。

  • 書き込みモデル は、データを更新または永続化するコマンドを処理するように設計されています。 検証とドメイン ロジックが含まれており、トランザクションの整合性とビジネス プロセス用に最適化することで、データの一貫性を確保するのに役立ちます。

  • 読み取りモデル は、データを取得するためのクエリを提供するように設計されています。 プレゼンテーション レイヤー用に最適化された DTO またはプロジェクションの生成に重点を置いています。 ドメイン ロジックを回避することで、クエリのパフォーマンスと応答性が向上します。

異なるデータ ストアでモデルを分離する

より高度な CQRS 実装では、読み取りモデルと書き込みモデルに個別のデータ ストアが使用されます。 読み取りデータ ストアと書き込みデータ ストアを分離することで、負荷に合わせて各モデルをスケーリングできます。 また、データ ストアごとに異なるストレージ テクノロジを使用することもできます。 読み取りデータ ストアにはドキュメント データベースを、書き込みデータ ストアにはリレーショナル データベースを使用できます。

個別の読み取りデータ ストアと書き込みデータ ストアを備えた CQRS アーキテクチャを示す図。

個別のデータ ストアを使用する場合は、両方が確実に同期されるようにする必要があります。 一般的なパターンは、読み取りモデルがデータの更新に使用するデータベースを更新するときに、書き込みモデルでイベントを発行することです。 イベントの使用方法の詳細については、「 イベント ドリブン アーキテクチャ スタイル」を参照してください。 通常、メッセージ ブローカーとデータベースを 1 つの分散トランザクションに参加させることはできないため、データベースの更新とイベントの発行時に一貫性の問題が発生する可能性があります。 詳細については、「Idempotent message processing」を参照してください。

読み取りデータ ストアでは、クエリ用に最適化された独自のデータ スキーマを使用できます。 たとえば、複雑な結合や O/RM マッピングを回避するために、データの マテリアライズド ビュー を格納できます。 読み取りデータ ストアは、書き込みストアの読み取り専用レプリカにすることも、異なる構造にすることもできます。 複数の読み取り専用レプリカをデプロイすると、待機時間を短縮し、可用性を向上させることで、特に分散シナリオでパフォーマンスを向上させることができます。

CQRS の利点

  • 独立したスケーリング。 CQRS を使用すると、読み取りモデルと書き込みモデルを個別にスケーリングできます。 このアプローチはロック競合を最小限に抑え、負荷のかかった状態でシステムのパフォーマンスを向上させるのに役立ちます。

  • 最適化されたデータ スキーマ。 読み取り操作では、クエリ用に最適化されたスキーマを使用できます。 書き込み操作では、更新プログラム用に最適化されたスキーマを使用します。

  • セキュリティ。 読み取りと書き込みを分離することで、適切なドメイン エンティティまたは操作のみがデータに対して書き込みアクションを実行するアクセス許可を持っていることを確認できます。

  • 懸念事項の分離。 読み取りと書き込みの責任を分離すると、よりクリーンで保守しやすいモデルになります。 通常、書き込み側は複雑なビジネス ロジックを処理します。 読み取り側は単純なままで、クエリの効率に重点を置くことができます。

  • より単純なクエリ。 具体化されたビューを読み取りデータベースに格納すると、アプリケーションがクエリを実行するときに複雑な結合を回避できます。

問題と考慮事項

このパターンを実装する方法を決定するときは、次の点を考慮してください。

  • 複雑さが増しました。 CQRS の主要な概念は簡単ですが、特に イベント ソーシング パターンと組み合わせると、アプリケーション設計に大きな複雑さが生じる可能性があります。

  • メッセージングの課題。 メッセージングは CQRS の要件ではありませんが、コマンドの処理や更新イベントの発行によく使用されます。 メッセージングが含まれている場合、システムは、メッセージの失敗、重複、再試行などの潜在的な問題を考慮する必要があります。 優先順位が異なるコマンドを処理する方法の詳細については、「 優先順位キュー」を参照してください。

  • 最終的な一貫性。 読み取りデータベースと書き込みデータベースが分離されている場合、読み取りデータに最新の変更がすぐに表示されないことがあります。 この遅延により、古いデータが発生します。 読み取りモデル ストアが書き込みモデル ストアの変更に伴って最新の状態を維持されるのは困難な場合があります。 また、ユーザーが古いデータに対して行動するシナリオを検出して処理するには、慎重に検討する必要があります。

このパターンを使用する場合

このパターンは次の状況で使用します。

  • 共同作業環境で作業します。 複数のユーザーが同じデータに同時にアクセスして変更する環境では、CQRS はマージの競合を減らすのに役立ちます。 コマンドには、競合を防ぐのに十分な粒度を含めることができます。また、コマンド ロジック内で発生した競合をシステムが解決できます。

  • タスク ベースのユーザー インターフェイスがあります。 一連の手順として、または複雑なドメイン モデルを使用して複雑なプロセスをユーザーにガイドするアプリケーションは、CQRS の利点があります。

    • 書き込みモデルには、ビジネス ロジック、入力検証、およびビジネス検証を含む完全なコマンド処理スタックがあります。 書き込みモデルでは、関連付けられている一連のオブジェクトをデータ変更の単一の単位として扱う場合があります。これは、ドメイン駆動型の設計用語の 集計 と呼ばれます。 書き込みモデルは、これらのオブジェクトが常に一貫性のある状態であることを確認するのにも役立ちます。

    • 読み取りモデルには、ビジネス ロジックまたは検証スタックがありません。 ビュー モデルで使用する DTO を返します。 読み取りモデルは、最終的には書き込みモデルと一致します。

  • パフォーマンスチューニングが必要です。 データ読み取りのパフォーマンスを、データ書き込みのパフォーマンスとは別に微調整する必要があるシステムでは、CQRS の利点があります。 このパターンは、読み取りの数が書き込みの数を超える場合に特に便利です。 読み取りモデルは、大規模なクエリ ボリュームを処理するために水平方向にスケーリングします。 書き込みモデルは、マージの競合を最小限に抑え、一貫性を維持するために、実行されるインスタンスが少なくなります。

  • 開発上の懸念事項は分離されています。 CQRS を使用すると、チームは個別に作業できます。 あるチームが書き込みモデルに複雑なビジネス ロジックを実装し、別のチームが読み取りモデルとユーザー インターフェイス コンポーネントを開発します。

  • あなたは進化するシステムを持っています。 CQRS は、時間の経過と共に進化するシステムをサポートします。 既存の機能に影響を与えずに、新しいモデル バージョン、ビジネス ルールの頻繁な変更、またはその他の変更に対応します。

  • システム統合が必要です。 他のサブシステム (特にイベント ソーシング パターンを使用するシステム) と統合されたシステムは、サブシステムが一時的に障害が発生した場合でも引き続き使用できます。 CQRS は障害を分離するため、1 つのコンポーネントがシステム全体に影響を与えるのを防ぎます。

このパターンは、次の場合に適さない場合があります。

  • ドメインやビジネス ルールが単純である。

  • 単純な CRUD スタイルのユーザー インターフェイスとデータ アクセス操作で十分である。

ワークロード設計

ワークロードの設計で CQRS パターンを使用して、 Azure Well-Architected Framework の柱で説明されている目標と原則に対処する方法を評価します。 次の表は、このパターンがパフォーマンス効率の柱の目標をサポートする方法に関するガイダンスを示しています。

このパターンが柱の目標をサポートする方法
パフォーマンス効率は、 スケーリング、データ、およびコードの最適化によってワークロードが効率的に需要を満たすのに役立ちます。 高い読み取り/書き込みワークロードで読み取り操作と書き込み操作を分離することで、各操作の特定の目的に合わせて、対象となるパフォーマンスとスケーリングの最適化が可能になります。

- PE:05 スケーリングとパーティショニング
- PE:08 データパフォーマンス

このパターンが導入する可能性がある他の柱の目標に対するトレードオフを検討してください。

イベント ソーシングと CQRS パターンの組み合わせ

CQRS の一部の実装には 、イベント ソーシング パターンが組み込まれています。 このパターンは、システムの状態を時系列の一連のイベントとして格納します。 各イベントは、特定の時刻にデータに加えられた変更をキャプチャします。 現在の状態を判断するために、システムはこれらのイベントを順番に再生します。 このセットアップで行われる操作は以下の通りです。

  • イベント ストアは、書き込みモデル であり、信頼の単一のソースです。

  • 読み取りモデル は、これらのイベントから具体化されたビューを生成します。通常は、高度に非正規化された形式です。 これらのビューは、クエリと表示の要件に合わせて構造を調整することで、データ取得を最適化します。

イベント ソーシングと CQRS パターンの組み合わせの利点

書き込みモデルを更新するのと同じイベントが、読み取りモデルへの入力として機能します。 読み取りモデルでは、現在の状態のリアルタイム スナップショットを作成できます。 これらのスナップショットは、データの効率的で事前計算されたビューを提供することで、クエリを最適化します。

システムは、現在の状態を直接格納する代わりに、イベントのストリームを書き込みストアとして使用します。 この方法により、集計での更新の競合が軽減され、パフォーマンスとスケーラビリティが向上します。 システムは、これらのイベントを非同期的に処理して、読み取りデータ ストアの具体化されたビューをビルドまたは更新できます。

イベント ストアは単一の信頼できるソースとして機能するため、履歴イベントを再生することで、具体化されたビューを簡単に再生成したり、読み取りモデルの変更に適応したりできます。 基本的に、具体化されたビューは、高速で効率的なクエリ用に最適化された永続的な読み取り専用キャッシュとして機能します。

イベント ソーシングと CQRS パターンを組み合わせる方法に関する考慮事項

CQRS パターンと イベント ソーシング パターンを組み合わせる前に、次の考慮事項を評価します。

  • 最終的な整合性: 書き込みデータ ストアと読み取りデータ ストアは別々であるため、読み取りデータ ストアの更新はイベント生成に遅れる可能性があります。 この遅延により、最終的な整合性が得られます。

  • 複雑さの増加: CQRS パターンとイベント ソーシング パターンを組み合わせるには、別の設計アプローチが必要です。これは、実装の成功をより困難にする可能性があります。 イベントを生成、処理、処理し、読み取りモデルのビューをアセンブルまたは更新するコードを記述する必要があります。 ただし、イベント ソーシング パターンを使用すると、ドメイン モデリングが簡略化され、すべてのデータ変更の履歴と意図を保持することで、新しいビューを簡単に再構築または作成できます。

  • ビュー生成のパフォーマンス: 読み取りモデルの具体化されたビューを生成すると、時間とリソースが大幅に消費される可能性があります。 特定のエンティティまたはコレクションのイベントを再生して処理することで、データを投影する場合にも同じことが当てはまります。 関連するすべてのイベントを調べる必要があるため、計算に長期間にわたって値を分析または合計する必要がある場合、複雑さが増します。 データのスナップショットを一定の間隔で実装します。 たとえば、エンティティの現在の状態や集計された合計の定期的なスナップショット (特定のアクションが発生した回数) を格納します。 スナップショットを使用すると、イベント履歴全体を繰り返し処理する必要が減り、パフォーマンスが向上します。

次のコードは、読み取りモデルと書き込みモデルに異なる定義を使用する CQRS 実装の例からの抜粋を示しています。 モデル インターフェイスは、基になるデータ ストアの機能を決定するわけではありません。また、これらのインターフェイスは独立しているため、進化し、個別に微調整できます。

次のコードは、読み取りモデルの定義を示しています。

// Query interface
namespace ReadModel
{
  public interface ProductsDao
  {
    ProductDisplay FindById(int productId);
    ICollection<ProductDisplay> FindByName(string name);
    ICollection<ProductInventory> FindOutOfStockProducts();
    ICollection<ProductDisplay> FindRelatedProducts(int productId);
  }

  public class ProductDisplay
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal UnitPrice { get; set; }
    public bool IsOutOfStock { get; set; }
    public double UserRating { get; set; }
  }

  public class ProductInventory
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int CurrentStock { get; set; }
  }
}

ユーザーは製品を評価することができます。 アプリケーション コードでは、次のコードに示す RateProduct コマンドを使用してこれを行います。

public interface ICommand
{
  Guid Id { get; }
}

public class RateProduct : ICommand
{
  public RateProduct()
  {
    this.Id = Guid.NewGuid();
  }
  public Guid Id { get; set; }
  public int ProductId { get; set; }
  public int Rating { get; set; }
  public int UserId {get; set; }
}

システムは、 ProductsCommandHandler クラスを使用して、アプリケーションが送信するコマンドを処理します。 クライアントは通常、キューなどのメッセージング システムを使用して、ドメインにコマンドを送信します。 コマンド ハンドラーはこれらのコマンドを受け入れ、ドメイン インターフェイスのメソッドを呼び出します。 各コマンドの細分性は、要求の競合が発生する可能性が少なくなるように設計されています。 次のコードは、ProductsCommandHandler クラスのアウトラインを示しています。

public class ProductsCommandHandler :
    ICommandHandler<AddNewProduct>,
    ICommandHandler<RateProduct>,
    ICommandHandler<AddToInventory>,
    ICommandHandler<ConfirmItemShipped>,
    ICommandHandler<UpdateStockFromInventoryRecount>
{
  private readonly IRepository<Product> repository;

  public ProductsCommandHandler (IRepository<Product> repository)
  {
    this.repository = repository;
  }

  void Handle (AddNewProduct command)
  {
    ...
  }

  void Handle (RateProduct command)
  {
    var product = repository.Find(command.ProductId);
    if (product != null)
    {
      product.RateProduct(command.UserId, command.Rating);
      repository.Save(product);
    }
  }

  void Handle (AddToInventory command)
  {
    ...
  }

  void Handle (ConfirmItemsShipped command)
  {
    ...
  }

  void Handle (UpdateStockFromInventoryRecount command)
  {
    ...
  }
}

次のステップ

このパターンを実装する場合、次の情報が関連する場合があります。

  • データパーティション分割のガイダンス では、スケーラビリティの向上、競合の削減、パフォーマンスの最適化のために個別に管理およびアクセスできるパーティションにデータを分割する方法のベスト プラクティスについて説明します。
  • イベント ソーシング パターン。 このパターンでは、複雑なドメインのタスクを簡略化し、パフォーマンス、スケーラビリティ、応答性を向上させる方法について説明します。 また、補正アクションを有効にできる完全な監査証跡と履歴を維持しながら、トランザクション データの整合性を提供する方法についても説明します。

  • Materialized View Pattern (具体化されたビュー パターン) このパターンでは、1 つ以上のデータ ストアからの効率的なクエリとデータ抽出のために、 事前設定されたビュー (具体化されたビューと呼ばれます) が作成されます。 CQRS 実装の読み取りモデルには、書き込みモデル データの具体化されたビューを含めることができます。また、読み取りモデルは具体化されたビューの生成に使用できます。