September 2017

Volume 32 Number 9

データ ポイント - DDD に最適な EF Core 2.0

Julie Lerman

Julie Lerman今回のコラムをしばらく読み進めていくと、ドメイン駆動設計 (DDD) のパターンとガイダンスを利用するソリューションをビルドする際に Entity Framework (EF) を実装することについての記事がたくさんあることに気付きます。DDD がデータを保存する方法ではなくドメインに重点を置いているとしても、どこかの時点ではソフトウェアを出入りするデータが必要になります。

これまでのバージョンの EF では、そのパターン (オリジナルまたはカスタマイズ) により、ユーザーはそれほどストレスを感じずに単純なドメイン クラスをデータベースに直接マッピングすることができました。また、これまでのガイダンスでは、追加のデータ モデルを作成する必要なく、EF マッピング レイヤーで、適切に設計されたドメイン モデルからデータベースに入出力できれば、それで十分だと説明してきました。しかし、EF を使ってもっと適切に動作するようにドメイン クラスとロジックを思わず調整してしまうと、それは危険信号になります。この場合は、データを保存するモデルを作成し、ドメイン モデル クラスからデータ モデル クラスにマッピングする必要があります。

以前に DDD と EF のマッピングに関するコラムを書いたのはどれくらい前だったでしょう。それに気付いたときは少し驚きました。3 部構成の「ドメイン駆動設計のコーディング: データを重視する開発者のためのヒント」(MSDN Magaizne の 2013 年 8 月、9 月、10 月号) を執筆したのは、4 年も前のことです。第 1 部へのリンクは msdn.com/magazine/dn342868 で、そのページにこの連載へのリンクがすべて記載されています。

具体的には 2 つのコラムです。1 つは DDD のパターンについて取り上げたコラム、もう 1 つは EF によってドメイン クラスとデータベースとのマッピングが簡単に行われる場合と行われない場合について説明するコラムです。EF6 時点の最大の問題点の 1 つは、"子" コレクションをカプセル化できないために保護できないことでした。コレクションの保護に既知のパターンを使用するのは (多くの場合は IEnumerable の使用を意味します)、EF の要件に合わないうえに、EF はナビゲーションがモデルに含まれていることも認識しませんでした。Steve Smith と共同で、Pluralsight の「Domain-Driven Design Fundamentals」(ドメイン駆動型設計の基礎) コース (bit.ly/PS-DDD、英語) を作ったとき、かなり時間を使ってこのことを考えました。最終的には、Steve がすばらしい回避策 (bit.ly/2ufw89D) を思い付きました。

EF Core のバージョン 1.1 でついにこの問題が解決されたため、2017 年 1 月のコラム (msdn.com/magazine/mt745093) でこの新機能を取り上げました。EF Core 1.0 と 1.1 では、他の DDD 制約もいくつか解決されましたが、ギャップも残っています。中でも注目すべきギャップは、ドメインの種類で使用される DDD 値オブジェクトをマッピングできないことです。これを行う機能は、EF には初めからありましたが、EF Core にはまだ取り入れられていませんでした。ただし、次の EF Core 2.0 では、その制限がなくなります。

今回は、利用可能な EF Core 2.0 の機能のうち、DDD の多くの概念に合わせた機能について説明します。EF Core 2.0 は DDD の考え方を採用している開発者には非常に使いやすく感じられますが、その考え方を紹介するのは今回が初めてです。DDD を採用する予定がない場合でも、多くの優れたパターンからメリットを得ることができます。 EF Core ではさらに多くのことが可能になります。

1 対 1 をスマートに

Eric Evans は著書『エリック・エヴァンスのドメイン駆動設計』の中で、「双方向の関連付けとは、双方のオブジェクトがまとめて理解されることを意味します。アプリケーションの要件で双方向のコミュニケーションを必要としなければ、コミュニケーションの方向を追加するだけで、内部の依存関係が減り、設計がシンプルになります」と書いています。 このガイダンスに従ったところ、コードの副作用が本当になくなりました。EF では常に 1 対多と 1 対 1 の単一方向リレーションシップを処理できます。実は、今回のコラムを執筆中に学んだことがあります。それは、両端が必要な 1 対 1 のリレーションシップでは双方向リレーションシップが必ず必要になるという考えが間違っていたことです。ただし、リレーションシップが必要であれば明示的に構成しなくてはなりません。しかし、EF Core では、エッジ ケースを除けば、構成を行う必要がなくなります。

