August 2016

Volume 31 Number 8

データ ポイント - EF Core の変更追跡の動作: 変更なし、変更済み、および追加

Julie Lerman

Julie Lerman今回のコラムのタイトルから、内容を推測できたでしょうか。 変更なし、変更済み、追加が、Entity Framework (EF) での Entity­State の列挙値だとわかりましたか。こうした列挙値は、EF Core の変更追跡動作を、以前のバージョンの Entity Framework と比べて説明する際に役立ちます。EF Core では、変更追跡の一貫性がより高められ、結び付きのないデータを操作している際に想定されることを、より確信を持って把握できます。

EF Core では、以前のバージョンの Entity Framework のパラダイムや構文の多くを維持するよう試みられていますが、EF Core の API は刷新されている (ゼロから作成されたまったく新しいコード ベースになっている) ことに注意してください。そのため、すべての機能が以前とまったく同じように動作すると想定しないことが重要です。その典型的で重要な例が、変更の追跡です。

EF Core の最初のイテレーションは ASP.NET Core と足並みを揃えることを目標としていました。そのため、帯域外から渡されるオブジェクトの状態を Entity Framework が処理できるように、結び付きのない状態に注目して多くの取り組みが行われました (つまり、EF ではこのようなオブジェクトが追跡されていませんでした)。この代表的なシナリオが、クライアント アプリケーションからオブジェクトを受け取ってデータベースに保持する Web API です。

2016 年 3 月号の「データ ポイント」コラムでは、「EF における結び付きのないエンティティの状態への対処」(msdn.com/magazine/mt694083) を取り上げました。今回は、結び付きのないエンティティに状態情報を割り当て、このようなオブジェクトを EF の変更追跡に渡して、状態情報を EF と共有することに注目します。ここでは EF6 を使用して例を示していますが、このパターンは EF Core にも関連するため、EF Core の動作に触れてから、EF Core でこのパターンを実装する方法の例を示します。

DbSet による追跡: 変更済み (Modified)

DbSet には、必ず Add メソッド、Attach メソッド、Remove メソッドが含まれています。こうしたメソッドを 1 つのオブジェクトに対して実行した結果はごく単純で、オブジェクトの状態を関連する EntityState に設定します。Add メソッドは追加 (Added)、Attach メソッドは変更なし (Unchanged) という結果になり、Remove メソッドは状態を削除済み (Deleted) に変更します。1 つ例外があり、既に追加 (Added) になっているエンティティを削除すると、新しいエンティティを追跡する必要がなくなるので、DbContext からデタッチされます。  EF6 では、このようなメソッドをグラフと共に使用する場合の関連オブジェクトへの影響には、まったく一貫性がありませんでした。以前に追跡されたことがないオブジェクトを削除することはできず、エラーがスローされます。既に追跡されているオブジェクトは、そのオブジェクトの状態に応じて、状態が変更される場合もされない場合もあります。さまざまな動作を理解できるように、EF6 で一連のテストを作成し、GitHub bit.ly/28YvwYd (英語) から入手できるようにしました。

EF Core の作成中、EF チームはベータ期間中に、このようなメソッドの動作を実験しました。EF Core の RTM 版のメソッドは、EF6 以前のメソッドのようには動作しなくなります。メソッドの大半に変更が加えられ、動作の一貫性が向上し、信頼性が高まっています。ただし、変更点を理解しておくことが重要です。

 グラフがアタッチされているオブジェクトと共に Add メソッド、Attach メソッド、および Remove メソッドを使用すると、変更追跡が把握していないグラフ内の各オブジェクトの状態が、メソッドによって決まる状態に設定されます。詳細については、映画「七人の侍」から引用するお気に入りの EF Core モデルを使用して説明します。このモデルでは、サムライ (samurai) に、映画のセリフと関連情報がアタッチされます。

サムライが新しく、追跡されていない状態であれば、Samurais.Add によって、そのサムライの状態は追加 (Added) に設定されます。Add を呼び出すときに、そのサムライにセリフをアタッチすると、セリフの状態も追加 (Added) に設定されます。これは想定どおりの動作で、実は EF6 の動作と同じです。

