変更検出と通知

DbContext インスタンスによって、エンティティに加えられる変更が追跡されます。 さらに、これらの追跡対象エンティティによって、SaveChanges が呼び出されたときにデータベースへの変更が実行されます。 このことについては「EF Core での変更の追跡」で説明されています。このドキュメントは、エンティティの状態と Entity Framework Core (EF Core) の変更追跡の基本を理解していることを前提として書かれています。

プロパティとリレーションシップの変更を追跡するには、DbContext からこれらの変更を検出できる必要があります。 このドキュメントでは、この検出がどのように行われるか、また、プロパティ通知や変更追跡プロキシを使って変更の即時検出を強制する方法について説明します。

ヒント

このドキュメントに含まれているすべてのコードは、GitHub からサンプル コードをダウンロードすることで実行およびデバッグできます。

スナップショットによる変更の追跡

EF Core の既定では、DbContext インスタンスによって初めて追跡されたときに、すべてのエンティティのプロパティ値のスナップショットが作成されます。 このスナップショットに格納された値は、変更されたプロパティ値を判断するために、エンティティの現在の値と比較されます。

データベースに更新が送信される前にすべての変更された値が確実に検出されるように、この変更検出は SaveChanges が呼び出されたときに行われます。 ただし、アプリケーションが最新の追跡情報を使って機能していることを確認するために、他のタイミングで変更検出が行われることもあります。 変更検出は、ChangeTracker.DetectChanges() を呼び出すことで、いつでも強制的に行うことができます。

変更検出が必要になる場合

変更検出が必要になるのは、プロパティまたはナビゲーションが EF Core を使わずに変更された場合です。 たとえば、ブログと記事を読み込み、これらのエンティティに変更を加えることを検討してください。

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

ChangeTracker.DetectChanges() を呼び出す前の変更トラッカーのデバッグ ビューを見ると、加えられた変更が検出されないため、エンティティの状態や変更されたプロパティ データに反映されていないことがわかります。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, <not found>]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

具体的には、ブログ エントリの状態は Unchanged のままであり、新しい投稿は追跡対象のエンティティとして表示されません (注意深く見ると、これらの変更がまだ EF Core によって検出されていないにもかかわらず、プロパティから新しい値が報告されていることがわかります。これは、デバッグ ビューにエンティティ インスタンスから現在の値が直接読み取られているためです)。

これを DetectChanges を呼び出した後のデバッグ ビューと比較してください。

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

これでブログは正しく Modified とマークされ、新しい投稿は検出されて Added として追跡されるようになりました。

このセクションの冒頭で、変更を加えるために EF Core を使用しない場合に変更検出が必要であると説明しました。 これが上のコードで起きていることです。 つまり、プロパティとナビゲーションの変更はエンティティ インスタンスに対して直接行われており、EF Core メソッドを使っていません。

次のコードは同じようにエンティティを変更していますが、今回は EF Core メソッドを使っています。

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
context.Entry(blog).Property(e => e.Name).CurrentValue = ".NET Blog (Updated!)";

