【EF Core】 探秘EF Core 更改跟踪:实体状态、快照机制与调试优化技巧

该文章已生成可运行项目,


前言

在EFCore 中,对实体的插入,更新,删除操作后,往往后面紧接着就是调用 SaveChanges 方法。通过每个 DbContext 实例跟踪对实体所做的更改,在调用 SaveChanges 方法时,这些跟踪的实体会相应地驱动对数据库的更改。


一、更改跟踪的核心原理

EF Core 通过DbContext来跟踪实体的状态变化。

DbContext是作为一个工作单元(unit of work)将持续跟踪在可能影响数据库的业务事务中执行的所有操作。 当你完成操作后,它将找出。更改数据库作为工作结果时需要执行的所有操作

使用 Entity Framework Core (EF Core) 时的典型工作单元包括:

  • 创建 DbContext 实例
  • 根据上下文跟踪实体实例。 实体将在以下情况下被跟踪
    • 正在从查询返回
    • 正在添加或附加到上下文
  • 根据需要对所跟踪的实体进行更改以实现业务规则
  • 调用 SaveChanges 或 SaveChangesAsync。 EF Core 检测所做的更改,并将这些更改写入数据
  • 释放 DbContext 实例

每个实体都有一个对应的EntityState

  • Added:DbContext 正在跟踪此实体,新实体,将被插入到数据库。
  • Modified:DbContext 正在跟踪此实体,现有实体被修改,将更新数据库记录。
  • Deleted:DbContext 正在跟踪此实体,实体被标记为删除,将从数据库中移除。
  • Unchanged:DbContext 正在跟踪此实体,实体未被修改。
  • Detached:DbContext 未跟踪该实体,实体不在 DbContext 的追踪范围内。
实体状态由 DbContext 跟踪存在于数据库中属性已修改SaveChanges 上的操作
Detached---
Added-插入
Unchanged-
Modified更新
Deleted-删除

既然每个都有实体一个对应实体状态,自然我们能想到通过比较当前自身和先前状态的差异,推断出
后续执行的步骤。EF Core团队在这里是默认使用快照(Snapshot)跟踪,既首次加载实体时保存副本,后续比较当前值与快照的差异。
也就是说在调用 SaveChanges 或 SaveChangesAsync。 EF Core 检测所做的更改,并将这些更改写入数据的依据就是来源于这当前值与快照的差异。

  • 已分离”和“未改变”的实体,SaveChanges()忽略
  • 已添加”的实体,SaveChanges()插入数据库;
  • 已修改”的实体,SaveChanges()更新到数据库
  • 已删除”的实体,SaveChanges()从数据库删除

二、访问已跟踪的实体

2.1 指定要跟踪实体 —— DbContext.Entry

将实体实例传递给 DbContext.Entry 会产生一个 EntityEntry< TEntity>的实体。

Teacher teacher = await dbContext.Teachers.Where(e => e.Id == 6).FirstOrDefaultAsync();
EntityEntry entry = dbContext.Entry(teacher);

这里我们获取一个Id为6的教师实体实例,并将该实例传递给 DbContext.Entry 返回 的EntityEntry 对象。以下是EntityEntry 对象的常用属性和方法

EntityEntry 成员描述
EntityEntry.State获取和设置实体的 EntityState 属性。
EntityEntry.Entity获取跟踪的实体实例。
EntityEntry.Context获取正在跟踪此实体的DbContext 。
EntityEntry.Metadata获取关于实体形状、其与其他实体的关系以及如何映射到数据库的元数据。
EntityEntry.IsKeySet实体是否已设置其关键值。
EntityEntry.Reload()用从数据库读取的值覆盖属性值。
EntityEntry.DetectChanges()仅强制检测此实体的更改;请参阅 更改检测和通知。

执行结果

entry.State--Unchanged
entry.Entity--SQLSearch.Model.Teacher
entry.Context--SQLSearch.ApplicationDbContext
entry.Metadata--EntityType: Teacher
entry.IsKeySet--True

2.2 访问跟踪实体的属性

EntityEntry.Property 允许访问有关实体单个属性的信息。

