EFCoreのChangeTrackerについて今まで雰囲気で分かった気になっていたので、理解を深めるためにDbContextのデータをいろいろ操作しながらChangeTrackerの内容を確認してみます。
実行環境
Entityを用意する
次のようなEntityを作成します。
public record ToDo { public required int Id { get; set; } public required string Description { get; set; } public required DateTime DueDate { get; set; } public required bool IsCompleted { get; set; } }
ChangeTrackerの取得
ChangeTrackerは以下で確認できます。 ShortViewとLongViewの2つが用意されていて、ShortViewはシンプル、LongViewはより詳細な内容が確認できます。
dbContext.ChangeTracker.DebugView.ShortView
// or
dbContext.ChangeTracker.DebugView.LongView
LongViewはこのような感じになっています。
ToDo {Id: 1} Unchanged Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True'
CRUDしながらChangeTrackerの内容を出力する
何もしていない状態
dbContextに対してまだ何も操作を行っていない状態です。
var dbContext =new AppDbContext(); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);
まだ何も追跡されていないので何も出力されません。
追加
var toDo =new ToDo { Id =1, Description ="コードレビュー", DueDate = DateTime.Today, IsCompleted =true }; await dbContext.ToDos.AddAsync(toDo); Console.WriteLine("AddAsync後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView); await dbContext.SaveChangesAsync(); Console.WriteLine("SaveChangesAsync後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);
ToDoの変更が追跡され、Addedとしてマークされました。また、SaveChangesAsyncすることでAddedからUnchangedに変わりました。
AddAsync後 ToDo {Id: 1} Added Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True' SaveChangesAsync後 ToDo {Id: 1} Unchanged Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True'
読み取り
var toDo =await dbContext.ToDos.FirstAsync(); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);
取得しただけの場合はUnchangedとしてマークされていました。
ToDo {Id: 1} Unchanged Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True'
更新
var toDo =await dbContext.ToDos.FirstAsync(); Console.WriteLine("FirstAsync後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView); toDo.IsCompleted =false; dbContext.ChangeTracker.DetectChanges(); Console.WriteLine("プロパティ変更後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView); await dbContext.SaveChangesAsync(); Console.WriteLine("SaveChangesAsync後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);
ToDoを取得し、IsCompletedをfalseにして更新してみました。 プロパティ変更後にModifiedとしてマークされています。また、IsCompletedというカラムがModifiedとなっているためカラムレベルで変更が追跡されていることが分かります。 こちらも追加のときと同様にSaveChangesAsyncするとUnchangedになりました。
FirstAsync後 ToDo {Id: 1} Unchanged Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True' プロパティ変更後 ToDo {Id: 1} Modified Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'False' Modified Originally 'True' SaveChangesAsync後 ToDo {Id: 1} Unchanged Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'False'
Modifiedに関してはLongViewを取得する前に
dbContext.ChangeTracker.DetectChanges();
を呼んでおかないとModifiedというマークがつかないというハマりポイントがありました。
削除
var toDo =await dbContext.ToDos.FirstAsync(); Console.WriteLine("FirstAsync後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView); dbContext.ToDos.Remove(toDo); Console.WriteLine("Remove後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView); await dbContext.SaveChangesAsync(); Console.WriteLine("SaveChangesAsync後"); Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);
削除の場合はDELETEDというマークがつきました。 SaveChangesAsync後、ChangeTrackerの内容は空になりました。
FirstAsync後 ToDo {Id: 1} Unchanged Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True' Remove後 ToDo {Id: 1} Deleted Id: 1 PK Description: 'コードレビュー' DueDate: '2023/06/29 0:00:00' IsCompleted: 'True' SaveChangesAsync後
まとめ
ChangeTrackerはEntityに対してCRUDを行ったときに変更を追跡し、カラムレベルで追跡を行っていることが分かりました。 また、SaveChangesAsyncしたタイミングでそれまでに追跡していた操作をデータベースに対して実行することが分かりました。