Share via



April 2016

Volume 31 Number 4

データ ポイント - EF における結び付きのないエンティティの状態への対処

Julie Lerman

Julie Lerman結び付きのないデータとは、Entity Framework 以前からある古い問題で、ほぼすべてのデータ アクセス ツールで問題になります。この問題の解決は簡単ではありません。サーバーはデータをネットワーク上に送信します。このとき、そのデータを要求したクライアント アプリで何が行われるかをサーバーは認識しません。さらには、データが返されるかどうかさえ把握しません。その後、突如として、あるデータが要求の中に再び出現します。ですが、これは前と同じデータでしょうか。知らないうちに、何か起きたのでしょうか。データに何か処理が施されたのでしょうか。それとも、まったく新しいデータなのでしょうか。わからないことばかりです。

.NET 開発者なら、この問題の解決パターンを見出せるかもしれません。ADO.NET データセットを思い出してください。ADO.NET データセットは、データを保持するだけでなく、各行と各列の変更状態情報をすべてカプセル化します。こうした状態情報には、「変更済み」や「新規」といった情報に限らず、オリジナルのデータも保持されます。ASMX Web サービスのビルドに着手したとき、データセットをシリアル化してネットワーク上に送信するのは非常に簡単でした。送信したメッセージが .NET クライアントに到着すると、クライアントはデータセットのシリアル化を解除して、変更の追跡を継続できます。データをサービスに返すときは、データを再びシリアル化します。その後、サーバー側でデータのシリアル化を解除してデータセットに戻し、変更追跡情報一式と共にデータベースに永続化します。この方法は機能し、非常に簡単ですが、膨大なデータがネットワーク上を行き来することになります。データ ビットそのものだけでなく、データセットの構造をシリアル化することで、大きな XML が作成されます。

ネットワーク上を行き来するシリアル化されたメッセージのサイズは、問題の 1 つにすぎません。Web サービスの長所はさまざまなプラットフォームにサービスを提供できることですが、そのため、メッセージ自体は別の .NET アプリケーションにとっても意味があります。2005 年、Scott Hanselman は、この問題について強く注意を促す「Web サービスからデータセットを返すことは悪魔の誕生であり、世界にとってまさに害悪である」(bit.ly/1TlqcB8、英語) という叙事詩的なタイトルのコラムを執筆しました。

.NET の主要データ アクセス ツールをデータセットから Entity Framework に置き換えると、ネットワーク上を行き来していた状態情報はすべて失われます。変更追跡情報 (オリジナル値、現在値、状態) は、EF ではデータと共に格納されるのではなく、ObjectContext の一部として格納されます。だとしても、EF の最初のイテレーションでは、シリアル化されたエンティティを EF EntityObject 型から継承する必要があったため、メッセージが煩雑になりました。しかし、ネットワーク上を行き来するエンティティ データを含むメッセージは、状態を理解する能力を失っています。過剰にも思える情報を帯同するデータセットに慣れていた開発者はパニックに陥りました。結び付きのない状態を扱うことに慣れていた開発者は、EntityObject 基本クラスのこの要件に困惑しました。結局、EF チームがこの問題に気付き、(大きな節目となる) 次のイテレーションの EF4 では、Plain Old CLR Object (POCO) をサポートするよう進化しました。つまり、ObjectContext がシンプル クラスの状態を保持します。このクラスを EntityObject から継承する必要はありません。

ですが、EF4 では、結び付きのない状態の問題はなくなっていません。EF にはエンティティの状態に関する手がかりがなく、状態を追跡できません。データセットに慣れていた開発者は、EF にも同等の解決策を求めていたため、軽量なメッセージと引き換えに、変更の追跡が切り離されることに不満を持ちました。そこで、開発者たち (筆者も含む) は、データの移動中に何が起きたかをサーバーに知らせる方法を多数調べました。データベースからデータを再度読み取り、EF に比較させれば、変更点があればそれを解明できます。「ID キー値が 0 ならば新しい ID キー」といった推測も可能です。低レベルの API を調査して、状態の把握や状態に応じたアクションを実行するコードを作成することもできます。当時はこのようなことをたくさん行いましたが、1 つとして満足できる解決策はありませんでした。

