データ ポイント

ドメイン駆動設計のコーディング: データを重視する開発者のためのヒント (第 3 部)

Julie Lerman

コード サンプルのダウンロード

Julie Lerman今回は、これまでデータ重視の開発を行ってきた開発者がドメイン駆動設計 (DDD) の難しいコーディングに頭を抱える場面に救いの手を差し伸べるシリーズの最終回です。私は Entity Framework (EF) を使用する Microsoft .NET Framework の開発者として、これまで長い間データを重視 (さらにはデータベースを重視) する開発を経験してきたため、DDD の実装技術に自身の能力をどのように組み合わせればよいか理解するのに苦労し、たくさんの話し合いを行い、しまいには泣き言まで言いだす始末でした。そんな私でも、現在は、プロジェクトに完全な DDD 実装 (クライアントとの折衝からコーディングまで) を取り込んでいるわけではありませんが、DDD の多くのツールからたくさんのメリットを得ています。

最終回の今回は、DDD コーディングの 2 つの重要な技術パターンと、それを私のオブジェクト リレーショナル マッピング (ORM) ツールである EF に当てはめる方法について説明します。以前の回で、一対一の関係性について取り上げました。今回は、DDD に適した一方向の関係性と、これがアプリケーションに及ぼす影響について説明します。一方向の関係性は難しい決断を迫ります。つまり、どのようなときに、EF が行ういくつかの優れた関係性の「魔法」を使用しないで物事が良い方向へ進むかを認識しなければなりません。また、集計ルートとリポジトリの間でタスクのバランスを取ることの重要性についても、少し取り上げます。

ルートからの一方向の関係性を構築する

EF でモデルを構築するようになってから、双方向の関係性が普通になり、それほど深く考えることなくこれを行ってきました。双方向でナビゲートできることには意味があります。注文と顧客が複数ある場合、顧客単位に注文を表示でき、注文ごとに顧客のデータにアクセスできると便利です。深く考えることなく、注文と注文明細との双方向の関係性も構築します。注文から明細への関係性には意味があります。しかし、一度違う視点で考えてみると、明細から注文にさかのぼるシナリオはほとんどないことがわかります。考えられるのは、製品についてのレポートを作成していて、よく一緒に注文される製品を分析したり、顧客や出荷データに関連する分析を行う場合です。このような場合、製品から明細にナビゲートして、注文までさかのぼる必要があります。ただし、これが生じるのはレポートのシナリオだけです。通常、このシナリオでは DDD を重視したオブジェクトを操作する必要はありません。

注文から明細にナビゲートする必要があるだけならば、モデルでこうした関係性を表す最善の方法は何でしょう。

既に指摘したように、DDD には一方向の関係性が適しています。Eric Evans は、「可能な限り関係性を制限することが重要です」、そして「ドメインを理解することで自然な方向性が明らかになります」と助言しています。関連付けの維持を Entity Framework に依存している場合は特に、関係性の複雑さの管理が、多くの混乱につながりかねない領域になるのは間違いありません。Entity Framework における関連付けについて、「データ ポイント」ではこれまで多くのコラムを執筆してきました。取り除けるあらゆるレベルの複雑さにはおそらくメリットがあります。

これまでの DDD シリーズで使用してきたシンプルな販売モデルについて考えてみると、注文から明細への関係性に偏っていることがわかります。注文がない状態で明細を作成、削除、または編集することは考えられません。

このシリーズでこれまでに構築した Order 集計について振り返ると、注文が明細を管理します。たとえば、新しい明細を追加するには Order クラスの CreateLineItem メソッドを使用する必要があります。

public void CreateLineItem(Product product, int quantity)
 {
   var item = new LineItem
   {
     OrderQty = quantity,
     ProductId = product.ProductId,
     UnitPrice = product.ListPrice,
     UnitPriceDiscount = CustomerDiscount + PromoDiscount
   };
   LineItems.Add(item);
 }

LineItem 型には OrderId プロパティはありますが、Order プロパティはありません。これは、OrderId 値を設定することはできても、LineItem から実際の Order インスタンスにナビゲートできないことを意味します。

この場合、Evans の言葉を借りると、「双方向の関係性に制限を課した」ことになります。つまり、Order から LineItem へはたどれますが、反対方向にはたどれません。