EF6 の 1 対 1 のリレーションシップの要件のうち気に入らないのは、依存する型のキー プロパティが、プリンシパル エンティティへの外部キーとしての役割も二重に果たす必要があることです。このため、そのことに慣れているとはいえ、奇妙な方法でクラスを設計せざるを得ませんでした。EF Core に一意外部キーのサポートが導入されたことにより、1 対 1 のリレーションシップの依存側に明示的な外部キーを持つことができるようになります。明示的な外部キーを用意するのは自然な流れです。また、多くの場合、EF Core はその外部キー プロパティの存在に基づいてリレーションシップの依存側を適切に推測できるはずです。なんらかのエッジ ケースが原因で正しく理解されない場合は、構成を追加する必要があります。これについては、外部キー プロパティの名前を変更する場面で簡単に示します。

1 対 1 のリレーションシップをデモするため、お気に入りの EF Core ドメインの、映画「7 人の侍」のクラスを使用します。

public class Samurai {
  public int Id { get; set; }
  public string Name { get; set; }
  public Entrance Entrance { get; set; }
}

public class Entrance {
  public int Id { get; set; }
  public string SceneName { get; set; }
  public int SamuraiId { get; set; }
}

EF Core では、Samurai と Entrance (映画の登場人物の初登場場面) というクラスのペアは、Entrance が依存型になる単方向の 1 対 1 リレーションシップとして正しく識別されます。Entrance にはナビゲーション プロパティを含める必要がなく、Fluent API での特殊なマッピングも必要ありません。外部キー (SamuraiId) は表記規則に従うため、EF Core はこのリレーションシップを認識できます。

EF Core は、データベースでは Entrance.SamuraiId が Samurai をポイントする一意外部キーであると推測します。かなり苦労しました。EF Core は EF6 ではないと絶えず自分に言い聞かせる必要がありました。 既定では、Entrance を強制的に必須にするようにドメイン ロジックを調整しない限り、.NET と EF Core は実行時に Samurai.Entrance をオプション プロパティとして扱います。EF4.3 からは、クラスやマッピングの [Required] 注釈に対応させる検証 API を利用できるようになります。しかし、EF Core にはこうした特定の問題を監視する検証 API が (まだ?) ありません。他にも、データベース関連の要件があります。たとえば、Entrance.SamuraiId は Null 値を許容しない int 型です。SamuraiId 値を設定しないで Entrance を挿入しようとしても、EF Core は無効なデータをキャッチしません。つまり、現状では InMemory プロバイダーは警告を発しません。ですが、リレーショナル データベースでは、制約の競合についてのエラーをスローするべきです。

ただし、DDD の観点では、ドメイン ロジックのエラーを指摘するために永続化レイヤーを使用するべきではないため、あまり問題にはなりません。Samurai が Entrance を必要とするとしたら、それはビジネス ルールです。Entrance を孤立させられないとしたら、それもビジネス ルールです。そのため、いずれにせよ検証をドメイン ロジックに含める必要があります。

以下に、こうしたエッジ ケースの例を示します。依存側のエンティティ (Entrance など) の外部キーが表記規則に従っていない場合、Fluent API を使用して EF Core にこれを認識させることができます。Entrance.SamuraiId が Entrance.SamuraiFK だったとしたら、おそらく次のように、その FK を明確にできます。

modelBuilder.Entity<Samurai>().HasOne(s=>s.Entrance)
  .WithOne().HasForeignKey<Entrance>(e=>e.SamuraiFK);

どちら側でもリレーションシップが必要な場合 (つまり、Entrance にも Samurai が必須の場合)、WithOne の後に IsRequired を追加できます。

細かくカプセル化できるプロパティ

