【EF Core】实体状态与变更追踪 [复制链接]
发表于 2026-1-26 05:49:42 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
好长时间没有水文章了,请容老周表明一下。由于近来老周进了两个厂,第一个厂子呆了八天左右,第二个厂子还在调试。管理很严格,带的电子装备都要登记、办手续。当初以为雷神条记本的屏幕大,在车间调试代码方便,以是登记了这个型号。但这个游戏本功耗大,而且充电只能充到 83% 就充不进去了。只能白天在车间调试时用,其他时间玩手机。手机是谁人 23800 mAH 的坦克3,以是电量多得是,充一次任意玩。在厂里很无聊,老周还另带了一台某宝买的开源掌机……扯远了。
第二个厂子的项目很诡异,老周乃至猜疑有人故意捣乱。他们工人本身测试的时间,总是报莫名其妙的错;但是,只要老周已往和他们一起测,就齐备正常。反正如今是测不出到底啥标题。从日记中记载的非常看,都是 Modbus TCP 毗连超时。把 time out 改为 50 分钟,也还是在无穷毗连中。老周以为是人为拨了网线的大概性更大。反正只要老周在现场就没标题,以是耗了近一个月也没效果。以是老周就请了四天假玩玩,不管他们同差异意,四天后老周定时归去报到。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
记得上一篇水文中,老周说了把一个实体映射到多个表的话题。注意,一实体一数据表的原则是稳定的,这种特别环境可以用在你这几个表可以构成一个团体,而且经常一起使用的,如许你在查询时就不消团结了,一样寻常是一对一关系的。
认识老周的人都知道,老周分享的都是纯知识和纯技能的东西。至于实际开辟中怎么用,那是你的事。实际应用是没办法写教程的,你得看具体环境,机动运用,不存在一个教程包万能的原理。做项目我从小周做成了老周,固然没做过什么大项目,但小 Case 是不少的(吹吹牛皮)。你别鄙视那些杂七杂八的项目,哪个不是要六边形兵士,哪个不是软硬联合,哪个不是既485又CAN又PLC又单片机的。别看它小,WinForms、Web、STM32(珠海极海的 APM32 也碰到过)、串口、Esp8266 全用上都是常见的事。这年初,不学点 C 语言连小项目都搞不起,哪像那些互联网巨头那么爽,天天盯着 HTML + CSS 玩。
老周不绝以为,履历着实不紧张的,跟一两周的项目你都有履历了,关键还得是根本踏实、技能过硬,如许才气来什么活接什么活。至于说根本标题,干活的家伙,实战更紧张,理论的东西着实知道是啥就好,咱们又不消写论文评职称。你理论知识说得一套一套的,真用的时间不会用,那有啥用?不要排挤实用主义,实用主义着实是精确的,技能学了就是拿来用的,不消就没意义了。学习是分两种的:一种是内修——好比琴棋字画,这是文化秘闻,个人气质。这种你不必学了就要用(但也可以用),更紧张的是养心养神,自我调解;另一种就是工作干活用的,叫技能,属于外修。从小老师都教我们要表里兼修。
好了,不扯了。在开始本日的主题前,咱们补一个内容:既然实体能分布到多个表中,那反过来呢?能把多个实体映射到一个表中吗?固然可以了,官方称作“表拆分”。同样的原理,一样寻常也是一对一的关系。
光说不练,惨不对恋。咱们直接用实例来分析。假设下面有两个实体。
  1. public class Person
  2. {
  3.     public int PsID { get; set; }
  4.     public string Name { get; set; } = null!;
  5.     public int Age {  get; set; }
  6.     // 导航属性
  7.     public PersonInfo OtherInfo { get; set; } = null!;
  8. }
  9. public class PersonInfo
  10. {
  11.     <strong>private int InfoID {  get; set; }</strong>        // 既做主键也做外键
  12.     /// <summary>
  13.     /// 体重
  14.     /// </summary>
  15.     public float Weight { get; set; }
  16.     /// <summary>
  17.     /// 身高
  18.     /// </summary>
  19.     public float Height {  get; set; }
  20.     /// <summary>
  21.     /// 民族
  22.     /// </summary>
  23.     public string? Ethnicity {  get; set; }
  24. }
