March 2016

Volume 31 Number 3

Cutting Edge - CQRS アーキテクチャのクエリ スタック

Dino Esposito | March 2016

Dino Espositoコマンド クエリ責務分離 (CQRS: Command Query Responsibility Segregation) は、アーキテクチャに関して現在特に話題になっているトピックの 1 つです。CQRS の中心にあるのはごく常識的なことで、システムに必要なクエリとコマンドを、個別のアドホックなスタックを利用してコーディングするというものです。つまり、コマンド スタックにドメイン層を用意し、ビジネスの各タスクをドメインのサービスに編成して、ビジネスの各ルールをドメインのオブジェクトにパッケージ化します。永続化層も、ニーズに合った最適なテクノロジとアプローチを選択するだけで、すべて自由にコーディングできます。プレーンなリレーショナル テーブル、NoSQL、イベント ストアのいずれを選択してもかまいません。

読み取りスタックについてはどうでしょう。CQRS アーキテクチャに着手するとわかるのですが、多くの場合、データを読み取るためだけにドメイン層に個別のセクションを設ける必要性を感じません。必要なのは、既存のテーブルからデータを読み取るプレーンなクエリだけです。クエリ スタックは、読み取り専用の操作をバックエンドに対して実行し、システムの状態を変更しません。このため、ビジネス ルールや、プレゼンテーション層とストレージ間の論理的な介入は一切必要ありません。少なくとも、SQL の高度なクエリ演算子の基本機能 (GROUP BY、JOIN、ORDER など) を上回る機能は不要です。最新のクエリ スタックを実装する場合、Microsoft .NET Framework でサポートされる LINQ 言語が非常に便利です。今回は、プレゼンテーション層が必要とするデータ編成に近づくようにストレージを設計した、読み取りスタック実装の可能性について取り上げます。

既にデータが用意されている状況

通常、CQRS プロジェクトでは、クエリ スタックとコマンド スタックにはそれぞれ別のプロジェクトを計画します。スタックごとに対応するチームを分けることも可能です。読み取りスタックには基本的に 2 つの構成要素があります。1 つはデータ転送オブジェクト (DTO) のコレクションで、プレゼンテーション層にデータを提供します。もう 1 つはデータベース コンテキストで、実際の読み取りを実行して読み取ったデータを DTO に設定します。

クエリ スタックの無駄をできるだけなくすには、格納されているデータと表示するデータをなるべく近づけるようにします。ですが、最近は特に、このようなアプローチが難しくなっています。最近では、保存に適した形式で格納されているデータを、効率よく利用するために、大きく異なる形式に調整しなければならない場面が増えています。一例として、ビジネス環境で会議室を予約するソフトウェア システムを考えてみます。図 1 は、このシナリオを図示したものです。

CQRS アーキテクチャでの予約スキーマ
図 1 CQRS アーキテクチャでの予約スキーマ

ユーザーには、予約申請の作成や、既存予約の更新を行うフォームを数ページ提供します。各操作は、コマンド データ ストアにそのままログ記録されます。ログに記録した予約イベントの一覧をスクロールすれば、各予約のシステムへの入力時、予約変更時、予約の変更回数、予約の削除時を簡単に追跡できます。ログ記録は、保存する項目の状態が時々刻々変化する動的なシステムに関する情報を保存する最も効果的な方法です。

2015 年 8 月号の Cutting Edge コラム (msdn.com/magazine/mt185569) で取り上げたように、システムの状態をイベント形式で保存することで、多くのメリットを得られます。しかし、この場合、システムの現在状態を即座に表示することができません。つまり、全履歴をさかのぼれば 1 件の予約を見つけることはできますが、保留中の予約の一覧を即座に返すことはできません。むしろ、システムのユーザーは、予約の一覧を取得しようとするのが一般的です。そのためには、2 つの個別のストアを同期した状態に保つ必要があります。クエリを容易にするには、新しいイベントをログに記録するたびに、関連するエンティティの状態を (同期または非同期に) 更新することになります。

同期手順の実行中に、必要なデータをイベントのログから抜き出し、クエリ スタックを通じて UI から利用しやすい形式に変換します。あとは、現在のビューの特定のデータ モデルに合わせてデータの形式を整えるだけです。

イベントの役割について

「なぜ、データをイベント形式で保存するのか。なぜ、使用する形式を考えて保存しないのか」といった意見をよく聞きます。

「場合によりけり」といつも答えています。興味深いことに、保存形式を決めるのは、通常アーキテクトではありません。決めるのは、ビジネス ニーズです。利用者にとって重要なことがビジネス項目の履歴 (予約) の追跡や、特定の日付の予約一覧の確認で、かつ会議室の空き状況が時々刻々と変化するのであれば、各イベントをログに記録し、それを基に必要なデータ プロジェクションを構築するのが、問題を解決する最もシンプルな方法になります。

