編集

次の方法で共有


CQRS パターン

Azure Storage

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

コンテキストと問題

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

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

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

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

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

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

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

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

解決策

CQRS パターンを使用して、書き込み操作 (コマンド) と読み取り操作 (クエリ) を分離します。 コマンドは、データの更新を担当します。 クエリは、データの取得を担当します。

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

絞り込みの領域 勧告
クライアント側の検証 明確なエラーを防ぐために、コマンドを送信する前に特定の条件を検証します。 たとえば、会議室がない場合は、[予約] ボタンを無効にして、予約できない理由を説明するわかりやすいわかりやすいメッセージを UI に表示します。 このセットアップにより、不要なサーバー要求が減り、ユーザーにすぐにフィードバックが提供され、エクスペリエンスが向上します。
サーバー側のロジック エッジ ケースと障害を適切に処理するようにビジネス ロジックを強化します。 たとえば、競合状態 (使用可能な最後の会議室を予約しようとしている複数のユーザー) に対処するには、待機リストにユーザーを追加するか、代替オプションを提案することを検討してください。
非同期処理 コマンドを同期的に処理するのではなく、キューに配置することで、 を非同期的に処理 することもできます。

クエリについて説明します。 クエリによってデータが変更されることはありません。 代わりに、ドメイン ロジックなしで、必要なデータを便利な形式で提示するデータ転送オブジェクト (DTO) を返します。 この明確な懸念事項の分離により、システムの設計と実装が簡略化されます。

読み取りと書き込みのモデルの分離について

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

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

1 つのデータ ストア内のモデルの分離

このアプローチは CQRS の基本レベルを表します。読み取りモデルと書き込みモデルの両方が 1 つの基になるデータベースを共有しますが、その操作に対して個別のロジックを保持します。 個別の懸念事項を定義することで、この戦略はシンプルさを高めると同時に、一般的なユース ケースのスケーラビリティとパフォーマンスの利点を提供します。 基本的な CQRS アーキテクチャでは、共有データ ストアに依存しながら、読み取りモデルから書き込みモデルを説明できます (図 2を参照)。

基本的な CQRS アーキテクチャを示す図。
図 2. 1 つのデータ ストアを備えた基本的な CQRS アーキテクチャ。

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

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

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

個別のデータ ストアでのモデルの物理的な分離

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

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

個別のデータ ストアの同期: 個別のストアを使用する場合は、両方が同期状態であることを確認する必要があります。一般的なパターンは、読み取りモデルがデータの更新に使用するデータベースを更新するたびに、書き込みモデルでイベントを発行することです。 イベントの使用方法の詳細については、「イベント ドリブン アーキテクチャ スタイルの」を参照してください。 ただし、通常、メッセージ ブローカーとデータベースを 1 つの分散トランザクションに参加させることはありません。 そのため、データベースの更新とイベントの発行時に一貫性を保証する際に課題が生じ得る場合があります。 詳細については、べき等メッセージ処理参照してください。

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

CQRS の利点

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

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

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

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

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

実装に関する問題と注意事項

このパターンの実装には、次のような課題があります。

  • 複雑さの増加. CQRS の主要な概念は簡単ですが、特に イベント ソーシング パターンと組み合わせると、アプリケーションの設計が大幅に複雑

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

  • 最終的な一貫性。 読み取りデータベースと書き込みデータベースが分離されると、読み取りデータに最新の変更がすぐに反映されず、古いデータが発生する可能性があります。 読み取りモデル ストアが書き込みモデル ストアの変更に伴 up-to日付のままであることを確認することは困難な場合があります。 さらに、ユーザーが古いデータに対して行動するシナリオを検出して処理するには、慎重に検討する必要があります。

CQRS パターンを使用する状況

CQRS パターンは、データ変更 (コマンド) とデータ クエリ (読み取り) を明確に分離する必要があるシナリオで役立ちます。 次の状況で CQRS を使用することを検討してください。

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

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

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

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

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

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

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

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

CQRS を使用しない場合

次の状況では CQRS を避けてください。

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

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

ワークロード設計

アーキテクトは、ワークロードの設計で CQRS パターンを使用して、Azure Well-Architected Framework の柱で説明されている目標と原則に対処する方法を評価する必要があります。 次に例を示します。

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

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

設計決定と同様に、このパターンで導入される可能性のある他の柱の目標とのトレードオフを考慮してください。

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

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)
  {
    ...
  }
}

次のステップ

このパターンを実装する場合、次のパターンとガイダンスが役に立ちます。

  • イベント ソーシング パターン。 CQRS パターンでイベント ソーシングを使用する方法について説明します。 ここでは、パフォーマンス、スケーラビリティ、応答性を向上させながら、複雑なドメインのタスクを簡略化する方法を示します。 また、補正アクションを有効にできる完全な監査証跡と履歴を維持しながら、トランザクション データの整合性を提供する方法についても説明します。

  • Materialized View Pattern (具体化されたビュー パターン) CQRS 実装の読み取りモデルには、書き込みモデル データの具体化されたビューを含めることができます。また、読み取りモデルは具体化されたビューの生成に使用できます。