// Add a new entity to the DbContext
context.Add(
    new Post
    {
        Blog = blog,
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

このケースでは、変更検出が行われていないにもかかわらず、変更トラッカーのデバッグ ビューには、すべてのエンティティの状態とプロパティの変更が既知であると表示されます。 これは、PropertyEntry.CurrentValue が EF Core メソッドであるためです。つまり、このメソッドによる変更は EF Core にすぐに認識されます。 同様に、DbContext.Add を呼び出すと、EF Core によって新しいエンティティがすぐに認識され、適切に追跡されます。

ヒント

エンティティの変更に常に EF Core メソッドを使って変更検出を回避しようとしないでください。 そうすると、通常の方法でエンティティを変更するよりも面倒で、パフォーマンスが低下することがよくあります。 このドキュメントの意図は、変更検出が必要な場合とそうでない場合について説明することです。 変更検出を回避することを推奨することではありません。

変更を自動的に検出するメソッド

DetectChanges() は、実行することで結果に影響する可能性が高いメソッドから自動的に呼び出されます。 それらの方法を次に示します。

また、追跡されるエンティティのグラフ全体ではなく、1 つのエンティティ インスタンスのみについて変更検出が行われる場合もあります。 次のような場合です。

  • DbContext.Entry。エンティティの状態と変更されたプロパティが最新であることを確認するために使われます。
  • EntityEntry メソッド (PropertyCollectionReferenceMember など) を使い、プロパティの変更、現在の値などが最新であることを確認するために使われます。
  • 必要なリレーションシップが切断されたために、依存または子エンティティが削除される場合。 これにより、エンティティの親が変更されたためにエンティティを削除すべきではない場合が検出されます。

1 つのエンティティに対する変更のローカル検出を明示的にトリガーするには、EntityEntry.DetectChanges() を呼び出します。

Note

ローカルの変更検出では、完全な検出なら見つかる変更が見逃される可能性があります。 これは、他のエンティティに対する検出されなかった変更に起因するカスケード アクションが、当該エンティティに影響する場合に起こります。 このような状況では、必要に応じてアプリケーションから ChangeTracker.DetectChanges() を明示的に呼び出し、すべてのエンティティのフル スキャンを強制する必要があります。

自動変更検出の無効化

変更検出のパフォーマンスは、ほとんどのアプリケーションにとってボトルネックではありません。 ただし、何千ものエンティティを追跡する一部のアプリケーションの場合、変更検出がパフォーマンスの問題になることがあります (正確な数は、エンティティのプロパティ数など、さまざまなものによって変わります)。このため、ChangeTracker.AutoDetectChangesEnabled を使って自動変更検出を無効にすることができます。 たとえば、多対多の関係にある結合エンティティをペイロードで処理することを検討してください。

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // Detects changes automatically
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
            entityEntry.Entity.TaggedOn = DateTime.Now;
        }
    }

    try
    {
        ChangeTracker.AutoDetectChangesEnabled = false;
        return base.SaveChanges(); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

前のセクションからわかるように、ChangeTracker.Entries<TEntity>()DbContext.SaveChanges の両方で変更が自動的に検出されます。 ただし、Entries を呼び出した後は、コードによってエンティティやプロパティの状態は変更されません (Added エンティティに通常のプロパティ値を設定しても、状態は変化しません)。そのため、このコードでは、ベースの SaveChanges メソッドを呼び出すときに不要な自動変更検出を無効にしています。 また、このコードでは、SaveChanges が失敗しても、既定の設定が復元されるように try/finally ブロックを使っています。

ヒント

コードがうまく機能するには、自動変更検出を無効にする必要があると思い込まないでください。 これは、多くのエンティティを追跡するアプリケーションのプロファイリング時に、変更検出のパフォーマンスが問題となる場合にのみ必要です。

変更と値変換の検出

エンティティ型と共にスナップショットの変更追跡を使うには、EF Core から次のことができる必要があります。

  • エンティティの追跡時に、各プロパティ値のスナップショットを作成する
  • この値をプロパティの現在の値と比較する
  • 値のハッシュ コードを生成する

データベースに直接マップできる型の場合、これは EF Core によって自動的に処理されます。 ただし、値コンバーターを使ってプロパティをマップする場合、そのコンバーターでそのアクションの実行方法を指定する必要があります。 これは値の比較演算子を使って実現されます。詳細については、「値の比較演算子」のドキュメントを参照してください。

通知エンティティ

スナップショット変更追跡は、ほとんどのアプリケーションで推奨されています。 ただし、追跡するエンティティ数が多いアプリケーションやそのようなエンティティに多くの変更を加えるアプリケーションの場合は、プロパティとナビゲーションの値が変更されたときに EF Core に自動的に通知するエンティティを実装すると役立つ場合があります。 これらは "通知エンティティ" と呼ばれます。

通知エンティティの実装

通知エンティティには、.NET の基本クラス ライブラリ (BCL) の一部である INotifyPropertyChangingINotifyPropertyChanged のインターフェイスが使われます。 これらのインターフェイスには、プロパティ値の変更前後に常に発生するイベントが定義されています。 次に例を示します。

public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private int _id;

    public int Id
    {
        get => _id;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
            _id = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
        }
    }

    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
            _name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

さらに、すべてのコレクション ナビゲーションには INotifyCollectionChanged を実装する必要があります。上の例では、post の ObservableCollection<T> を使ってこれを満たしています。 EF Core には、安定した順序付けを犠牲にしてより効率的に検索する ObservableHashSet<T> の実装も付属しています。

この通知コードのほとんどは、マップされていない基底クラスに移動されます。 次に例を示します。

public class Blog : NotifyingEntity
{
    private int _id;

    public int Id
    {
        get => _id;
        set => SetWithNotify(value, out _id);
    }

    private string _name;