この考え方は、モデルだけでなくデータ層でも意味があります。私は Entity Framework を ORM ツールとして使用して、単に Order クラスの LineItems プロパティからこの関係性を適切に把握します。また、たまたま EF の規則に従っているため、EF は LineItem.OrderId が Order クラスに戻る外部キー プロパティであることを理解します。OrderId に別の名前を使用すると、Entity Framework にとっては事態が複雑になります。

しかし、このシナリオでは、次のように新しい LineItem を既存の注文に追加できます。

order.CreateLineItem(aProductInstance, 2);
 var repo = new SimpleOrderRepository();
 repo.AddAndUpdateLineItemsForExistingOrder(order);
 repo.Save();

ここで order 変数は、既存の注文と 1 つの新しい LineItem を持つグラフを表すことになります。既存の注文はデータベース内に保存され、既に OrderId 値を持っていますが、新しい LineItem には OrderId プロパティの既定値 0 しか含まれていません。

私のリポジトリ メソッドは、この注文グラフを受け取り、このグラフを EF コンテキストに追加して、正しい状態を適用します (図 1 参照)。

図 1 注文グラフに状態を適用する

public void AddAndUpdateLineItemsForExistingOrder(Order order)
 {
 _context.Orders.Add(order);
 _context.Entry(order).State = EntityState.Unchanged;
 foreach (var item in order.LineItems)
 {
   // Existing items from database have an Id & are being modified, not added
   if (item.LineItemId > 0)
   {
     _context.Entry(item).State = EntityState.Modified;
   }
 }
 }

EF の動作に不慣れな場合、Add メソッドによってコンテキストがグラフ内のすべて (注文と 1 つの明細) の追跡を開始することになります。同時に、グラフ内のそれぞれのオブジェクトには、Added 状態のフラグが設定されます。しかし、このメソッドは既存の注文を使用することを重視しているため、Order は新規ではないことがわかっています。そのため、メソッドは Order インスタンスの状態を Unchanged に設定して解決します。また、既存の LineItems をチェックし、状態を Modified に設定することで、新規挿入されるのではなくデータベースが更新されるようにします。さらに細かい処理を行うアプリケーションでは、それぞれのオブジェクトの状態をより明確に把握するためにパターンを使用しますが、今回のサンプルに細かい処理を加えて複雑にするのは望ましくありません (このパターンの以前のバージョンは Rowan Miller のブログ bit.ly/1cLoo14(英語) を、また更新された例は私たちの共著『Programming Entity Framework: DbContext』(O’Reilly Media、2012 年) を参照してください)。

これらすべての操作はコンテキストがオブジェクトを追跡している間に実行されるため、Entity Framework は新しい LineItem インスタンスの OrderId の値も「魔法のように」解決します。そのため、Save を呼び出すときには、LineItem は OrderId 値が 1 であることを把握しています。

更新のために EF 関係性管理の魔法を手放す

今回すばらしい幸運に恵まれたのは、偶然にも LineItem 型が外部キー名を使って EF の規則に従っていたためです。OrderId 以外の名前 (OrderFK など) を付ける場合、型を変更して (本来不要な Order ナビゲーション プロパティを導入するなど)、その後 EF マッピングを指定する必要があります。ORM を満たすためだけに複雑さが増すことになるため、これは望ましくありません。これが必要な場合もありますが、できれば避けた方が無難でしょう。

EF 関係性の魔法との依存関係から離れ、コードで外部キーの設定を制御する方がシンプルです。

最初の手順は EF にこの関係性を無視するように指示することです。この指示がなければ外部キーを探し続けることになります。

以下に、EF が関係性を無視するように、DbContext.OnModelBuilder メソッド オーバーライドで使用するコードを示します。

modelBuilder.Entity().Ignore(o => o.LineItems);

これで、関係性を自身で制御できるようになります。これはリファクタリングを意味するので、LineItem に OrderId と他の値を要求するコンストラクターを追加し、これにより LineItem は DDD エンティティのようにします。また、オブジェクト初期化子ではなくこのコンストラクターを使用するように、Order の CreateLineItem メソッドを修正します。

図 2は、更新したバージョンのリポジトリ メソッドを示します。

図 2 リポジトリ メソッド

