【EF Core】实体追踪——Entry中记录的数据

张开发
2026/4/3 17:54:37 15 分钟阅读
【EF Core】实体追踪——Entry中记录的数据
性值就行了。毕竟实体类通常就是一个普通类EF Core 需要状态追踪功能总不能让开发者自己去跟踪吧所以EF 内部会用字典数据结构来保存实体的各个属性的值。字典是个好东西啥都能放。有时候在写 Web API 时一些返回 JSON 结构是动态的为它们都定义一个类来序列化是不明智的直接拼装 JSON 有点麻烦这时候用字典就很爽。当实体从数据库查询出来时EF 先为实体对象创建一个快照表明它的原始数据。然后当你对实体进行各种搔操作之后调用一下 DetectChanges 方法它会扫描实体对象各个属性的值并和当初创建的快照比较以确定实体是否被修改或删除。为了让初学的大伙伴们好理解咱们做个对比实验。假设有这么个实体类它表示一本书的信息。public class Book { public int BookId { get; set; } public string Name { get; set; } null!; public string ISBN { get; set; } null!; public string Author { get; set; } null!; public int PubYear { get; set; } }然后是实现数据库上下文。public class MyDb : DbContext { public DbSetBook Books { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite(data sourceshop.db); } protected override void OnModelCreating(ModelBuilder modelBuilder) { var bookEnt modelBuilder.EntityBook(); // 主键名称 bookEnt.HasKey(x x.BookId).HasName(PK_Book); // 字符串长度 bookEnt.Property(a a.Name).HasMaxLength(65); bookEnt.Property(b b.Author).HasMaxLength(20); bookEnt.Property(c c.ISBN).HasMaxLength(15); bookEnt.ToTable(tb_Books, t { // 约束 t.HasCheckConstraint(CK_Pubyear, \PubYear\ 2020 AND \PubYear\ 2080); }); } }上面那些都是常规操作了大家瞄两眼就行。下面代码创建数据库并插入一条数据。using MyDb context new(); context.Database.EnsureCreated(); Book bb new() { Name 回魂术, Author 老周, ISBN 551269882, PubYear 2028 }; context.Books.Add(bb); context.SaveChanges();下面重头戏来了。咱们从数据库中查询出这条记录然后改变 PubYear 属性的值。var theBook context.Books.FirstOrDefault(); if (theBook null) return; // 打印一次追踪 Console.WriteLine(context.ChangeTracker.ToDebugString()); // 更改属性 theBook.PubYear 2030; // 再打印一次 Console.WriteLine(context.ChangeTracker.ToDebugString());ChangeTracker.ToDebugString 方法方便测试可以直接观察框架对实体对象的更改记录。两次调用的输出如下Book {BookId: 1} Unchanged BookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2028 Book {BookId: 1} Unchanged BookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2030 Originally 2028很明显EF 并不知道咱们修改了实体所以调用一下 DetectChanges 方法会触发一次扫描和比较。// 打印一次追踪 Console.WriteLine(context.ChangeTracker.ToDebugString()); // 更改属性 theBook.PubYear 2030;context.ChangeTracker.DetectChanges(); // 再打印一次 Console.WriteLine(context.ChangeTracker.ToDebugString());这次 EF 就知道实体被修改了。Book {BookId: 1} Unchanged BookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2028 Book {BookId: 1} Modified BookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2030 Modified Originally 2028由于 PubYear 属性被更新使得实体的状态变更为 Modified。那为什么我调用 SaveChanges 方法时 EF 能顺利生成更新 SQL 呢因为这个方法会先 DetectChanges再根据实体的状态来生成更新语句。但是如果你在代码里面把 AutoDetectChangesEnabled 属性设置为 false那么调用 SaveChanges 方法是不会更新的。using MyDb context new(); // 注意这一行 context.ChangeTracker.AutoDetectChangesEnabled false; // 找出第一条记录 Book onebook context.Books.First(); // 打印一次 Console.WriteLine(${onebook.Name}, {onebook.PubYear}); // 改一下年份 onebook.PubYear 2031; // 提交更改 context.SaveChanges(); // 再查询一次 Book otherOne context.Books.First(b b.BookId onebook.BookId); // 再次打印 Console.WriteLine(${otherOne.Name}, {otherOne.PubYear}); // 看看实体追踪信息 Console.WriteLine(context.Entry(otherOne).DebugView.LongView);看看调试信息。回魂术, 2028 回魂术, 2031 Book {BookId: 1}UnchangedBookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2031 Originally 2028虽然 EF 追踪到 PubYear 属性被改为 2031但注意它现在的状态是 Unchanged所以 SaveChanges 不会更新数据库。大伙伴们这里你千万别犯糊涂把概念搞混了。AutoDetectChangesEnabled 属性设置为 false 只表明 EF 在 SaveChanges 方法中不会自动扫描检测实体的状态可人家没说不会追踪实体的变更哟。你如果只是查询数据不更改数据不需要追踪实体状态以提升不太明显的性能那么你不应该关闭 AutoDetectChangesEnabled 属性而应该设置 QueryTrackingBehavior 属性。context.ChangeTracker.QueryTrackingBehavior QueryTrackingBehavior.NoTracking;禁用追踪后更新数据库就跟 Sugar 一样需要你开手动档。你只需要调用一下 Update 方法将实体状态变为 Modified 就行缺点是生成的 UPDATE 语句会把所有字段都 SET 一遍。// 找出第一条记录 Book onebook context.Books.First(); // 打印一次 Console.WriteLine(${onebook.Name}, {onebook.PubYear}); // 改一下年份 onebook.PubYear 2024; // 改变状态context.Update(onebook); // 提交更改 context.SaveChanges();那有没有方法让生成的 UPDATE 语句只 SET 被改动过的字段呢大伙伴肯定猜到老周既然这么写那说明肯定有的。但老周希望你不要死记方法而是思路。咱们好好想一下EF 是不是在查询出数据到为实体建立快照然后进行比较以确定哪些属性字段被修改了。既然这样咱们在查询实体后手动给它弄个快照然后再修改属性值再提交更新不就完事了吗好想干就干。// 找出第一条记录 Book onebook context.Books.First(); context.Books.Attach(onebook); // 记录快照context.Entry(onebook).OriginalValues.SetValues(onebook); // 改一下年份 onebook.PubYear 2021; // 打印一下状态信息 Console.WriteLine(context.Entry(onebook).DebugView.LongView); // 严重警告如果你把 AutoDetectChangesEnabled 属性设置为 false那一定要调用下面这一行以扫描更改否则只能更新个毛线 // 如果你没有改动 AutoDetectChangesEnabled 属性它默认是打开的那下面这行可以忽略 // context.ChangeTracker.DetectChanges(); // 提交更改 context.SaveChanges(); // 再查询一次 Book otherOne context.Books.First(b b.BookId onebook.BookId); // 再次打印状态信息 Console.WriteLine(context.Entry(otherOne).DebugView.LongView);结果如下Book {BookId: 1} Modified BookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2021Modified Originally 2025Book {BookId: 1} Detached BookId: 1 PK Author: 老周 ISBN: 551269882 Name: 回魂术 PubYear: 2021生成的SQL语句如下UPDATE tb_Books SET PubYear p0 WHERE BookId p1 RETURNING 1;因为咱们设置为不追踪实体QueryTrackingBehavior.NoTracking所以在查询后要用 Attach 方法把实体连接到追踪器并设定状态为 Unchanged。然后context.Entry 方法获取到 EntityEntry 对象本文的主角出场了再往 OriginalValues 里面放点原材料目前查询出来的值这样快照就建立了。再然后可以大胆地修改实体了这时候 EF 能扫描到更改。由于咱们设置的不追踪所以更新之后 EF 又把实体给甩了于是实体状态又变回 Detached。说简单点在手动档追踪下实体的状态经历了 Detached - Unchanged - Modified - Detached 的生死轮回。-----------------------------------------------------------------------------------------------------------------------------------------------------------Entry 是“记录”的意思EntityEntry 望文生义一下就是“实体记录”它维护着实体的状态和各属性的值。一句话斯基总结它是为实体追踪跟踪服务的管理着实体相关的数据。在 80% 的使用场景下我们不需要用 Entry 的走常规流程从数据库中查询数据自动追踪修改后提交就完事了。不过像影子属性这种不在实体类中的成员你咋办影子属性的元数据在数据库模型中而值是保存在 EntityEntry 中。下面咱们用一个实例来说明。咱们定义一个用户实体。public class User { public int Id { get; set; } public string Name { get; set; } string.Empty; public string LogName { get; set; } string.Empty; public string? Password { get; set; } }然后是实现自己的数据库上下文。public class AppContext : DbContext { public DbSetUser Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite(data sourcedemo996.db); } protected override void OnModelCreating(ModelBuilder modelBuilder) { EntityTypeBuilderUser entUser modelBuilder.EntityUser(); // 常规配置 entUser.Property(x x.Name).HasColumnName(u_name); entUser.Property(w w.LogName).HasColumnName(log_name); entUser.Property(t t.Password).HasColumnName(u_pwd); entUser.Property(m m.Id).HasColumnName(u_id); entUser.HasKey(x x.Id).HasName(PK_Userid); // 影子属性entUser.PropertyDateTime?(LastLog).HasColumnName(_last_log); } }User 实体有一个影子属性 LastLog记录用户上一次登录的时间。在实体中不需要使用或不希望被访问的属性在建立数据库模型时可以作为影子属性。在查询表达式中可以使用 EF.Property 方法来获取影子属性的值EF 类中的方法成员都会抛出异常没有真正实现而是通过表达式树来翻译处理。因此在非查询语句中访问影子属性或要修改影子属性就不能使用 EF 类了。影子属性的值保存在 Entry 中可以用以下代码来设置 LastLog 属性的值。// 先查询出实体 User? u context.Users.FirstOrDefault(x x.Name Teto); if (u is null) { Console.WriteLine(用户不存在); return; } // 通过 Entry 修改影子属性 DateTime theTime DateTime.Now; context.Entry(u).Property(LastLog).CurrentValue theTime; // 打印追踪信息 Console.WriteLine(context.ChangeTracker.DebugView.LongView); // 提交保存 context.SaveChanges();直接使用 Entry 修改属性值会自动应用实体的状态。User 实体变为 Modeified 状态。User {Id: 3}ModifiedId: 3 PK LastLog: 2026/2/12 18:25:01 Modified Originally 2026/2/12 17:41:20 LogName: teto Name: Teto Password: balabala想验证是否更新数据库可以查询一遍整个表。using(var context new AppContext()) { Console.WriteLine(\n------ 所有用户 ------); foreach(User usr in context.Users) { Console.WriteLine($ 用户ID{usr.Id} 用户名{usr.Name} 上次登录时间{context.Entry(usr).Property(LastLog).CurrentValue} ); Console.Write(\n); } }------ 所有用户 ------ 用户ID1 用户名Kaito 上次登录时间2026/2/12 17:40:45 用户ID2 用户名Gumi 上次登录时间2026/2/12 17:40:13 用户ID3 用户名Teto 上次登录时间2026/2/12 18:25:01上面的做法要先查询一次然后更新即数据库往返两回。99.99965% 的情况下也没啥影响的而且很多时候用户编辑数据时确实得先查后改的毕竟编辑界面你得先显示现有的数据才方便用户去修改。如果你不想 SELECT 只想直接 UPDATE 也可以的。// 直接实例化 // Id 是主键必须赋值明确要更新的记录 User data new() {Id 2}; var entry context.Entry(data); // 标记实体的状态为已修改 entry.State EntityState.Modified; // 先改变实体的状态再去改变某个属性的状态 // 因为在设置实体为 Modified 时会把所有属性都设置为 Modified // 先设置实体再设置属性成员就不会被覆盖 foreach (var p in entry.Properties) { if (p.Metadata.Name LastLog) { // 只修改这个属性 p.IsModified true; p.CurrentValue DateTime.Now; } else { // 其他属性不改标记为非修改状态 p.IsModified false; } } // 打印追踪信息 Console.WriteLine(context.ChangeTracker.DebugView.LongView); // 提交保存 context.SaveChanges();在改变实体状态时先设置整个实体为 Modified此时由于没有初始快照做比较实体的所有属性不含主键都被标记为 Modified如果这样更新数据库的话会把 Name、LogName、Password 等属性都更改为 null 了。所以咱们在设置实体为 Modified 后还要对各个属性做做手脚。用 foreach 枚举各个属性只有 LastLog 属性才设置为 Modified其他的属性设置为未更改这样发送到数据库的 UPDATE 语句只会 SET LastLog 属性。UPDATE Users SET _last_log p0 WHERE u_id p1 RETURNING 1;

更多文章