DDD では、集計 (オブジェクト グラフ) をビルドでき、その集計ルート (グラフ内のプライマリ オブジェクト) がグラフ内の他のすべてのオブジェクトを管理します。つまり、他のコードによってルールが誤用も悪用もされないコードを作成します。プロパティをランダムに設定できない (また、多くの場合、ランダムに読み取られない) ようにカプセル化するのが、グラフを保護する重要な方法です。EF6 以前では、スカラー プロパティとナビゲーション プロパティにプライベート セッターを用意して、データの読み取り時と更新時を EF が必ず認識するようにできましたが、プロパティをプライベートにするのは簡単ではありませんでした。Rowan Miller の投稿に、EF6 でそれを行う 1 つの方法が紹介され、以前の回避策へのリンクがいくつか提供されています (bit.ly/2eHTm2t)。また、1 対多リレーションシップでナビゲーション コレクションを保護する確実な方法はありませんでした。この後者の問題については多くの資料があります。IEnumerable<T> のマッピングがサポートされたおかげで、バッキング フィールド (推定バッキング フィールド) を持つプライベート プロパティと EF Core を簡単に連携させることができるだけでなく、コレクション プロパティを実際にカプセル化することもできるようになります。バッキング フィールドと IEnumerable<T> については、2017 年 1 月のコラムで既に取り上げましたので、ここでは触れません。ただし、これは DDD のパターンにとって非常に重要で、今回の内容にも関係しています。

スカラーとコレクションは非表示にできますが、カプセル化が強く望まれるもう 1 つのプロパティとしてナビゲーション プロパティがあります。ナビゲーション コレクションは IEnumerable<T> のサポートによってメリットを得られますが、Samurai.Entrance などのプライベートなナビゲーション プロパティはモデルによって理解されません。ただし、集計ルートが非表示のナビゲーション プロパティを理解するようにモデルを構成する方法はあります。

たとえば、次のコードでは、Entrance を Samurai のプライベート プロパティとして宣言しています (明示的なバッキング フィールドは、必要に応じて使用できましたが、使用していません)。CreateEntrance メソッド (Entrance のファクトリ メソッドを呼び出します) を使って新しい Entrance を作成できます。この場合、Entrance の SceneName プロパティのみを読み取ることができます。C# 6 の null 条件演算子を使用して、まだ Entrance を読み込んでいない場合に例外が発生するのを防いでいます。

private Entrance Entrance {get;set;}
public void CreateEntrance (string sceneName) {
    Entrance = Entrance.Create (sceneName);
  }
public string EntranceScene => Entrance?.SceneName;

表記規則によって、EF Core はこのプライベート プロパティを推測しません。バッキング フィールドがある場合でも、プライベート Entrance は自動検出されないため、データ ストアの操作時には使用できません。これは意図的な API のデザインで、発生する可能性がある他の副作用から保護するのに役立ちます。しかし、明示的に構成することもできます。Entrance がパブリックの場合、EF Core は 1対 1 リレーションシップを理解できます。ただし、プライベートなので、まずは EF に認識させる必要があります。

OnModelCreating では、HasOne/WithOne fluent マッピングを追加して EF Core に認識させる必要があります。Entrance はプライベートなので、HasOne のパラメーターとしてラムダ式を使用することはできません。代わりに、プロパティをその型と名前で記述する必要があります。WithOne は通常、ラムダ式を使用して、ナビゲーション プロパティをペアリングのもう一方に指定します。しかし Entrance には Samurai ナビゲーション プロパティがなく、外部キーしかありません。それで十分です。 EF Core はパラメーターを認識するための情報を十分持っているため、パラメーターを空のままにすることができます。

modelBuilder.Entity<Samurai> ()
  .HasOne (typeof (Entrance), "Entrance").WithOne();

次に示すように、Samurai クラスで _entrance などのバッキング プロパティを使用する場合はどうでしょう。

private Entrance _entrance;
private Entrance Entrance { get{return _entrance;} }
public void CreateEntrance (string sceneName) {
    _entrance = _entrance.Create (sceneName);
  }
public string EntranceScene => _entrance?.SceneName;