public void UpdateLineItemsForExistingOrder(Order order)
 {
   foreach (var item in order.LineItems)
   {
     if (item.LineItemId > 0)
     {
       _context.Entry(item).State = EntityState.Modified;
     }
     else
     {
       _context.Entry(item).State = EntityState.Added;
       item.SetOrderIdentity(order.OrderId);
     }
   }
 }

注文グラフを追加することなく、注文の状態が Unchanged に解決されます。実際、EF は関係性を認識しないため、context.Orders.Add(order) を呼び出すと、order インスタンスは追加しますが、以前のように関連する明細を追加することはありません。

代わりに、グラフの明細を反復処理し、既存の明細の状態を Modified に設定するだけでなく、新しい明細の状態を Added に設定します。ここで使用している DbContext.Entry 構文は、2 つのことを行います。状態を設定する前に、コンテキストが既にこの特定のエンティティを把握 (または「追跡」) しているかどうかを確認します。把握していなければ、内部でエンティティを付加します。これで、コードが状態プロパティを設定するという事実に対応できます。そのため 1 行のコードで LineItem の状態を付加および設定します。

これで、コードは DDD で EF を使用するもう 1 つの健全な方法と一致するようになります。つまり、EF に依存しないで関係性を管理します。EF は、さまざまなシナリオで多くの魔法を実行し、膨大なメリットをもたらします。私は何年もの間これを活用してきました。しかし、DDD 集計の場合、必要な操作を実行するには、本当にモデルでこれらの関係性を管理し、データ層に依存しないようにします。

長い間、キー (Order.OrderId など) に整数型を使用することにこだわっていて、これらのキーの値を提供するためにデータベースを利用していたため、明細を伴う新しい注文などの新しい集計のためにリポジトリでいくらか追加作業が必要になります。永続化を厳密に制御する必要があるため、注文の挿入、新しいデータベースが生成した OrderId 値の取得、取得した値の新しい明細への適用、およびデータベースへの保存という、グラフを挿入する古くからのパターンを使用します。これが必要なのは、EF がこの魔法を実行するために通常使用する関係性を崩したためです。これをリポジトリで実装する方法については、サンプル ダウンロードを参照してください。

長い年月を経て、ID の作成をデータベースに頼るのをやめ、GUID をアプリケーションで生成してキー値に割り当てるようになりました。これにより、さらにドメインをデータベースから切り離せるようになります。

クエリのために EF の関係性管理の魔法を維持する

EF 関係性のモデルを取り除くことは、前のシナリオで更新を実行するのに本当に助かりました。しかし、EF のすべての関係性機能を失うのは望ましくありません。データベースからクエリする際に関連データを読み込む機能は、手放したくない機能の 1 つです。一括読み込みでも、遅延読み込みでも、明示的に読み込む場合でも、追加クエリを記述して実行しないで、関連データを取得する EF の機能を活用したいと思います。

ここで、懸念事項を分離するという考え方を広げることが重要になります。設計を DDD の教訓に合わせるときに、同様のクラスの表現を変えるのは珍しいことではありません。たとえば、顧客管理のコンテキストで使用するように設計された Customer クラスでこのようなことを行うことがありますが、顧客の名前と ID のみを必要とする選択リストを設定するだけの Customer クラスではこのようなことは行いません。

DbContext 定義を変えることにも意味があります。データを取得するシナリオでは、Order と LineItems の関係性を把握しているコンテキストがあれば、データベースから注文と一緒に明細を一括で読み込むことができます。しかし、以前のように更新を実行するときは、関係性を明示的に無視するコンテキストがあれば、ドメインを細かく制御できます。

ソフトウェアで解決する複雑な問題の特定のサブセットには、Command Query Responsibility Segregation (CQRS) と呼ばれるこの視点を突き詰めたパターンを利用します。CQRS は、データの取得 (読み取り) とデータの保存 (書き込み) を、モデルとアーキテクチャが異なる個別のシステムと考えます。今回の小さな例では、データ取得操作とデータ保存操作に異なる関係性の把握を当てはめることで得られるメリットに注目し、CQRS が何を実行するのに役立つかについての考え方を示します。CQRS の詳細については、すばらしいリソースである CQRS Journey (msdn.microsoft.com/library/jj554200、英語) を参照してください。

データ アクセスは集計ルートではなくリポジトリで行う

