文章目录
前言
在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.Metadata | IProperty 属性的元数据,包含属性的类型、名称、是否为主键等信息。 |
| 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 的性能优化作用。

2986

被折叠的 条评论
为什么被折叠?