    public string Name
    {
        get => _name;
        set => SetWithNotify(value, out _name);
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
    protected void SetWithNotify<T>(T value, out T field, [CallerMemberName] string propertyName = "")
    {
        NotifyChanging(propertyName);
        field = value;
        NotifyChanged(propertyName);
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private void NotifyChanging(string propertyName)
        => PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

通知エンティティの構成

EF Core で使うために INotifyPropertyChangingINotifyPropertyChanged が完全に実装されていることを EF Core 側で検証する方法はありません。 特に、このようなインターフェイスの一部の用途では、EF Core に必要なすべてのプロパティ (ナビゲーションを含む) ではなく、特定のプロパティについてのみ通知が行われます。 この理由から、EF Core によってこれらのイベントに自動的にフックされません。

代わりに、これらの通知エンティティを使うように EF Core を構成する必要があります。 これは通常、ModelBuilder.HasChangeTrackingStrategy を呼び出すことによって、すべてのエンティティ型に対して行われます。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}

(EntityTypeBuilder.HasChangeTrackingStrategy を使ってエンティティ型ごとに異なる戦略を設定することもできますが、通知エンティティではない型には DetectChanges がまだ必要なので、通常、これは逆効果です。)

完全な通知変更追跡には、INotifyPropertyChangingINotifyPropertyChanged の両方が実装されている必要があります。 これにより、プロパティ値が変更される直前に元の値を保存できるので、エンティティを追跡するときに EF Core からスナップショットを作成する必要がなくなります。 INotifyPropertyChanged のみを実装したエンティティ型を EF Core で使うこともできます。 この場合、元の値を追跡するためにエンティティの追跡時に EF によってスナップショットが作成されますが、DetectChanges を呼び出す必要はなく、通知を使って変更をすぐに検出することができます。

次の表は、さまざまな ChangeTrackingStrategy の値をまとめたものです。

ChangeTrackingStrategy 必要なインターフェイス DetectChanges が必要 スナップショットの元の値
スナップショット なし 有効 はい
ChangedNotifications INotifyPropertyChanged いいえ はい
ChangingAndChangedNotifications INotifyPropertyChanged と INotifyPropertyChanging いいえ いいえ
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged と INotifyPropertyChanging いいえ はい

通知エンティティの使用

通知エンティティは、他のエンティティと同様に動作しますが、エンティティ インスタンスに変更を加えても、その変更を検出するための ChangeTracker.DetectChanges() の呼び出しは必要ありません。 次に例を示します。

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

通常のエンティティの場合、DetectChanges が呼び出されるまで、変更トラッカーのデバッグ ビューにはこれらの変更が検出されませんでした。 通知エンティティが使われているときにデバッグ ビューを見ると、これらの変更がすぐに検出されていることがわかります。

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

変更追跡のプロキシ

EF Core では、INotifyPropertyChangingINotifyPropertyChanged を実装するプロキシの種類を動的に生成できます。 このためには、Microsoft.EntityFrameworkCore.Proxies NuGet パッケージをインストールし、UseChangeTrackingProxies を使って変更追跡プロキシを有効にする必要があります。次に例を示します。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseChangeTrackingProxies();

動的プロキシを作成するには、(Castle.Core プロキシの実装を使って) 新しい動的 .NET 型を作成する必要があります。これはエンティティ型から継承され、すべてのプロパティ セッターがオーバーライドされます。 そのため、プロキシのエンティティ型は継承できる型であり、オーバーライドできるプロパティを持っている必要があります。 また、明示的に作成するコレクション ナビゲーションには INotifyCollectionChanged を実装する必要があります。次に例を示します。

public class Blog
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }

    public virtual IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public class Post
{
    public virtual int Id { get; set; }
    public virtual string Title { get; set; }
    public virtual string Content { get; set; }

    public virtual int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

変更追跡プロキシの重大な欠点の 1 つは、EF Core によって常にプロキシのインスタンスを追跡し、基となるエンティティ型のインスタンスは追跡しない必要があることです。 これは、基となるエンティティ型のインスタンスからは通知が生成されないためです。つまり、このようなエンティティに加えられた変更は見逃されます。

データベースへのクエリ実行時に EF Core によって自動的にプロキシ インスタンスが作成されるので、通常、この欠点は新しいエンティティ インスタンスの追跡に限定されます。 これらのインスタンスは CreateProxy 拡張メソッドを使って作成する必要があります。new を使った通常の方法ではありません。 つまり、前の例のコードには CreateProxy を使用する必要があります。

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    context.CreateProxy<Post>(
        p =>
        {
            p.Title = "What’s next for System.Text.Json?";
            p.Content = ".NET 5.0 was released recently and has come with many...";
        }));

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

変更追跡イベント

EF Core によってエンティティが初めて追跡されるときに、ChangeTracker.Tracked イベントが発生します。 今後エンティティの状態が変更されると、ChangeTracker.StateChanged イベントが発生します。 詳細については、「EF Core での .NET イベント」を参照してください。

Note

状態が Detached から他のいずれかの状態に変更された場合でも、エンティティが初めて追跡されたときに StateChanged イベントは発生しません。 関連するすべての通知を受け取るには、StateChangedTracked の両方のイベントをリッスンしてください。