复制代码
待会儿咱们要做的是把这两个实体映射到一个表中,以是为了安全,你可以让 PersonInfo 实体的 InfoID 属性变成私有成员,这可以防止三只手的人不测修改主键值。由于这个实体的 ID 值必须始终与 Person 的 ID 划一。
下面代码数据库上下文类。
  1. public class DemoDbContext : DbContext
  2. {
  3.     public DbSet<Person> People { get; set; }
  4.     public DbSet<PersonInfo> PersonInfos { get; set; }
  5.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  6.     {
  7.         optionsBuilder.UseSqlite("data source=恭喜发财.db")
  8.                                 .LogTo(
  9.                                     // 输出日志日志的委托
  10.                                     action: msg => Console.WriteLine(msg),
  11.                                     // 过滤器,只显示即将执行的命令日志日志,可以看到SQL语句
  12.                                     filter: (eventId, _) => eventId.Id == RelationalEventId.CommandExecuting
  13.                                 );
  14.     }
  15.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  16.     {
  17.         // 配置实体
  18.         modelBuilder.Entity<Person>(pse =>
  19.         {
  20.             pse.<strong>Property(e </strong><strong>=> e.PsID).HasColumnName("person_id"</strong><strong>)</strong>;
  21.             pse.Property(b => b.Name).HasMaxLength(16).IsRequired().HasColumnName("person_name");
  22.             pse.Property(d => d.Age).HasColumnName("person_age");
  23.             // 主键
  24.             pse.HasKey(w => w.PsID).<strong>HasName("PK_Person"</strong><strong>)</strong>;
  25.             // 表名
  26.             pse.<strong>ToTable("tb_people"</strong><strong>)</strong>;
  27.         });
  28.         modelBuilder.Entity<PersonInfo>(pie =>
  29.         {
  30.             pie.<strong>Property(</strong><strong>"InfoID").HasColumnName("person_id"</strong><strong>).ValueGeneratedNever()</strong>;
  31.             pie.Property(r => r.Height).HasColumnName("info_height");
  32.             pie.Property(i => i.Weight).HasColumnName("info_weight");
  33.             pie.Property(k => k.Ethnicity).HasMaxLength(10).HasColumnName("info_ethnic");
  34.             // 主键
  35.             pie.HasKey("InfoID").<strong>HasName("PK_Person"</strong><strong>)</strong>;
  36.             // 同一个表名
  37.             pie.<strong>ToTable("tb_people"</strong><strong>)</strong>;
  38.         });
  39.         // 两实体的关系
  40.         modelBuilder.Entity<Person>().HasOne(n => n.OtherInfo)
  41.                                                           .WithOne()
  42.                                                           // info --> person
  43.                                                           .HasForeignKey<PersonInfo>("InfoID").HasConstraintName("FK_PersonInfo")
  44.                                                           // person --> info
  45.                                                           .HasPrincipalKey<Person>(p => p.PsID);
  46.     }
  47. }
复制代码
根本代码信赖各位能看懂的。和设置一样寻常实体区别不大,但要注意几点:
1、两个实体所映射的表名要雷同。这是F话了,都说映射到同一个表了,表名能不一样的?
2、两个实体中作为主键的属性名可以差异,但范例要雷同(可以镌汰翻车变乱);更紧张的是:肯定要映射到同一个列名。由于映射后,两个实体作为主键的属性会集并;再者,主键的束缚名称也要雷同,不表明白,一样的原理。
  1. modelBuilder.Entity<Person>(pse =>
  2. {
  3.     pse.Property(e => e.PsID).HasColumnName("person_id");
  4.     ……
  5.     // 主键
  6.     pse.HasKey(w => w.PsID).HasName("PK_Person");
  7.     // 表名
  8.     pse.ToTable("tb_people");
  9. });
  10. modelBuilder.Entity<PersonInfo>(pie =>
  11. {
  12.     pie.Property("InfoID").HasColumnName("person_id").ValueGeneratedNever();
  13.     ……
  14.     // 主键
  15.     pie.HasKey("InfoID").HasName("PK_Person");
  16.     // 同一个表名
  17.     pie.ToTable("tb_people");
  18. });