newQuote.SamuraiId に Samurai.Id の値を設定するという推奨事項に従わずに、newQuote.Samurai=oldSamurai を使ってナビゲーション プロパティを設定して既存のサムライに新しいセリフを追加するとどうなるでしょう。EF によってセリフも oldSamurai も追跡されない結び付きのないシナリオでは、Quotes.Add(newQuote) が前述と同じ処理を行います。つまり、newQuote も oldSamurai も追加 (Added) としてマークされます。SaveChanges は両方のオブジェクトをデータベースに挿入するため、重複する oldSamurai がデータベース内に存在するようになります。

たとえば、Windows Presentation Foundation (WPF) のようなクライアント アプリケーションに取り組んでおり、コンテキストを使用してサムライをクエリした後、同じコンテキストのインスタンスを使用して context.Quotes.Add(newQuote) を呼び出すと、コンテキストは既に oldSamurai を把握しているため、変更なし (Unchanged) の状態が追加 (Added) に変更されません。つまり、このような状況では、既に追跡されているオブジェクトの状態は変更されません。

変更追跡が、結び付きのないグラフ内の関連オブジェクトに影響を与える方法が大きく異なっているため、EF Core でこのようなメソッドを使用する場合は、こうした相違点に注意が必要です。

Rowan Miller が、GitHub の記事 (bit.ly/295goxw、英語) でこの新しい動作についてまとめています。

Add: まだ追跡されていないアクセス可能なあらゆるエンティティを追加します。

Attach: アクセス可能なエンティティがストア生成のキーを保持し、そのキー値が割り当てられていない場合を除いて、アクセス可能なあらゆるエンティティをアタッチします。キー値が割り当てられていない場合は、追加 (Added) としてマークされます。

Update: Attach と同じです。ただし、エンティティは変更済み (Modified) としてマークされます。

Remove: Attach と同じです。ルートを削除済み (Deleted) としてマークします。SaveChanges では連鎖削除が行われるようになったため、この設定により、後から連鎖のルールを各エンティティに適用できるようになります。

このメソッド一覧で示すように、DbSet のメソッドにはもう 1 つ変更が加えられています。 ようやく DbSet に Update メソッドが追加されました。これにより、追跡されていないオブジェクトの状態が変更済み (Modified) に設定されます。めでたしめでたし。 これにより、これまでいつも追加またはアタッチしてから明示的に状態を変更済み (Modified) に変更していた操作が必要なくなるため、これは優れた機能追加です。

DbSet の Range メソッド: 変更済み (Modified)

EF6 では、DbSet に 2 つの range メソッド (AddRange と RemoveRange) が導入され、類似の型の配列を渡すことができるようになりました。これにより、変更追跡は配列の各要素に対してではなく、配列全体を 1 回だけ処理すればよいので、パフォーマンスが大きく向上します。これらのメソッドは前述の Add と Remove を呼び出すので、関連グラフのオブジェクトが影響を受けるしくみを考慮する必要があります。

EF6 では、range メソッドが Add と Remove のみに存在しますが、EF Core では、UpdateRange と AttachRange も導入されています。Range メソッドに渡される各オブジェクトまたはグラフに対して呼び出される Update メソッドと Attach メソッドは、前述と同様に動作します。

DbContext の変更追跡のメソッド: 追加 (Added)

DbContext が導入される前に EF ObjectContext を操作した方は、ObjectContext に Add メソッド、Attach メソッド、Delete メソッドがあったのを覚えているかもしれません。コンテキストには、どの ObjectSet にターゲットのエンティティが属しているかを把握する方法がないので、パラメーターとして ObjectSet 名の文字列表記を追加しなければなりませんでした。これは非常に煩雑になるため、多くの開発者は ObjectSet の Add メソッド、Attach メソッド、Delete メソッドのみを使用するほうが簡単だと考えていました。DbContext が追加されたことで、このような煩雑なメソッドはなくなり、DbSet の Add メソッド、Attach メソッド、Remove メソッドを使用するだけになりました。