EF4.1 に軽量の DbContext が導入されたときに、エンティティの状態についてのコンテキストを簡単に通知できる機能が用意されました。DbContext から継承するクラスにより、以下のようなコードを作成できます。

myContext.Entity(someEntity).State=EntityState.Modified;

someEntity がコンテキストにとって新しいエンティティのときは、コンテキストはそのエンティティの追跡を開始するよう強制され、同時に、エンティティの状態を指定するよう強制されます。これにより、EF は SaveChanges 時に構成する SQL コマンドの種類を十分把握できるようになります。上記の例では、コマンドの種類は UPDATE になります。Entry().State は、ネットワーク経由でデータが到着したときにその状態を把握するという課題には役立ちませんが、Entity Framework を使用する開発者に広く普及するようになった優れたパターンを実装できるようになります。詳しくは、これから順を追って説明していきます。

次期バージョンの Entity Framework である EF Core (旧称 EF7) を使用すれば、結び付きのないグラフの処理の一貫性が向上しますが、今回紹介するパターンも依然として有効な手段です。

結び付きのないデータの問題は、データのグラフがやり取りされるようになるとさらに複雑になります。最大の問題の 1 つは、受け取ったエンティティの状態変化を検出する既定の方法がサーバーにない状況で、グラフに混在した状態のオブジェクトが含まれる場合です。DbSet.Add を使用すると、エンティティはすべて既定で Added とマークされます。DbSet.Attach を使用すると、Unchanged とマークされます。これは、すべてのデータがデータベースに由来し、キー プロパティが設定されている場合です。EF は Add や Attach という指示に従います。EF Core には Update メソッドがあり、このメソッドの動作は Add、Attach、および Delete と同じ動作に従いますが、エンティティを Modified とマークします。注意が必要な例外が 1 つあり、DbContext が既にエンティティを追跡していると、そのエンティティの既知の状態は上書きされません。ただし、非接続型のアプリでは、クライアントから返されるデータを接続する前に、コンテキストを追跡することは考えません。

既定の動作のテスト

ここでは既定の動作を調べ、問題点を明らかにします。例を示すために、Ninja、NinjaEquipment、Clan などの関連クラスをいくつか含むシンプルなモデル (ダウンロードして入手可) を用意しました。Ninja は NinjaEquipment のコレクションを含み、1 つの Clan に関連付けられます。以下のテストには、新しい Ninja と既存の未編集の Clan を含むグラフが必要です。通常は Ninja.ClanId に値を割り当て、参照データとの混同を避けます。実際には、ナビゲーション プロパティではなく外部キーを設定するのが 1 つの方法です。これにより、EF が複数の関係にまたがる状態を解析する「処理」に起因する問題の多くが回避されます。詳細については、2013 年 4 月のコラム「Entity Framework が既存のオブジェクトをデータベースに再挿入する理由」(bit.ly/20XVxQi、英語) を参照してください。ただし、今回はこの方法でコードを作成して EF の動作のデモを行います。clan オブジェクトには、キー プロパティとして、データベースからの既存のデータであることを示す ID が設定されています。

[TestMethod]
public void EFDoesNotComprehendsMixedStatesWhenAddingUntrackedGraph() {
  var ninja = new Ninja();
  ninja.Clan = new Clan { Id = 1 };
  using (var context = new NinjaContext()) {
    context.Ninjas.Add(ninja);
    var entries = context.ChangeTracker.Entries();
    OutputState(entries);
    Assert.IsFalse(entries.Any(e => e.State != EntityState.Added));
  }
}