ちなみに、これはビジネス インテリジェンス (BI) のサービスやアプリケーションで使用される内部メカニズムです。したがって、イベントを使用すれば、社内 BI を基盤とすることも可能です。

Entity Framework の読み取り専用コンテキスト

Entity Framework は、(少なくとも .NET アプリケーションや ASP.NET アプリケーション内から) 格納済みのデータにアクセスする一般的な方法で、データベース呼び出しに DbContext クラスのインスタンスを利用します。DbContext クラスは、基盤となるデータベースへの読み取り/書き込みアクセスを提供します。最上位層から DbContext のインスタンスを認識できるようにすると、アーキテクチャがクエリ スタック内からも状態の更新を受け取るおそれがあります。そのこと自体はバグではありませんが、アーキテクチャに関するルールの深刻な違反であり、避けるべき事態です。データベースへの書き込みアクセスを回避するには、破棄可能なコンテナー クラスに DbContext インスタンスをラップします (図 2 参照)。

図 2 破棄可能なコンテナー クラスへの DbContext インスタンスのラップ

public class Database : IDisposable
{
  private readonly SomeDbContext _context = new SomeDbContext();
  public IQueryable<YourEntity> YourEntities
  {
    get
    {
      return _context.YourEntities;
    }
  }
  public void Dispose()
  {
    _context.Dispose();
  }
}

この Database クラスは、データのクエリのみを目的に、DbContext を使用する場面であればどこでも使用できます。Database クラスは、プレーンな DbContext とは 2 つの面で一線を画しています。何よりもまず、Database クラスでは DbContext インスタンスをプライベート メンバーとしてカプセル化しています。2 つ目に、すべてまたは一部の DbContext コレクションを、DbSet<T> コレクションではなく IQueryable<T> コレクションとして公開しています。こうすれば、追加、削除、変更の保存をできないようにして、LINQ のクエリ機能を利用できます。

ビュー用のデータの整形

アプリケーション層の編成に関して、CQRS アーキテクチャには基準となる考え方はありません。コマンド スタックとクエリ スタックに別の層を用意しても、1 つの一意層を使用してもかまいません。多くの場合、その決定はアーキテクトのビジョンに委ねられます。それぞれ別のストレージを二重に管理し、プレゼンテーション用に調整したデータを用意できるのであれば、複雑なデータ取得ロジックを使用する必要はほとんどありません。そうすれば、クエリ スタックの実装も最低限の作業ですみます。アプリケーション層コードのクエリ部分では、データに直接アクセスするコードを用意し、Entity Framework DbContext エントリ ポイントを直接呼び出すことができます。ただし、クエリ操作に限定するため、ネイティブの DbContext ではなく、Database クラスを使用することになります。Database クラスから返されるデータをクエリするには、LINQ を使用するしかありません。図 3 に例を示します。

図 3 LINQ による Database クラスのクエリ

using (var db = new Database())
{
  var queryable = from i in db.Invoices
                              .Include("Customers")
    where i.InvoiceId == invoiceId
    select new InvoiceFoundViewModel
    {
      Id = i.InvoiceId,
      State = i.State.ToString(),
      Total = i.Total,
      Date = i.IssueDate,
      ExpiryDate = i.GetExpiryDate()
    };
    // More code here
}

ご覧のように、クエリから取得するデータの実際のプロジェクションは、最後のアプリケーション層で指定します。つまり、インフラストラクチャ層のどこかの時点で IQueryable オブジェクトを作成し、複数の層間を移動します。どの層でも、クエリを実際に実行することなく、クエリを調整して、オブジェクトに変更を加えるチャンスがあります。その結果、データを複数の層間で移動するために転送オブジェクトを大量に作成する必要がなくなります。

かなり複雑なクエリについて考えてみましょう。アプリケーションのあるユース ケースで、支払い期限から 30 日間が経過しても支払いが行われていないビジネス部門の請求書を、すべて取得する必要があるとします。クエリが表現するビジネス ニーズが安定的で統一されていて、この先改定されることがなければ、プレーンな T-SQL で記述するのはそれほど難しくありません。このクエリは 3 つの主要部分から構成されます。すべての請求書を取得する部分が 1 つ、特定のビジネス部門の請求書を選び出す部分が 1 つ、30 日が経過しても支払いが実行されていない請求書を選び出す部分が 1 つです。実装の点では、元のクエリを 3 つのサブクエリに分割して、ほとんどの操作をメモリ内で処理することにあまり意味はありません。ただし、概念的な面では、クエリを 3 つに分割することで、特にその領域に詳しくない人にとっては、はるかに理解しやすいクエリになります。