EF Core では、この Add メソッド、Attach メソッド、Remove メソッドは、DbContext のメソッドとして戻され、Update メソッドと 4 つの関連 Range メソッド (AddRange など) が追加されています。ただし、これらのメソッドはよりスマートになっており、型を判断したり、エンティティを適切な DbSet に自動的に関連付けることができるようになりました。これにより、DbSet のインスタンスを作成することなく、ジェネリック コードを記述できるようになるため、非常に便利です。コードはシンプルになり、さらに重要なことに、検出が容易になっています。EF6 と EF Core の比較を以下に示します。

private void AddToSetEF6<T>(T entity) where T : class {Pull
  using (var context = new SamuraiContext()) {
    context.Set<T>().Add(entity);
  }
}
private void AddToSetEFCore(object entity) {
  using (var context = new SamuraiContext()) {
    context.Add(entity);
   }
}

range メソッドは、さまざまな型を受け取って EF で処理できるため、より便利です。

private void AddViaContextEFCore(object[] entitiesOfVaryingTypes) {
  using (var context = new SamuraiContext()) {
     context.AddRange(entitiesOfVaryingTypes);
  }
}

DbContext.Entry: 変更済み (Modified) — 動作の変更に注意

EF Core は EF6 と異なるため、使い慣れたコードでも EF6 と同じように動作すると想定してはいけないと注意しましたが、繰り越されている動作も多いため、想定するなと言っても無理なことかもしれません。DbContext.Entry もその 1つで、どのように変更されたかを理解しておくことは重要です。

変更自体は変更追跡に一貫性をもたらすため、歓迎すべきものです。EF6 では、DbSet Add (などの) メソッドと DbContext.Entry メソッドを State プロパティと組み合わさせて、エンティティとグラフに対して同じ影響を与えていました。DbContext.Entry(object).State=EntityState.Added を使用すると、(まだ追跡されていない) グラフのすべてのオブジェクトが追加 (Added) に変更されます。

さらに、グラフ オブジェクトを変更追跡に渡す前に、結び付きのないグラフ オブジェクトが直感的にわかる方法はありませんでした。

EF Core の DbContext.Entry は、渡されたオブジェクトにのみ影響を与えるようになりました。このオブジェクトに他の関連オブジェクトが結び付いていても、DbContext.Entry はそれらのオブジェクトを無視します。

Entry メソッドを使用してグラフを DbContext のインスタンスに結び付けることに慣れている開発者は、これが大幅な変更である理由を理解できるでしょう。これは、グラフ内に含まれているオブジェクトでも個別にターゲットにできることを意味します。

さらに重要なのは、DbSet や DbContext の追跡メソッド (Add など) を明示的に使用して、明示的にグラフを操作でき、DbContext.Entry メソッドを使用して、個々のオブジェクトを具体的に操作できることです。これを、次に説明する変更と組み合わせると、オブジェクトのグラフを EF Core の変更追跡に渡すときに明確なオプションを利用できるようになります。

DbContext.ChangeTracker.TrackGraph: 追加 (Added)

TrackGraph は、EF Core で初めて採用された考え方です。これは、DbContext で追跡を開始するオブジェクト グラフの各オブジェクトを完全に制御できるようにします。

TrackGraph はグラフ全体を処理 (つまり、グラフ内の各オブジェクトを反復処理) し、各オブジェクトに、指定した関数を適用します。適用する関数は、TrackGraph メソッドの 2 つ目のパラメーターで指定します。

最も一般的なのは、各オブジェクトの状態を共通の状態に設定する例です。以下のコードは、TrackGraph が newSword グラフのすべてのオブジェクトを反復処理し、その状態をすべて追加 (Added) に設定します。

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

DbSet のメソッドと DbContext のメソッドと同じ注意点が TrackGraph にも当てはまり、エンティティが既に追跡されている場合、TrackGraph はそのエンティティを無視します。TrackGraph のこの特別な動作は、DbSet の追跡メソッドと DbContext の追跡メソッドと同様に機能しますが、再利用可能なコードを記述するチャンスが広がります。