Teacher teacher = await dbContext.Teachers.Where(e => e.Id == 6).FirstOrDefaultAsync();
PropertyEntry<Teacher, string> propertyEntry = dbContext.Entry(teacher).Property(e => e.Name);
Console.WriteLine($"entry.Property--{propertyEntry.CurrentValue}");

执行结果

entry.Property--李副教授

如果编译时实体或属性的类型未知,则可以改为获取非泛型 PropertyEntry 。

PropertyEntry _propertyEntry = dbContext.Entry(teacher).Property("Name");
Console.WriteLine($"entry.Property--{_propertyEntry.CurrentValue}");

PropertyEntry 有以下的几个公开的属性信息

PropertyEntry 成员DESCRIPTION
PropertyEntry<TEntity,TProperty>.CurrentValue获取并设置某属性的当前值。
PropertyEntry<TEntity,TProperty>.OriginalValue获取并设置属性的原始值(如果可用)。
PropertyEntry<TEntity,TProperty>.EntityEntry对实体的 EntityEntry< TEntity> 反向引用,指向当前属性所属实体的 EntityEntry 对象。
PropertyEntry.MetadataIProperty 属性的元数据,包含属性的类型、名称、是否为主键等信息。
PropertyEntry.IsModified指示此属性是否标记为已修改,并允许更改此状态。
PropertyEntry.IsTemporary指示此属性是否标记为 临时属性,并允许更改此状态。

属性的原始值是从数据库查询实体时该属性具有的值。也就是实体加载时或最后一次被跟踪时的属性值。当实体断开连接并显式附加到其他 DbContext(例如,使用 Attach 或 Update)时,其原始值会不可用。 在这种情况下,返回的原始值将与当前值相同。
SaveChanges 将仅更新已标记为修改的属性。 设置为 IsModified true 以强制 EF Core 更新给定属性值,或将其设置为 false 以防止 EF Core 更新属性值。

2.3 访问所有跟踪的实体 —— ChangeTracker.Entries

ChangeTracker.Entries() 返回 DbContext 当前跟踪的每个实体的 EntityEntry。
代码如下

List<Student> students  = await dbContext.Students.Include(s => s.Class).ToListAsync();
foreach (var student in dbContext.ChangeTracker.Entries())
{
    Console.WriteLine($"Entity : {student.Metadata.Name} | ID :{student.Property("Id").CurrentValue}");
}

执行结果

Entity : SQLSearch.Model.Student | ID :7
Entity : SQLSearch.Model.Class | ID :3
Entity : SQLSearch.Model.Student | ID :8
Entity : SQLSearch.Model.Student | ID :9
Entity : SQLSearch.Model.Class | ID :4
Entity : SQLSearch.Model.Student | ID :10

2.4 Find 与 FindAsync

Find 和 FindAsync 是用于根据主键值查找实体的常用方法,通过主键值查找实体。如果实体已在上下文中被跟踪,则直接返回,否则会查询数据库。优先使用 FindAsync,在异步环境)中,使用 FindAsync 避免阻塞请求线程。

值得注意的是Find 与 FindAsync是DbSet的虚方法,调用方法的时候需要确定对象

下面我们通过一段例子来观察Find 和 FindAsync的执行。

Console.WriteLine("/*------------------------首次访问--------------------------*/");
Teacher teacher1st = await dbContext.Teachers.FindAsync(6);
Console.WriteLine($"teacher1st--{teacher1st.Name}");

Console.WriteLine("/*------------------------第二次访问--------------------------*/");
Teacher teacher2nd = await dbContext.Teachers.FindAsync(6);
Console.WriteLine($"teacher2nd--{teacher2nd.Name}");

执行结果