ここで少し戻って、一方向の関係性に目を向けたときに悩まされた最後の問題に対処します (DDD に関してこれ以上問題がないというわけではありませんが、これがこのシリーズで扱う最後のトピックになります)。一方向の関係性についてのこの問題は、データベース重視の考え方をする私たちにとって一般的な次のような疑問です。(DDD での) データ アクセスは正確にはどこで行われるのでしょう。

EF が最初にリリースされたときには、データベースで作業するために既存のデータベースからリバース エンジニアリングするしかありませんでした。そのため、既に述べたように、すべての関係性を双方向にすることに慣れていました。データベースの Customers と Orders のテーブルに、一対多の関係性を表すプライマリ キーまたは外部キーの制約があったなら、モデルでこの一対多の関係性に直面したでしょう。Customer には、注文のコレクションへのナビゲーション プロパティがありました。Order には、Customer のインスタンスへのナビゲーション プロパティがありました。

モデルを表しデータベースを生成できる Model First と Code First に進化したため、このパターンに継続して従い、関係性の両方の側でナビゲーション プロパティを定義します。これは EF にとって望ましく、マッピングはよりシンプルになり、コーディングが自然になります。

そのため、DDD で、CustomerId または完全な Customer 型も把握した Order 集計ルートを使用しており、Order から Customer にさかのぼってナビゲートできなかったときには、当惑しました。私の最初の疑問は「1 人の顧客の注文をすべて見つける場合はどうすればよいか」というものです。常に、これを行える必要があると考えてきましたし、当たり前のように双方向のナビゲーションへのアクセスを利用していました。

ロジックが Order 集計のルートで始まる場合、この疑問にどのように答えられるでしょう。さらに、当初は集計ルートで行うすべてのことについて誤った考え方をしており、どうすればよいかわかりませんでした。

解決策がわかったとき、自分の頭をたたき、それまで気付かなかった自分に腹が立ちました。同じ困難に直面する人がいるかもしれないので、ここで私の失敗を共有できるようにします。この疑問に答えるのに役立つのは、集計ルートの問題でも、Order の問題でもありません。ただし、クエリと永続化を実行するのに使用する Order に重点を置いたリポジトリでは、疑問に答えるためのメソッドを作成できるでしょう。

public ListGetOrdersForCustomer(Customer customer)
   {
     return _context.Orders.
       Where(o => o.CustomerId == customer.Id)
       .ToList();
   }

メソッドは、Order 集計ルートの一覧を返します。当然、DDD を実行するためにこれを作成する場合、念のためではなく、特定のコンテキストで必要になることがわかっている場合にのみ、リポジトリにメソッドを配置します。レポート アプリなどで必要になることはありますが、販売の注文を構築するために設計されたコンテキストでは必要ありません。

旅は始まったばかり

過去数年間にわたって DDD について学習してきたため、今回のシリーズでは私にとって最も難しかったトピックとして、Entity Framework がデータ層の一部となる場合に DDD を実装する方法を取り上げました。私が直面した課題のいくつかは、何年もの間、物事がデータベースでどのように動作するかという観点からソフトウェアについて考えていたことに起因しています。この観点から離れることで、ドメインの問題という、そのためにソフトウェアを設計していた目の前の問題に集中できるようになりました。同時に、ソリューションに追加する際にはデータ層の問題に直面する場合があるため、適切なバランスを見つける必要がありました。

Entity Framework でクラスを直接データベースにマッピングして戻す場合に注目してきましたが、ドメイン ロジックとデータベースの間にもう 1 つ (または複数) の層があるかもしれないと考えることが重要です。たとえば、ドメイン ロジックがやり取りするサービスがあるかもしれません。この場合、データ層はドメイン ロジックからマッピングするのにそれほど (または、まったく) 重要ではなく、問題はサービスに移行します。

ソフトウェア ソリューションには多くの手法があります。完全なエンドツーエンドの DDD の手法 (習熟するのはかなり大変です) を実装するのではなくても、私のすべてのプロセスに DDD から学んだ教訓と技術が生かされています。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET Framework トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog(英語) で、『Programming Entity Framework』(O'Reilly Media、2010 年)、および同書の Code First 版 (O'Reilly Media、2011 年) と DbContext 版 (O'Reilly Media、2012 年) の著者でもあります。Twitter (twitter.com/julielerman、英語) で彼女をフォローし、juliel.me/PS-Videos(英語) で Pluralsight のコースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Stephen Bohlen (マイクロソフト) に心より感謝いたします。