ラムダ (上記のコードの "e") は、EntityEntryGraphNode 型を表します。EntityEntryGraphNode 型は、NodeType というプロパティも公開しており、このラムダを入力する際に IntelliSense に表示されます。これは内部使用を目的とし、e.Entry.State が提供する効果がなくなるため、無意識に使用しないよう注意してください。

結び付きのないシナリオでは、既に追跡されているオブジェクトが無視されるという注意点は、該当しない可能性があります。それは、DbContext の新しいインスタンスが空になるためです。したがって、グラフ内のすべてのオブジェクトが DbContext にとって新しいものになります。ただし、グラフのコレクションを Web API に渡す可能性について考慮してください。現在は、共通オブジェクトへの参照が複数存在できるチャンスがあります。EF の変更追跡は ID をチェックして、エンティティが既に追跡されているかどうかを判断することになります。これにより、変更追跡にオブジェクトを重複して追加しないようにすることができます。

この既定の動作は、最も一般的なシナリオに対処できるように設計されています。ただし、このパターンが失敗するエッジ ケースを既に思い付いている開発者がいることも想像に難くありません。

そこで、2016 年 3 月のコラムを振り返ります。クラス オブジェクトの状態を設定後、その状態を読み取って変更追跡にオブジェクトの EntityState のあるべき姿を伝えるパターンを再確認してください。現在は、このパターンと TrackGraph メソッドを組み合わせることができるようになります。そのためには、TrackGraph から呼び出される関数が、オブジェクトの State メソッドに基づいて EntityState を設定するタスクを実行するようにします。

domain クラスでの作業は、3 月号のコラムで実行した作業と同じです。まず、ローカルに追跡する ObjectState の列挙値を定義します。

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

次に、列挙値に基づいて State プロパティを公開する IEntityObjectWithState インターフェイスをビルドします。

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

続いて、クラスを修正してこのインターフェイスを実装します。例として、このインターフェイスを実装した Location の小さなクラスを以下に示します。

using SamuraiTracker.Domain.Enums;
using SamuraiTracker.Domain.Interfaces;
namespace SamuraiTracker.Domain
{
  public class Location : IObjectWithState
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public ObjectState State { get; set; }
  }
}

3 月号のコラムでは、ローカルの状態を管理できるインテリジェントなクラスをビルドする方法について説明しました。今回の例では、同じことは繰り返しません。今回のサンプルでは、セッターをパブリックのままにして、その状態を手動で設定します。具体的なソリューションでは、このようなクラスをより簡素化して、前述のコラムのようなクラスにします。

DbContext の場合、ChangeTrackerHelpers というヘルパー クラスに静的メソッドを用意します (図 1 参照)。

図 1 ChangeTrackerHelpers クラス

public static class ChangeTrackerHelpers
    {
    public static void ConvertStateOfNode(EntityEntryGraphNode node) {
      IObjectWithState entity = (IObjectWithState)node.Entry.Entity;
      node.Entry.State = ConvertToEFState(entity.State);
    }
    private static EntityState ConvertToEFState(ObjectState objectState) {
      EntityState efState = EntityState.Unchanged;
      switch (objectState) {
        case ObjectState.Added:
          efState = EntityState.Added;
          break;
        case ObjectState.Modified:
          efState = EntityState.Modified;
          break;
        case ObjectState.Deleted:
          efState = EntityState.Deleted;
          break;
        case ObjectState.Unchanged:
          efState = EntityState.Unchanged;
          break;
      }
      return efState;
    }
  }

ConvertStateOfNode は、TrackGraph から呼び出されるメソッドです。オブジェクトの EntityState を ConvertToEFState メソッドによって決定される値に設定します。このメソッドは、IObjectWithState.State 値を EntityState 値に変換します。