EF Core は、Entrance プロパティを具体化するときにバッキング フィールドを使用する必要があることを理解します。これについて学んでいたときに GitHub での非常に長い会話の中で Arthur Vickers が説明していたように、「バッキング フィールドがあって、セッターがない場合、EF は単純にバッキング フィールドを使用しますが、それは、EF で使用できるものが他に何もない」ためです。 したがって問題なく動作します。

バッキング フィールドの名前が表記規則に従っていない場合、たとえば、_foo という名前を付けた場合、次のようなメタデータ構成が必要になります。

modelBuilder.Entity<Samurai> ()
  .Metadata
  .FindNavigation ("Entrance")
  .SetField("_foo");

これでデータベースへの更新とクエリによって、そのリレーションシップを解決できます。一括読み込みを使用する場合、ラムダ式では検出できないため、Entrance に文字列を使用する必要があります。次に例を示します。

var samurai = context.Samurais.Include("Entrance").FirstOrDefault();

フィルターなどを目的にバッキング フィールドを操作する場合、標準構文を使用できます。これについては、「バッキング フィールド」のドキュメント ページ (bit.ly/2wJeHQ7) で紹介されています。

サポートされるようになった値オブジェクト

値オブジェクトは DDD の重要な概念で、ドメイン モデルを値の型として定義できます。値オブジェクトは独自の ID を持たず、それをプロパティとして使用するエンティティの一部になります。一連の文字で構成される文字列値型について考えてみます。1 文字を変更すると、単語の意味が変わるため、文字列は不変です。文字列を変更するには、文字列オブジェクト全体を置き換える必要があります。DDD では、1 対 1 リレーションシップを識別しているすべての場所で値オブジェクトの使用を検討するようガイドしています。値オブジェクトについては、既に紹介した「Domain-Driven Design Fundamentals」(ドメイン駆動型設計の基礎) コースをご覧ください。

EF では常に ComplexType 型を使って値オブジェクトを含める機能がサポートされています。キーを持たない型を定義して、その型をエンティティのプロパティとして使用することができました。ComplexType として認識し、そのプロパティをエンティティのマッピング先テーブルにマッピングするには、EF をトリガーするだけです。その後、型を不変にすることや、等しいかどうかを判断して Hash をオーバーライドするときにすべてのプロパティを評価する手段など、値オブジェクトに必要な機能を持つように型を拡張できます。これまでのコラムでは多くの場合、Jimmy Bogard の ValueObject 基底クラスから型を派生して、その属性をすばやく適用しています。

人の名前は、値オブジェクトとしてよく使用される型です。エンティティに人の名前を含めることを望んだら、必ずルールの共通セットに従うようにすることができます。図 1 は、First プロパティと Last プロパティ (どちらも完全にカプセル化されています) を持つ単純な PersonName クラスと、FullName を返すプロパティを示しています。このクラスは、名前の姓名両方を必ず指定する設計になっています。

図 1 PersonName 値オブジェクト

public class PersonName : ValueObject<PersonName> {
  public static PersonName Create (string first, string last) {
    return new PersonName (first, last);
  }
  private PersonName () { } 
  private PersonName (string first, string last) {
    First = first;
    Last = last;
  }
  public string First { get; private set; }
  public string Last { get; private set; }
  public string FullName => First + " " + Last;
}

PersonName を他の型のプロパティとして使用し、PersonName クラスにロジックを追加しながら機能を増やしていくことができます。ここで 1 対 1 リレーションシップでの値オブジェクトのすばらしさは、コーディング中にリレーションシップを管理する必要がないことです。これは、標準のオブジェクト指向プログラミングです。しかも、単なるプロパティです。Samurai クラスで、この型の新しいプロパティを追加し、そのセッターをプライベートにし、Identify という別のメソッドを提供してセッターの代わりに使用します。

 

public PersonName SecretIdentity{get;private set;}
public void Identify (string first, string last) {
  SecretIdentity = PersonName.Create (first, last);
}

