ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

Entity Framework Core でエンティティの変更記録を確認する方法

f:id:masatsuna:20211201000255p:plain

前提となる環境

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 でハマったときは、今回解説した情報を参考に、デバッグしてみてください。

サンプルコード

本稿で紹介したサンプルコードは、以下からダウンロードできます。

github.com