OutputState メソッドは、DbEntityEntry オブジェクトを反復処理します。DbEntityEntry オブジェクトでは、追跡する各エンティティの状態情報をコンテキストが保持し、State の型と値を出力します。

テストでは、新しい Ninja をどこかに作成して、既存の Clan と関連付けるシナリオのエミュレーションを行います。clan は単なる参照データで、編集されていません。次に、新しいコンテキストを作成し、DbSet.Add メソッドを使用して、このグラフを追跡するよう EF に指示します。ここでは、追跡されているエンティティがなく Added とマークされることをアサートしています。このアサーションに合格すると、コンテキストは Clan が Unchanged だと理解しなかったことが証明されます。以下のテスト出力は、両方のエンティティが Added であると EF が判断したことを示しています。

Result StandardOutput:
Debug Trace:
EF6WebAPI.Models.Ninja:Added
EF6WebAPI.Models.Clan:Added

そのため、SaveChanges を呼び出すと、Ninja と Clan の両方が挿入され、Clan が重複する結果になります。代わりに DbSet.Attach メソッドを使用すると、両方のエンティティは Unchanged とマークされ、SaveChanges は新しい Ninja をデータベースに挿入しないため、データ永続化で問題が発生します。

もう 1 つよくあるのが、データベースから Ninja とその Equipment を取得して、クライアントに渡すシナリオです。クライアントはその後 Equipment の一部を編集して、新しい Equipment を追加します。エンティティの本当の状態は、Ninja が Unchanged、Equipment の一部が Modified、他の Equipment が Added になります。DbSet.Add と DbSet.Attach はどちらも、自力では状態変化を理解できません。そこで、なんらかの支援を提供します。

各エンティティの状態を EF に通知

EF がグラフの各エンティティの正しい状態を理解できるようにする簡単な方策は、以下の 4 つの手順からなる解決策です。

  1. オブジェクトが取り得る状態を表す列挙値を定義する。
  2. 列挙値によって定義される ObjectState プロパティを備えたインターフェイスを作成する。
  3. ドメインのエンティティにこのインターフェイスを実装する。
  4. オブジェクトの状態を読み取り、EF に通知するように DbContext SaveChanges をオーバーライドする。

EF には、Unchanged、Added、Modified、Deleted という列挙子を含む EntityState 列挙値があります。今回は、ドメインのクラスで使用する別の列挙値を作成します。以下の列挙値は、4 つの状態を表していますが、Entity Framework API には結び付けられていません。

public enum ObjectState
{
  Unchanged,
  Added,
  Modified,
  Deleted
}

先頭の Unchanged が既定値になります。値を指定する場合は、Unchanged がゼロ (0) に等しくなるようにします。

次に、インターフェイスを作成し、この列挙値を使用してオブジェクトの状態を追跡するためのプロパティを公開します。基本クラスを作成しても、既に使用している基本クラスに追加してもかまいません。

public interface IObjectWithState
{
  ObjectState State { get; set; }
}

この State プロパティはメモリ内でのみ使用し、データベースに永続化する必要はありません。State プロパティを実装するすべてのオブジェクトでこのプロパティを無視するように NinjaContext を更新します。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
  modelBuilder.Types<IObjectWithState>().Configure(c => c.Ignore(p=>p.State));
}

定義したインターフェイスはクラスに実装できます。たとえば、Ninja クラスに実装します (図 1 参照)。

図 1 IObjectState を実装する Ninja クラス

public class Ninja : IObjectWithState
{
  public Ninja() {
    EquipmentOwned = new List<NinjaEquipment>();
  }
  public int Id { get; set; }
  public string Name { get; set; }
  public bool ServedInOniwaban { get; set; }
  public Clan Clan { get; set; }
  public int ClanId { get; set; }
  public List<NinjaEquipment> EquipmentOwned { get; set; }
  public ObjectState State { get; set; }
}