EF Core 2.0 までは、ComplexTypes と同様の機能はなかったので、値オブジェクトを個別のデータ モデルに追加しないで簡単に使用することはできませんでした。EF チームは、EF Core に ComplexType を再実装するのではなく、所有エンティティという概念を生み出しました。所有エンティティでは、もう 1 つの EF Core 機能、シャドウ プロパティを活用します。ここで、所有エンティティはそれらを所有する型の追加プロパティとして認識され、EF Core はそれらのエンティティがデータベース スキーマでどのように解決されるか、そのデータを尊重するクエリと更新をどのように構築するかを理解します。

EF Core 2.0 の表記規則は、この新しい SecretIdentity プロパティが持続的なデータに組み込まれる型であることを自動的には検出しません。OwnsOne メソッドを使用して、Samurai.SecretIdentity プロパティが DbContext.OnModelCreating の所有エンティティであることを DbContext に明示的に指示する必要があります。

protected override void OnModelCreating (ModelBuilder modelBuilder) {
  modelBuilder.Entity<Samurai>().OwnsOne(s => s.SecretIdentity);
}

これにより、PersonName のプロパティを Samurai のプロパティとして解決するよう強制されます。コードでは Samurai.SecretIdentity 型を使用して First プロパティと Last プロパティにナビゲートする一方、それら 2 つのプロパティは Samurais データベース テーブル内の列として解決されます。EF Core の表記規則では、Samurai のプロパティの名前 (SecretIdentity) と所有エンティティ プロパティの名前を含む名前を付けます (図 2 参照)。

値のプロパティを含む Samurais テーブルのスキーマ
図 2 値のプロパティを含む Samurais テーブルのスキーマ

これで、Samurai のシークレット名を識別して、次のようなコードで保存できます。

using (var context = new SamuraiContext()) {
  var samurai = new Samurai { Name = "HubbieSan" 
  samurai.Identify ("Late", "Todinner");
  context.Samurais.Add (samurai);
  context.SaveChanges ();
}

データ ストアでは、"Late" は SecretIdentity_First フィールドに、"Todinner" は SecretIdentity_Last フィールドに保存されます。

次に Samurai に対して単純にクエリを実行します。

var samurai=context.Samurais .FirstOrDefaultAsync (s => s.Name == "HubbieSan")

EF Core では結果となる Samurai の SecretIdentity プロパティが設定されるので、次のように要求することで ID を参照できます。

samurai.SecretIdentity.FullName

EF Core では、所有エンティティであるプロパティが設定されている必要があります。PersonName 型を設計して適用する方法については、サンプル ダウンロードをご確認ください。

簡単なレッスン用のシンプルなクラス

ここで紹介したのは、最低限の方法で DDD 実装の中核となる概念の一部を活用するシンプルなクラスです。これにより、EF Core がそれらのコンストラクトにどのように対応するかがわかります。EF Core 2.0 が 1 対 1 の単方向リレーションシップを理解できることがわかりました。スカラー プロパティ、ナビゲーション プロパティ、およびコレクション プロパティが完全にカプセル化されるエンティティからデータを永続化できます。また、ドメイン モデルで値オブジェクトを使用でき、それらのオブジェクトを永続化することもできます。

今回は、クラスをシンプルに保っていますが、DDD のパターンを使用するエンティティと値オブジェクトを適切に制限する追加ロジックが欠けています。このシンプルさは、ダウンロード サンプルに反映されています。ダウンロード サンプルは GitHub (bit.ly/2tDRXwi) にもあります。そのページには、シンプルなバージョンと詳細なブランチの両方があります。このドメイン モデルを強化して、いくつかの追加の DDD プラクティスを集計ルート (Samurai)、それが関連するエンティティ (Entrance)、および値オブジェクト (PersonName) に適用することで、EF Core 2.0 が DDD 集計の現実的な式をどのように処理するかを理解できるようになっています。そのブランチに適用される詳細パターンについては、次回のコラムで取り上げます。

最終リリース直前の EF Core 2.0 バージョンを使用していることに注意してください。ここで設計した動作の多くは信頼できますが、2.0.0 のリリース前にマイナーチェンジされる可能性もあります。


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


この記事について MSDN マガジン フォーラムで議論する