LINQ は、クエリを概念的に表現して 1 つの手順で実行できる魔法のようなツールです (適切なクエリ言語への変換は、基盤となる LINQ プロバイダーが対応します)。以下は、LINQ クエリの表現方法の一例です。

var queryable = from i in db.Invoices
                            .Include("Customers")
  where i.BusinessUnitId == buId &&
     DateTime.Now – i.PaymentDueBy > 30
                select i;

ただし、この表現が返すのはデータではありません。単に、実行前のクエリを返します。クエリを実行して関連するデータを取得するには、ToList や FirstOrDefault などの LINQ 実行ツールを呼び出す必要があります。その結果初めてクエリが構築され、すべての部分が組み合わせられて、データが返されます。

LINQ とユビキタス言語

IQueryable オブジェクトは、後で変更を加えることが可能です。これを行うには、Where メソッドと Select メソッドをプログラムから呼び出します。これにより、IQueryable オブジェクトがシステムの各層を移動するにつれて、フィルターの作成を経て最終的なクエリへと進化します。さらに重要なのは、各フィルターが適用されるのは、基盤とするロジックを使用できる場合のみであるという点です。

CQRS アーキテクチャのクエリ スタックで、LINQ オブジェクトと IQueryable オブジェクトについてもう 1 つ取り上げるべき点が、読みやすさとユビキタス言語です。ドメイン駆動設計 (DDD) におけるユビキタス言語とは、明確な業務用語の語い (名詞と動詞) の作成と管理のほか、さらに重要となる実際のコードへの反映を提案するパターンです。たとえば、ユビキタス言語を実装するソフトウェア システムでは、注文を削除するのではなく「キャンセル」し、注文を送信するのではなく「チェックアウト」します。

最新のソフトウェアが抱える最大の課題は、技術ソリューションではなく、ビジネス ニーズを細かく理解して、ビジネス ニーズとコードの最適な組み合わせを見つけることです。クエリを読みやすくするには、IQueryable オブジェクトと C# の拡張メソッドを併せて使用します。先ほどのクエリを書き直して、大幅に読みやすくしたものを以下に示します。

var queryable = from i in db.Invoices
                            .Include("Customers")
                            .ForBusinessUnit(buId)
                            .Unpaid(30)
                select i;

ForBusinessUnit と Unpaid の 2 つは、IQueryable<Invoice> 型を拡張するユーザー定義の拡張メソッドです。これらのメソッドでは、実行中のクエリの定義に WHERE 句を追加します。

public static IQueryable<Invoice> ForBusinessUnit(
  this IQueryable<Invoice> query, int businessUnitId)
{
  var invoices =
    from i in query
    where i.BusinessUnit.OrganizationID == buId
    select i;
  return invoices;}

同様に Unpaid メソッドも、返されるデータをさらに制限するもう 1 つの WHERE 句のみで構成されます。最終的なクエリは同じですが、クエリの式ははるかに明確になり、誤認識が発生しにくくなります。つまり、拡張メソッドを使用することで、ドメイン固有の言語を手に入れたも同然といえます (これが DDD ユビキタス言語の目標の 1 つです)。

まとめ

CQRS と プレーンな DDD を比べてみると、ユース ケースと、システムの状態を変更する操作の基盤となる関連モデルに取り組むだけで、ほとんどの場合にドメイン分析と設計の複雑さを軽減できることがわかります。その他のすべての操作 (現在状態を読み取るだけの操作は)、大幅に単純化したコード インフラストラクチャで表現でき、プレーンなデータベース クエリよりも簡潔になります。

また、CQRS アーキテクチャのクエリ スタックでは、本格的な O/RM でも過剰な操作が実行される場合があります。1 日の終わりに必要な作業は、(ほとんどの場合は既製のリレーショナル テーブルから) データをクエリすることだけです。遅延読み込み、グループ化、結合などの高度な操作は必要ありません。必要な操作は必要な分だけ既に揃っているからです。Entity Framework 6 などの高度な O/RM ではニーズに比べて操作が複雑になり、PetaPoco や NPoco などのマイクロ O/RM でもニーズに十分事足りる場合もあります。興味深いことに、このトレンドは、新しい Entity Framework 7 と ASP.NET 5 スタックの設計にも部分的に反映されています。


Dino Esposito は『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014年) および『Modern Web Applications』(Microsoft Press、2016年) の著者です。JetBrains の .NET および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents@wordpress.com (英語) や Twitter (@despos、英語) でソフトウェアに関するビジョンを紹介しています。