ObjectState 列挙値の既定値を Unchanged として定義しているので、すべての Ninja は Unchanged 状態から始まります。Ninja クラスを使用してコーディングする際に、必要に応じて State 値を設定します。

状態の設定をクライアントに委ねることが問題になる場合は、ドメイン駆動設計手法を取り入れた別のアプローチとして、Ninja オブジェクトの動作と状態への関与度合いを高めます。図 2 に、定義が多いバージョンの Ninja クラスを示します。以下の点に注意してください。

  • Create ファクトリ メソッドはどちらも State を Added に設定します。
  • プロパティのセッターは利用できないようにしています。
  • プロパティを変更するメソッドを作成し、新しい Ninja でない (つまり、State がまだ Added に設定されていない) 場合に State を Modified に設定しています。

図 2 高度な Ninja クラス

public class Ninja : IObjectWithState
{
  public static RichNinja CreateIndependent(string name, 
   bool servedinOniwaban) {
    var ninja = new Ninja(name, servedinOniwaban);
    ninja.State = ObjectState.Added;
    return ninja;
  }
  public static Ninja CreateBoundToClan(string name,
    bool servedinOniwaban, int clanId) {
    var ninja = new Ninja(name, servedinOniwaban);
    ninja.ClanId = clanId;
    ninja.State = ObjectState.Added;
    return ninja;
  }
  public Ninja(string name, bool servedinOniwaban) {
    EquipmentOwned = new List<NinjaEquipment>();
    Name = name;
    ServedInOniwaban = servedinOniwaban;
  }
  // EF needs parameterless ctor for queries
  private Ninja(){}
  public int Id { get; private set; }
  public string Name { get; private set; }
  public bool ServedInOniwaban { get; private set; }
  public Clan Clan { get; private set; }
  public int ClanId { get; private set; }
  public List<NinjaEquipment> EquipmentOwned { get; private set; }
  public ObjectState State { get; set; }
  public void ModifyOniwabanStatus(bool served) {
    ServedInOniwaban = served;
    SetModifedIfNotAdded();
  }
  private void SetModifedIfNotAdded() {
    if (State != ObjectState.Added) {
      State = ObjectState.Modified;
    }
  }
  public void SpecifyClan(Clan clan) {
    Clan = clan;
    ClanId = clan.Id;
    SetModifedIfNotAdded();
  }
  public void SpecifyClan(int id) {
    ClanId = id;
    SetModifedIfNotAdded();
  }
  public NinjaEquipment AddNewEquipment(string equipmentName) {
    return NinjaEquipment.Create(Id, equipmentName);
  }
  public void TransferEquipmentFromAnotherNinja(NinjaEquipment equipment) {
    equipment.ChangeOwner(this.Id);
  }
  public void EquipmentNoLongerExists(NinjaEquipment equipment) {
    equipment.State = ObjectState.Deleted;
  }
}

同様に、NinjaEquipment 型への関与度合いも高め、AddNew、Transfer、NoLongerExists の各 equipment メソッドでそのメリットを活かしています。この変更により、Ninja をポイントする外部キーが正しく永続化されるようになります。Equipment が破棄されている場合、この特定のドメインのビジネス ルールに従って、Equipment がデータベースから完全に削除されます。グラフを EF に再び結び付ける際に関係の変化を追跡するのはやや複雑なので、ドメイン レベルで関係を厳密に制御し続けられるようにします。たとえば、ChangeOwner メソッドは、State を Modified に設定します。

public NinjaEquipment ChangeOwner(int newNinjaId) {
  NinjaId = newNinjaId;
  State = ObjectState.Modified;
  return this;
}

これで、クライアントが状態を明示的に設定する場合も、クライアント側でこのようなクラス (または、クライアントの言語で同じようにコーディングされたクラス) を使用する場合も、API またはサービスに返されるオブジェクトはその状態が定義されます。

今度は、このクライアント側の状態をサーバー側のコードで利用します。

