次の方法で共有


変更の検出と通知

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

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

ヒント

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

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

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

この変更の検出は、SaveChanges が呼び出されたときに発生し、変更されたすべての値がデータベースに更新を送信する前に検出されるようにします。 ただし、変更の検出は、アプリケーションが up-to-date 追跡情報を使用していることを確認するために、他の場合にも発生します。 変更の検出は、 ChangeTracker.DetectChanges()を呼び出すことによっていつでも強制できます。

変更検出が必要な場合

この変更を 行うために EF Core を使用せずにプロパティまたはナビゲーションが変更された場合は、変更の検出が必要です。 たとえば、ブログや投稿を読み込んでから、次のエンティティに変更を加えてみましょう。

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(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 = await context.Blogs.Include(e => e.Posts).FirstAsync(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を使用する場合、エンティティの状態と変更されたプロパティがup-to-dateであることを確認します。
  • EntityEntryPropertyCollectionReferenceなどのMemberメソッドを使用して、プロパティの変更、現在の値などを -date に up-toする場合。
  • 必要なリレーションシップが切断されたために依存/子エンティティが削除される場合。 この機能は、エンティティの親が変更されたために削除すべきでない場合を検出します。

1 つのエンティティの変更のローカル検出は、 EntityEntry.DetectChanges()を呼び出すことによって明示的にトリガーできます。

ローカル検出の変更は、完全な検出で検出される変更の一部を見逃す可能性があります。 これは、検出されなかった他のエンティティへの変更によって発生する連鎖アクションが、問題のエンティティに影響を与える場合に発生します。 このような状況では、アプリケーションが明示的に ChangeTracker.DetectChanges()を呼び出すことによって、すべてのエンティティのフル スキャンを強制する必要がある場合があります。

変更の自動検出を無効にする

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

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    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 await base.SaveChangesAsync(cancellationToken); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

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

ヒント

コードが適切に動作するためには、自動変更検出を無効にする必要があると想定しないでください。 これは、多くのエンティティを追跡するアプリケーションのプロファイリングが、変更検出のパフォーマンスが問題であることを示している場合にのみ必要です。

変更と値の変換の検出

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

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

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

通知エンティティ

ほとんどのアプリケーションでは、スナップショット変更の追跡をお勧めします。 ただし、多数のエンティティを追跡したり、それらのエンティティに多くの変更を加えたりするアプリケーションでは、プロパティとナビゲーション値が変更されたときに EF Core に自動的に通知するエンティティを実装するとメリットがあります。 これらは"通知エンティティ" と呼ばれます。

通知エンティティの実装

通知エンティティは、.NET 基本クラス ライブラリ (BCL) の一部である INotifyPropertyChanging インターフェイスと INotifyPropertyChanged インターフェイスを利用します。 これらのインターフェイスは、プロパティ値を変更する前と後に発生する必要があるイベントを定義します。 例えば次が挙げられます。

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を実装する必要があります。上記の例では、投稿の 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 で使用するために INotifyPropertyChanging または INotifyPropertyChanged が完全に実装されていることを 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 値を次の表にまとめます。

変更追跡戦略 必要なインターフェイス DetectChanges が必要 スナップショットの元の値
スナップショット 無し イエス イエス
変更された通知 INotifyPropertyChanged いいえ イエス
変更中および変更後の通知 INotifyPropertyChanged と INotifyPropertyChanging いいえ いいえ
変更および変更済み通知と元の値 INotifyPropertyChanged と INotifyPropertyChanging いいえ イエス

通知エンティティの使用

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

using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(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();

動的プロキシを作成するには、エンティティ型から継承し、すべてのプロパティ セッターをオーバーライドする新しい動的な .NET 型 ( Castle.Core プロキシの実装を使用) を作成する必要があります。 したがって、プロキシのエンティティ型は継承可能な型である必要があり、オーバーライドできるプロパティが必要です。 また、明示的に作成されたコレクション ナビゲーションでは、 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 = await context.Blogs.Include(e => e.Posts).FirstAsync(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 イベント 」を参照してください。

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