/*------------------------首次访问--------------------------*/
info: 5/28/2025 10:24:20.499 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (61ms) [Parameters=[@__p_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      SELECT TOP(1) [t].[Id], [t].[Name], [t].[Subject]
      FROM [T_Teacher] AS [t]
      WHERE [t].[Id] = @__p_0
teacher1st--李副教授
/*------------------------第二次访问--------------------------*/
teacher2nd--李副教授

能明显看到 FindAsync在第二次执行的获取Teacher对象的时候并没有去执行数据库查询,而是通过已被上下文跟踪的实体直接返回缓存中的实体。缓存果然是程序优化的万金油呀

2.5 调试视图

ChangeTracker.DebugView 提供了有关要跟踪的所有实体的直观易懂的视图。调试视图具有短视图形式和长视图形式。 短视图形式显示跟踪的实体、它们的状态和键值。 长视图形式还包括所有属性以及导航值和状态。
下面我们插入一条数据来观察一下

Teacher newTeacher = new Teacher()
{
    Name = "刘讲师",
    Subject = "高等数学"
};
dbContext.Teachers.Add(newTeacher);
Console.WriteLine(dbContext.ChangeTracker.DebugView.ShortView);
Console.WriteLine("------------------------------------------");
Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);

执行结果

Teacher {Id: -2147482647} Added

------------------------------------------
Teacher {Id: -2147482647} Added
    Id: -2147482647 PK Temporary
    Name: '刘讲师'
    Subject: '高等数学'
  Classes: <null>
  TeacherCourses: <null>

值得说明的是对于实体的修改(Update操作),EF Core采用的是惰性检测。EF Core 不会在实体状态改变时立即检测变更,而是在需要时,比如如调用 SaveChanges()、DetectChanges() 或访问 ChangeTracker才执行检测。因为变更检测涉及遍历所有跟踪的实体,可能影响性能。因此,EF Core 会避免重复检测。

我们以更新举例

Teacher teacher = await dbContext.Teachers.Where(e => e.Id == 6).FirstAsync();
teacher.Name = "刘讲师";
//dbContext.ChangeTracker.DetectChanges();
Console.WriteLine(dbContext.ChangeTracker.DebugView.ShortView);
Console.WriteLine("------------------------------------------");
Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);

执行结果-未调用dbContext.ChangeTracker.DetectChanges();

Teacher {Id: 6} Unchanged

------------------------------------------
Teacher {Id: 6} Unchanged
    Id: 6 PK
    Name: '刘讲师' Originally '陈讲师'
    Subject: '操作系统'
  Classes: <null>
  TeacherCourses: <null>

执行结果-调用dbContext.ChangeTracker.DetectChanges();

Teacher {Id: 6} Modified

------------------------------------------
Teacher {Id: 6} Modified
    Id: 6 PK
    Name: '刘讲师' Modified Originally '陈讲师'
    Subject: '操作系统'
  Classes: <null>
  TeacherCourses: <null>

在没有手动调用dbContext.ChangeTracker.DetectChanges()之前EntityEntry.State始终是Unchanged,只有在手动调用 dbContext.ChangeTracker.DetectChanges(),EntityEntry.State才会更新为Modified。

三、优化策略 —— AsNoTracking

当查询数据时,EF Core 默认会跟踪返回的实体,记录其状态变化,使用 AsNoTracking() 后,EF Core 不会跟踪实体,减少内存占用并提升查询性能。
也就是说如果通过Dbcontext查询出来的对象只是用来展示不会发生状态改变,则可以使用AsNoTracking()来“禁用跟踪”,如果查询出来的对象不会被修改、删除等,那么查询时可以AsNoTracking(),就能降低内存占用。

我们还是拿上文的代码举例,在DbSet对象后面加入.AsNoTracking()。

Teacher teacher = await dbContext.Teachers.AsNoTracking().Where(e => e.Id == 6).FirstAsync();
teacher.Name = "刘讲师";
dbContext.ChangeTracker.DetectChanges();
Console.WriteLine(dbContext.ChangeTracker.DebugView.ShortView);
Console.WriteLine("------------------------------------------");
Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);

执行结果


------------------------------------------

并没有任何关于实体的跟踪信息。


总结

本文详细介绍了 EF Core 的更改跟踪原理、实体状态管理及优化策略,包括如何通过 DbContext.Entry 和 ChangeTracker 访问跟踪实体,演示了 AsNoTracking 的性能优化作用。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值