オブジェクトまたはオブジェクト グラフをコンテキストに結び付けたら、そのコンテキストが各オブジェクトの状態を読み取る必要があります。以下の ConvertState メソッドは ObjectState 列挙値を受け取り、対応する EntityState 列挙値を返します。

public static EntityState ConvertState(ObjectState state) {
  switch (state) {
    case ObjectState.Added:
      return EntityState.Added;
    case ObjectState.Modified:
      return EntityState.Modified;
    case ObjectState.Deleted:
      return EntityState.Deleted;
    default:
      return EntityState.Unchanged;
  }
}

次に、NinjaContext クラスにメソッドが必要です。このメソッドでは、(EF がデータを保存する直前に) エンティティをすべて反復処理し、オブジェクトの State プロパティに応じて各エンティティの状態についてのコンテキストの認識を更新します。このメソッドが以下に示す FixState です。

public class NinjaContext : DbContext
{
  public DbSet<Ninja> Ninjas { get; set; }
  public DbSet<Clan> Clans { get; set; }
  public void FixState() {
    foreach (var entry in ChangeTracker.Entries<IObjectWithState>()) {
      IObjectWithState stateInfo = entry.Entity;
      entry.State = DataUtilities.ConvertState(stateInfo.State);
    }
  }
}

すべてが自動的に行われるように、SaveChanges 内部から FixState を呼び出すことを考えましたが、多くのシナリオで副作用が生じる可能性があります。たとえば、ローカルの状態をわざわざ設定しない接続型のアプリケーションで IObjectState エンティティを使用する場合、FixState は常にエンティティを Unchanged に戻すことになります。FixState は、明示的に実行するメソッドにする方が無難です。Rowan Miller と共同執筆した『Programming Entity Framework: DbContext』(Oreilly & Associates Inc、2012 年) では、興味深い追加のエッジ ケースを取り上げています。

今度は、前述のテストの新しいバージョンを作成します。このテストでは、テスト内のクラスを高度なバージョンにするなど、新しい機能を使用します。新しいテストでは、既存の Clan に結び付けられている新しい Ninja の混在状態を、EF が理解することをアサートしています。テスト メソッドは、NinjaContext.FixState の呼び出し前後に EntityState を出力します。