复制代码
对于第二个实体,ValueGeneratedNever 方法可以不调用,EF 会自动感知到不须要自动天生列值。
3、两个实体设置为一对一关系,这个和通例实体利用一样。
然后在 Main 方法中测试一下。
  1. static void Main(string[] args)
  2. {
  3.     using var context = new DemoDbContext();
  4.     context.Database.EnsureDeleted();
  5.     context.Database.EnsureCreated();
  6.     // 打印数据库模型
  7.     Console.WriteLine(context.Model.ToDebugString());
  8. }
复制代码
运行效果:
  1.       Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
  2.       CREATE TABLE "tb_people" (
  3.           "person_id" INTEGER NOT NULL CONSTRAINT "PK_Person" PRIMARY KEY AUTOINCREMENT,
  4.           "person_name" TEXT NOT NULL,
  5.           "person_age" INTEGER NOT NULL,
  6.           "info_weight" REAL NOT NULL,
  7.           "info_height" REAL NOT NULL,
  8.           "info_ethnic" TEXT NULL
  9.       );
  10. // 以下是数据库模型
  11. Model:
  12.   EntityType: Person
  13.     Properties:
  14.       PsID (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
  15.       Age (int) Required
  16.       Name (string) Required MaxLength(16)
  17.     Navigations:
  18.       OtherInfo (PersonInfo) Required ToDependent PersonInfo
  19.     Keys:
  20.       PsID PK
  21.   EntityType: PersonInfo
  22.     Properties:
  23.       InfoID (int) Required PK FK AfterSave:Throw
  24.       Ethnicity (string) MaxLength(10)
  25.       Height (float) Required
  26.       Weight (float) Required
  27.     Keys:
  28.       InfoID PK
  29.     Foreign keys:
  30.       PersonInfo {'InfoID'} -> Person {'PsID'} Unique Required RequiredDependent Cascade ToDependent: OtherInfo
复制代码
--------------------------------------------------------------------------------------------------------------------------------------
下面正片开始。本日咱们说说 EF Core 中几大紧张功能模块之一——追踪(叫跟踪也行)。正常环境下,EF Core 从实体被查询出来的时间开始跟踪。跟踪前会为实体的各个属性/字段的值创建一个快照(就备份一下,不是拷贝对象,而是用一个字典来存放)。然后在特定条件下,会触发比力,即比力实体引用当前各属性的值与当初快照中的值,从而确定实体的状态。
为了方便访问,DbContext 类会公开 ChangeTracker 属性,通过它你能访问到由 EF Core 创建的 ChangeTracker 实例(在Microsoft.EntityFrameworkCore.ChangeTracking 定名空间)。该类包罗与实体追踪有关的信息。调用 DetectChanges 方法会触发实体的追踪扫描,方法只负责触发状态查抄,不返回任何效果,调用后实体的状态自动更新。实体的状态由 EntityState 罗列表现。
1、Unchanged:实体从数据库中查询出来后就是这个状态,条件是这个实体是从数据库中查出来的,也就是说它已经在数据库中了。
2、Added:当你用 DbContext.Add 或 DbSet.Add 方法添加新实体后,实体就处在这个状态。实体只存在 EF Core 中,还没生存到数据库。提交时天生 INSERT 语句。
3、Modified:已修改。实体自从数据库中查询出来到如今为止,它的某些属性或全部属性被修改过。提交时天生 UPDATE 语句。
4、Deleted:已删除。实体已从 DbSet 中删除(还在数据库中)就是这个状态,提交后天生 DELETE 语句。
5、Detached:失落生齿,EF Core 未追踪其状态。
EF Core 内部有个名为 IStateManager 的服务接口,默认实现类是 StateManager。该类可以修改实体的状态,也可以控制开始/克制追踪实体的状态。咱们在写代码时不须要直接访问它,DbContext 以及 DbContext.ChangeTracker、DbSet 已经封装了相干访问入口。
对 DbSet 对象来说,你调用 Add、Remove、Update 等方法只是更改了实体的状态,并没有真正更新到数据库,除非你调用 SaveChanges 方法。SaveChanges 方法内部会先调用 DetectChanges 方法触发状态变更扫描,然后再根据实体的最新状态天生相应的 SQL 语句,再发送到数据库中实行。
下面以插入新实体为例,演示一下。本示例在插入新实体前、后,以及提交到数据库后都打印一次实体的状态。
先界说实体类。
  1. public class Pet
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; } = string.Empty;
  5.     public string? Description { get; set; }
  6.     public string? Category {  get; set; }
  7. }
