前提となる環境
- Visual Studio 2022
- .NET 6
- Entity Framework Core 6.0.0
- SQL Server LocalDB
Change Tracker とは何か
Entity Framework Core は、 Change Tracker という仕組みを持っています。
Change Tracker は、 DbContext を経由して取得したデータに対して、更新や削除、追加などの操作を行った記録を保持する役割を持ちます。
DbContext.SaveChanges()
メソッドを呼び出しただけで、 DbContext に対して行った変更がすべて反映できるのは、 Change Tracker のおかげといっても過言ではありません。
Change Tracker は思わぬ不具合を生みやすい
この Change Tracker という仕組みは、わかって使うと非常に便利な仕組みです。 しかし、ロジック内で DbContext から取得したオブジェクトの値を不用意に書き換えてしまうと、 Change Tracker の追跡対象になってしまって、意図しない更新クエリが発行されてしまいます。 このようなケースでは、 Change Tracker がどのような状態になっているか、意図した変更記録が残されているか、デバッグしながら確認することになります。
Change Tracker が追跡している変更記録を確認する
Change Trakcer の状態を確認するには、 DbContext.ChangeTracker.DebugView.LongView
または DbContext.ChangeTracker.DebugView.ShortView
を参照しましょう。
現在の Change Tracker の状態をすべて表示してくれます。
LongView
のほうは、各レコードのカラム単位まで、どのような状態になっているかを表示できます。
ShortView
はレコードの単位でしか把握することができません。
実際に動かしてみる
DebugView
の動作を確認してみましょう。
今回は以下のような DB モデルを作りました。
namespace ChangeTrackerConfirm.Models; public class Category { public Category() { this.Name = string.Empty; this.Products = new HashSet<Product>(); } public long Id { get; set; } public string Name { get; set; } public ICollection<Product> Products { get; set; } } public class Product { public Product() => this.Name = string.Empty; public long Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public long CategoryId { get; set; } public Category? Category { get; set; } }
DbContext は以下のようにしています。
using Microsoft.EntityFrameworkCore; namespace ChangeTrackerConfirm.Models; public class SampleDbContext : DbContext { public SampleDbContext() { } public DbSet<Category> GetCategories { get; set; } public DbSet<Product> Products { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { ArgumentNullException.ThrowIfNull(optionsBuilder); if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=SampleDatabase;Integrated Security=True"); } } // 後略
このような DbContext に対して、データの取得と変更、新規追加を行いながら、DbContext.ChangeTracker.DebugView.LongView
をコンソールに出力してみます。
カテゴリー ID が 1 のデータを取得して、全レコードの価格を 1.1 倍に更新し、新たなレコードを 1 件追加しています。
using ChangeTrackerConfirm.Models; using var dbContext = new SampleDbContext(); ShowChangeTracker(1, dbContext); var products = dbContext.Products .Where(p => p.CategoryId == 1) .ToList(); ShowChangeTracker(2, dbContext); foreach (var product in products) { product.Price = product.Price * 1.1M; } ShowChangeTracker(3, dbContext); var newProduct = new Product { Name = "Entity Framework の本", Price = 2800, CategoryId = 1, }; dbContext.Products.Add(newProduct); ShowChangeTracker(4, dbContext); dbContext.SaveChanges(); ShowChangeTracker(5, dbContext); void ShowChangeTracker(int count, SampleDbContext dbContext) { Console.WriteLine($"= No.{count} ======================"); Console.WriteLine(dbContext.ChangeTracker.DebugView.ShortView); Console.WriteLine("============================="); }
実行結果は以下のようになります。
= No.1 ====================== ============================= = No.2 ====================== Product {Id: 1} Unchanged Id: 1 PK CategoryId: 1 FK Name: 'C#の本' Price: 2000 Category: <null> Product {Id: 2} Unchanged Id: 2 PK CategoryId: 1 FK Name: 'F#の本' Price: 1800 Category: <null> ============================= = No.3 ====================== Product {Id: 1} Unchanged Id: 1 PK CategoryId: 1 FK Name: 'C#の本' Price: 2200.0 Originally 2000 Category: <null> Product {Id: 2} Unchanged Id: 2 PK CategoryId: 1 FK Name: 'F#の本' Price: 1980.0 Originally 1800 Category: <null> ============================= = No.4 ====================== Product {Id: -9223372036854774807} Added Id: -9223372036854774807 PK Temporary CategoryId: 1 FK Name: 'Entity Framework の本' Price: 2800 Category: <null> Product {Id: 1} Unchanged Id: 1 PK CategoryId: 1 FK Name: 'C#の本' Price: 2200.0 Originally 2000 Category: <null> Product {Id: 2} Unchanged Id: 2 PK CategoryId: 1 FK Name: 'F#の本' Price: 1980.0 Originally 1800 Category: <null> ============================= = No.5 ====================== Product {Id: 1} Unchanged Id: 1 PK CategoryId: 1 FK Name: 'C#の本' Price: 2200.0 Category: <null> Product {Id: 2} Unchanged Id: 2 PK CategoryId: 1 FK Name: 'F#の本' Price: 1980.0 Category: <null> Product {Id: 12} Unchanged Id: 12 PK CategoryId: 1 FK Name: 'Entity Framework の本' Price: 2800 Category: <null> =============================
No.1 のタイミングでは、 DbContext のオブジェクトを作っただけなので、 Change Tracker は何も追跡しておらず、データも空っぽです。
No.2 は、データベースに対して問い合わせを行った後のタイミングで表示しています。 Change Trakcer は、取得したデータを追跡し続けます。 よってこのタイミングでは、追跡対象のオブジェクトとして認識されているものの、データは変更していないため、 Unchanged とマークされます。
No.3 は、価格のデータをすべて 1.1 倍した後のタイミングで表示しています。
Price
の状態を見ると、「Price: 2200.0 Originally 2000」のような記載になっており、データを 2000 から 2200 へ、 1.1 倍した様子を確認できます。
No.4 は、新たな商品データを追加した後のタイミングで表示しています。 「Entity Framework の本」のレコードが Added にマークされており、新規追加されたレコードであることを確認できます。 今回 Id はデータベース側で採番するよう設定しているため、 Id は「PK Temporary」が設定され、値も long の最小値が入っています。
No.5 は、 DbContext.SaveChanges()
メソッドを呼び出した後のタイミングで表示しています。
ここまでに行ってきたデータの編集と新規レコードの追加は、 SaveChanges メソッドを呼び出すことでデータベースに反映されます。
そのため、 DbContext 内のデータはすべて Unchanged 状態に戻されます。
また新規追加したレコードは、データベース側で採番した Id の値が書き戻されていることも確認できます。
まとめ
今回は Entity Framework Core の Change Tracker の情報を確認する方法について解説しました。 Change Tracker でハマったときは、今回解説した情報を参考に、デバッグしてみてください。
サンプルコード
本稿で紹介したサンプルコードは、以下からダウンロードできます。