[TestMethod]
public void EFComprehendsMixedStatesWhenAddingUntrackedGraph() {
  var ninja = Ninja.CreateIndependent("julie", true);
  ninja.SpecifyClan(new  Clan { Id = 1, ClanName = "Clan from database" });
  using (var context = new NinjaContext()) {
    context.Ninjas.Add(ninja);
    var entries = context.ChangeTracker.Entries();
    OutputState(entries);
    context.FixState();
    OutputState(entries);
    Assert.IsTrue(entries.Any(e => e.State == EntityState.Unchanged));
}

テストに合格すると、出力には、FixState メソッドが適切な状態を Clan に適用したことが示されます。SaveChanges を呼び出した場合、手違いにより、この Clan はデータベースに再挿入されなくなりました。

Debug Trace:
Before:EF6Model.RichModels.Ninja:Added
Before:EF6Model.RichModels.Clan:Added
After:EF6Model.RichModels.Ninja:Added
After:EF6Model.RichModels.Clan:Unchanged

上記のパターンを使用することにより、Ninja に編集されておらず、Equipment に多くの変更 (挿入、変更、または削除) が加えられている可能性があるという、前の Ninja グラフの問題も解決されます。図 3 に、エントリの 1 つが変更されたことを EF が正しく特定するかどうかを確認するテストを示します。

図 3 グラフ内の子の状態のテスト

[TestMethod]
public void MixedStatesWithExistingParentAndVaryingChildrenisUnderstood() {
  // Arrange
    var ninja = Ninja.CreateIndependent("julie", true);
    var pNinja =new PrivateObject(ninja);
    pNinja.SetProperty("Id", 1);
    var originalOwnerId = 99;
    var equip = Create(originalOwnerId, "arrow");
  // Act
    ninja.TransferEquipmentFromAnotherNinja(equip);
    using (var context = new NinjaContext()) {
      context.Ninjas.Attach(ninja);
      var entries = context.ChangeTracker.Entries();
      OutputState(entries);
      context.FixState();
      OutputState(entries);
  // Assert 
    Assert.IsTrue(entries.Any(e => e.State == EntityState.Modified));
  }
}

テストに合格すると、出力には、オリジナルの Attach メソッドがすべてのオブジェクトを Unchanged とマークしたことが示されます。FixState を呼び出した後、Ninja は Unchanged (正しいまま) ですが、equipment オブジェクトは Modified に正しく設定されます。

Debug Trace:
Before:EF6Model.RichModels.Ninja:Unchanged
Before:EF6Model.RichModels.NinjaEquipment:Unchanged
After:EF6Model.RichModels.Ninja:Added
After:EF6Model.RichModels.NinjaEquipment:Modified

EF Core とは

EF Core に移行しても、このパターンをそのまま利用できます。結び付きのないグラフの問題を単純化する方向へ大きく前進しました。ほとんどは、一貫性のあるパターンを提供するかたちです。EF Core では、DbContext.Entry().State プロパティを使用して状態を設定することは、グラフのルートの状態を設定することに他なりません。これは多くのシナリオにとってメリットがあります。また、TrackGraph という新しいメソッドがあり、、「グラフを移動」し、グラフ内のすべてのエンティティを探して、指定した関数を各メソッドに適用します。最も明白な関数は、単に状態を設定する関数です。

context.ChangeTracker.TrackGraph(Samurai_GK,
  e => e.Entry.State = EntityState.Added);

前述の FixState メソッドを使用して、クライアント側で設定する ObjectState に基づいて EF の状態を適用する関数へと、この関数を変更するところを想像してください。

クライアントの状態制御を単純化するリッチなドメイン モデル

必要に応じて状態を更新するリッチなドメイン クラスをビルドできますが、明示的に状態を設定するクラスをクライアントが使用する限り、シンプルな CRUD クラスを使用して同じ結果を実現できます。ただし、手動の場合は、変更済みの関係に細心の注意を払って、外部キーの変更を考慮するようにします。

長年このパターンを使用して、書籍、カンファレンス、顧客、Pluralsight コースで共有してきました。このパターンは、多くのソフトウェア ソリューションに使用できます。EF5 または EF6 を使用する場合でも、EF Core に適用する場合でも、この方策は結び付きのないデータに関連する大きな障害を取り除きます。

自己追跡エンティティ

EF4.1 のもう 1 つの特徴は、「自己追跡エンティティ」を生成する T4 テンプレートです。これにより、新たに導入された POCO も不要になります。自己追跡エンティティは、Windows Communication Foundation (WCF) サービスが .NET クライアントにデータを提供するシナリオを具体的に念頭において設計されました。自己追跡エンティティはあまり好みではなかったので、いつのまにか姿を消して安心していました。ですが、自己追跡エンティティを利用している開発者もいます。このメリットを提供する API がいくつかあります。たとえば、Tony Sneed が「追跡可能なエンティティ」という簡易実装をビルドしています (trackableentities.github.io、英語)。IdeaBlade (ideablade.com、英語) には、EF をサポートする主力製品の DevForce により、結び付きのないデータの問題を解決する高度な履歴を備えています。IdeaBlade はこの知識を活かして、クライアント側とサーバー側の状態追跡も提供するオープン ソースの Breeze.js 製品と Breeze# 製品を無償で提供しています。Breeze については、2012 年 12 月のコラム (bit.ly/1WpN0z3、英語) や 2014 年 4 月のコラム (https://msdn.microsoft.com/ja-jp/magazine/dn630644) を参照してください。


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

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