复制代码
正规流程,写数据库上下文类。
  1. public class TestDbContext : DbContext
  2. {
  3.     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  4.     {
  5.         optionsBuilder.UseSqlite("data source=天宫赐福.db");
  6.     }
  7.     protected override void OnModelCreating(ModelBuilder modelBuilder)
  8.     {
  9.         modelBuilder.Entity<Pet>(et =>
  10.         {
  11.             et.ToTable("tb_pets");
  12.             et.Property(g => g.Name).HasMaxLength(20);
  13.             et.Property(k => k.Description).HasMaxLength(200);
  14.             et.Property(q => q.Category).HasMaxLength(15);
  15.             et.HasKey(m => m.Id).HasName("PK_PetID");
  16.         });
  17.     }
  18. }
复制代码
好,如今进入测试环节。
  1. static void Main(string[] args)
  2. {
  3.     using var context = new TestDbContext();
  4.     context.Database.EnsureCreated();
  5.     // 添加一个实体
  6.     Pet p = new() { Name = "Jack", Description = "不会游泳的巴西龟", Category = "爬行动物" };
  7.     // 打印一下状态
  8.     Console.WriteLine("----------- 添加前 -------------");
  9.     Console.WriteLine(context.ChangeTracker.DebugView.LongView);
  10.     context.Add(p);
  11.     // 再打印一下状态
  12.     Console.WriteLine("\n---------- 添加后 ------------");
  13.     Console.WriteLine(context.ChangeTracker.DebugView.LongView);
  14.     // 提交
  15.     context.SaveChanges();
  16.     // 再打印状态
  17.     Console.WriteLine("\n---------- 提交后 ------------");
  18.     Console.WriteLine(context.ChangeTracker.DebugView.LongView);
  19. }
复制代码
和 Model 雷同,ChangeTracker 对象也有个 DebugView,用于获取调试用的信息。这个能打印出实体以及它的各个属性的状态。
运行一遍,效果如下:
  1. ----------- 添加前 -------------
  2. ---------- 添加后 ------------
  3. Pet {Id: -2147482647} Added
  4.     Id: -2147482647 PK Temporary
  5.     Category: '爬行动物'
  6.     Description: '不会游泳的巴西龟'
  7.     Name: 'Jack'
  8. ---------- 提交后 ------------
  9. Pet {Id: 1} Unchanged
  10.     Id: 1 PK
  11.     Category: '爬行动物'
  12.     Description: '不会游泳的巴西龟'
  13.     Name: 'Jack'
复制代码
新实体被 Add 之前,它是没有被追踪的,以是打印状态信息空缺。调用 Add 方法后,它的状态就变成 Added 了。此时,你不须要调用 DetectChanges 方法,由于 Add 方法本身就会修改实体的状态。新实体还未存入数据库,以是主键 ID 赋了个负值,且是临时的。当调用 SaveChanges 方法后,提交数据库生存,并取回数据库天生的ID值,故此时 ID 的值是 1。而且,实体的状态被改回 Unchanged。这是公道的,如今新的实体已经在数据库了,而且自从插入后没有修改过,状态应当是 Unchaged。
假如你有其他想法,盼望在 SaveChanges 之后实体的状态稳定回 Unchaged,可以如许调用 SaveChanges 方法。
  1. context.SaveChanges(acceptAllChangesOnSuccess: false);