こうすることで、TrackGraph を使用して、オブジェクトおよびそのオブジェクトに適切に割り当てられた EntityStates の追跡を開始できるようになります。関連するセリフと刀が設定されたサムライで構成される、samurai というオブジェクト グラフを渡す例を以下に示します。

context.ChangeTracker.TrackGraph(samurai, ChangeTrackerHelpers.ConvertStateOfNode);

EF6 ソリューションでは、項目を変更追跡に追加した後、変更追跡のすべてのエントリを反復処理するメソッドを明示的に呼び出して、各オブジェクトの関連状態を設定する必要がありました。EF Core のソリューションははるかに効率的です。ただし、単一のトランザクションで大量のデータを処理する際に起こりうるパフォーマンスの影響については、未調査ですのでご注意ください。

本稿付属のサンプル コードをダウンロードすると、Can­ApplyStateViaChangeTracker という統合テスト内でこの新しいパターンを使用していることがわかります。この CanApplyStateViaChangeTracker では、このグラフを作成し、さまざまな状態を異なるオブジェクトに割り当てた後、結果として得られる EntityState 値が正確であることを検証します。

IsKeySet: 追加 (Added)

エンティティごとに追跡情報を保持する EntityEntry オブジェクトには、IsKeySet という新しいプロパティがあります。IsKeySet は、API への優れた追加です。IsKeySet は、エンティティの key プロパティに値が設定されているかどうかをチェックします。これは、推測処理 (および関連するコード) を取り除き、オブジェクトの key プロパティ (または構成キーがある場合は複数のプロパティ) に値が既に設定されているかどうかをチェックします。IsKeySet は、値が、key プロパティに指定した特定の型の既定値かどうかをチェックします。型が int の場合、値が 0 かどうかをチェックします。 型が GUID の場合、Guid.Empty (00000000-0000-0000-0000-000000000000) かどうかをチェックします。 値が、その型の既定値でない場合、IsKeySet は true を返します。

キーのプロパティによって、システムで既存のオブジェクトと新しいオブジェクトを明白に区別できることがわかっている場合、IsKeySet は、エンティティの状態を決定するのに非常に便利なプロパティになります。

注意して EF Core に取り組む

EF チームは、以前のバージョンの Entity Framework から EF Core への切り替えに伴う負担を軽減するためにできることを確かに実行しました (多くの構文や動作を複製しました) が、API はまったく異なることには注意が必要です。使い慣れた機能のサブセットしか RTM 版に導入されていないこうした黎明期には、コードの移植は複雑になるためお勧めしません。EF Core の機能セットに必要なものが含まれていると確信を持って新しいプロジェクトに取り組む場合でも、同じように動作するとは想定しないようにします。この注意点は、依然として心がけておく必要があります。とはいえ、変更追跡に対する変更は歓迎しています。このような変更は、結び付きのないデータを処理するための明確性、一貫性、および制御性を向上します。

便利なショートカット bit.ly/efcoreroadmap (英語) を利用して、EF チームが GitHub ページに公開しているロードマップを参照してください。このロードマップでは機能を追跡できますが、動作の変更のような細かい情報は記載されていません。そのため、数多くのテストを実行して、想定どおり動作するかどうかを確認することをお勧めします。以前のバージョンの EF からコードを移植することを計画している場合は、Llewellyn Falco の Approval Tests (approvaltests.com、英語) を参照してください。ここでは、テストの結果を比較して、結果が一致しているかどうかを確認できます。


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 コースをご覧ください。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Erik Ejlskov Jensen に心より感謝いたします。
Erik Ejlskov Jensen は、NNIT A/S の .NET およびデータベースの開発者であり、Microsoft データ プラットフォームの MVP でもあります。彼は、GitHub の .NET データベース開発者を対象に、Visual Studio の人気の拡張機能である "SQLite & SQL Server Compact Toolbox" など、無償のオープン ソースのツールやライブラリを提供しています。また、Entity Framework Core 用にデータベース プロバイダーも作成しています。彼の Twitter @ErikEJ (英語) をフォローして、.NET データ アクセス開発の最新情報を参照してください。