复制代码
acceptAllChangesOnSuccess 参数设置为 false 后,数据库实行乐成后不会改变实体的当前状态。于是,数据库中插入新记载后,实体状态还是 Added。
  1. ---------- 添加后 ------------
  2. Pet {Id: -2147482647} Added
  3.     Id: -2147482647 PK Temporary
  4.     Category: '爬行动物'
  5.     Description: '不会游泳的巴西龟'
  6.     Name: 'Jack'
  7. ---------- 提交后 ------------
  8. Pet {Id: 1} <strong>Added</strong>
  9.     Id: 1 PK
  10.     Category: '爬行动物'
  11.     Description: '不会游泳的巴西龟'
  12.     Name: 'Jack'
复制代码
如许做大概会导致逻辑错误,除非你有特别用途,好比如许用。
  1. using var context = new TestDbContext();
  2. context.Database.EnsureDeleted();
  3. context.Database.EnsureCreated();
  4. // 处理事件
  5. context.ChangeTracker.Tracked += (_, e) =>
  6. {
  7.     var backupcolor = Console.ForegroundColor;
  8.     Console.ForegroundColor = ConsoleColor.Green;
  9.     Console.WriteLine($"实体被追踪:\n{e.Entry.DebugView.LongView}\n");
  10.     Console.ForegroundColor = backupcolor;
  11. };
  12. context.ChangeTracker.StateChanged += (_, e) =>
  13. {
  14.     var bkColor = Console.ForegroundColor;
  15.     Console.ForegroundColor = ConsoleColor.Blue;
  16.     Console.WriteLine($"实体(ID={e.Entry.Property(nameof(Pet.Id)).CurrentValue})状态改变:{e.OldState} --> {e.NewState}\n");
  17.     Console.ForegroundColor = bkColor;
  18. };
  19. // 新实体
  20. Pet p = new Pet { Name = "Tom", Description = "会游泳的鸟", Category = "猛禽" };
  21. context.Add(p);
  22. // 保存,但状态不改变
  23. context.SaveChanges(false);
  24. // 因为是 Added 状态,所以还可以继续insert
  25. p.Name = "Simum";
  26. p.Description = "三手青蛙";
  27. p.Category = "两栖动物";
  28. // 保存,状态改变
  29. context.SaveChanges();
  30. // 把它们查询出来看看
  31. var set = context.Set<Pet>();
  32. Console.WriteLine("\n数据库中的记录:");
  33. foreach(var pp in set)
  34. {
  35.     Console.WriteLine($"{pp.Id}  {pp.Name}  {pp.Description}  {pp.Category}");
  36. }
复制代码
上面代码中,侦听了两个变乱:Tracked——当 EF Core 开始跟踪某个实体时发生;当有实体的状态改变之后发生。着实尚有一个 StateChanging 变乱,是在实体状态即将改变时发生。总结来说就是:状态改变之前发生 StateChanging 变乱,改变之后发生 StateChanged 变乱。要注意,StateChanged 和 StateChanging 变乱在 EF Core 初次追踪实体时不会引发。好比,刚开始追踪时状态为 Unchanged,不会引发变乱,而之后状态变为 Added,就会引发变乱(最开始谁人状态不会触发变乱)。
上面代码处置处罚 Tracked 变乱,当开始追踪某实体时,打印一下调试信息,记载某状态;处置处罚 StateChanged 变乱,在开始追踪状态后,状态发生改变之后打印厘革前后的状态。
代码运行效果如下:

起首,new 了一个 Pet 对象,赋值,再调用 Add 方法添加到数据聚会合,此时状态会被改为 Added。Tracked 变乱输出第一块绿色字体,表现实体开始追踪的状态为 Added,ID 值是随机分配的负值,并分析是临时主键值。
然后调用 SaveChanges 方法并转达 false 给acceptAllChangesOnSuccess 参数,表明 INSERT 进数据库后,状态不改变,还是 Added。
然后,还是用谁人实体实例,改变一部属性值,由于它的状态仍旧是 Added,调用 SaveChanges() 方法时未传参数,它会调用 SaveChanges(acceptAllChangesOnSuccess: true),效果是这次实体的状态变成了 Unchanged。就是输出效果中蓝色字体那一行。此时实体的 ID=2,记着这个值,待会儿用到。
再以后,咱们 foreach 语句给 DbSet 会触发 EF Core 去查询数据库,于是,我们看到,控制台在“数据库中的记载:”一行之后又发生了 Tracked 变乱,有一个 ID=1 的实体被追踪了,它刚从数据库中查询出来,就是第二块绿色字体那边。
这时间你是不是迷乎了?不是从数据库查出两条记载吗,为什么只有 ID=1 的被追踪了,ID=2 呢?着实,ID = 2 已经被追踪了。忘了吗?它前面不是从 Added 状态变为 Unchanged 状态吗。这是由于咱们这连续串利用都在同一个 DbContext 实例的生命周期举行的,EF Core 对实体的追踪不会断开。
假如你把上面的代码改成如许,那就明确了。
  1.     static void Main(string[] args)
  2.     {
  3.         using (var context = new TestDbContext())
  4.         {
  5.             context.Database.EnsureDeleted();
  6.             context.Database.EnsureCreated();
  7.             // 处理事件
  8.             context.ChangeTracker.Tracked += OnTracked;
  9.             context.ChangeTracker.StateChanged += OnStateChanged;
  10.             // 新实体
  11.             Pet p = new Pet { Name = "Tom", Description = "会游泳的鸟", Category = "猛禽" };
  12.             context.Add(p);
  13.             // 保存,但状态不改变
  14.             context.SaveChanges(false);
  15.             // 因为是 Added 状态,所以还可以继续insert
  16.             p.Name = "Simum";
  17.             p.Description = "三手青蛙";
  18.             p.Category = "两栖动物";
  19.             // 保存,状态改变
  20.             context.SaveChanges();
  21.         }
  22.         // 把它们查询出来看看
  23.         using(var context2 = new TestDbContext())
  24.         {
  25.             // 依旧要处理事件
  26.             context2.ChangeTracker.Tracked += OnTracked;
  27.             context2.ChangeTracker.StateChanged += OnStateChanged;
  28.             var set = context2.Set<Pet>();
  29.             Console.WriteLine("\n数据库中的记录:");
  30.             foreach (var pp in set)
  31.             {
  32.                 Console.WriteLine($"{pp.Id}  {pp.Name}  {pp.Description}  {pp.Category}");
  33.             }
  34.         }
  35.     }
  36.     // 下面两个方法处理事件
  37.     static void OnTracked(object? _,  EntityTrackedEventArgs e)
  38.     {
  39.         var backupcolor = Console.ForegroundColor;
  40.         Console.ForegroundColor = ConsoleColor.Green;
  41.         Console.WriteLine($"实体被追踪:\n{e.Entry.DebugView.LongView}\n");
  42.         Console.ForegroundColor = backupcolor;
  43.     }
  44.     static void OnStateChanged(object? _,  EntityStateChangedEventArgs e)
  45.     {
  46.         var bkColor = Console.ForegroundColor;
  47.         Console.ForegroundColor = ConsoleColor.Blue;
  48.         Console.WriteLine($"实体(ID={e.Entry.Property(nameof(Pet.Id)).CurrentValue})状态改变:{e.OldState} --> {e.NewState}\n");
  49.         Console.ForegroundColor = bkColor;
  50.     }
复制代码
如今再次运行,看看效果是不是符合你当初的渴望。

如今的环境是:向数据库插入记载是第一个 DbContext 实例,完事后就开释了,实体追踪器自然就挂了;随后创建了第二个 DbContext 实例,这时间从数据库中查询出两条记载都是没有被追踪的,以是要启动追踪,自然就能引发两次 Tracked 变乱了。
好了,各位,本日咱们就粗浅地聊到这里。反面老周还会继续讨论实体追踪的话题,本文紧张是让大同伴们相识一下实体的状态厘革。
 

免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表