• EFCore学习笔记(9)——实体跟踪


    一、引言

    在用EF Core时,遇到了如下错误提示:

    The instance of entity type ‘XXX’ cannot be tracked because another instance with the same key value for {‘XX’} is already being tracked.

    因为EF Core我也是初学,之前只以以下写法简单使用过:

    using(var context = new XXXContext())
    {
    	var xDbSet = context.XDbset...;
    	// do some operations
    	context.SaveChanges();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以遇到这种错误,一脸懵逼。
    只能通过IDE的错误提示大概了解是具有相同键值的实例被重复跟踪而报错。
    于是我先上网搜索了一下,找到一种解决方法,

    var selfDevice = context.Devices.Single(d => d.DeviceId.Equals(this.Device.DeviceId));
    context.Entry(selfDevice).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
    
    • 1
    • 2

    该方法就是在从context(上下文)取得实体实例之后,把该实例的状态设为Detached,然后该错误就消失了。

    这个错误直觉上来说,就是多次获取同一实体,EF Core重复跟踪实体,导致错误。
    因为同一实体被多次获取后,在内存中出现了多个指向它的对象,而你可能对多个对象做不同的变更,这就可能会导致后期写入数据库时产生不确定性,我写入的时候到底应该按哪个变更来写入呢?计算机执行是不能有二义性的。
    当然以上是我的猜测,其中还有一些注意项,得往下深入学习才知道。

    也借此机会,参考官方文档,稍微深入一下EF Core的实例跟踪相关知识。

    正式学习之前,你得具备一些EF Core的常识,
    比如,后面说的DbContext是上下文。
    变更跟踪有时也可以叫实体跟踪、实例跟踪,因为EF Core使用时实体是在DbContext中的,而DbContext需要实例化生成实例才能用。你所做的变更,是对实体的变更。


    二、EF Core中的变更跟踪

    首先得知道什么是变更跟踪,跟踪的是什么,为什么要跟踪?
    每个DbContext实例都会跟踪实体所做的变更。当调用SaveChanges时,这些被跟踪的实体会反过来将变更写入数据库,驱动数据库变更。

    回答一下本节开始的问题:

    1. 实例跟踪就是使用EF Core时,EF Core会对上下文中对实体做的操作进行跟踪记录,以便后续同步到数据库中。
    2. 跟踪的对象是上下文中的实体。
    3. 跟踪的目的是将对实体的更改同步到数据库。

    简单讲就是var context = new XXXContext()得到上下文后,用诸如context.DbSet.property = xxx的语句对上下文中的实体做出更改会被跟踪到,然后调用SaveChanges会把对实体的更改写入到数据库中。
    PS:实体在上下文中由DbSet暴露。

    本文主要介绍EFCore变更跟踪,以及它怎样与数据库查询(Query)和更新(Update)关联起来。

    1. 怎样跟踪实体

    当实体实例处于以下状态时,会被跟踪:

    • 从数据库查询中返回时。
    • 通过Add、Attach、Update或类似方法显式附加到DbContext时。
    • 被检测为连接到已有被跟踪实体的新实体时。// 指导航属性之类的

    当处于以下状态时,实体实例不再被跟踪:

    • DbContext被释放。
    • 变更跟踪器被清除(EF Core 5.0以后)。
    • 实体被显式分离。

    DbContext被设计成一个短期的工作单元,就像DbContext初始化和配置中描述的那样(用完就关,下次用时再重新获取)。这意味着释放DbContext是停止跟踪实体的标准方法。换句话说,DbContext的生命周期应该是这样的:

    1. 创建DbContext实例
    2. 跟踪(某些)实体
    3. 更改实体
    4. 调用SaveChanges更新数据库
    5. 释放DbContext实例

    💡小提示
    采用该方法时,不需要清除变更跟踪器或显式地分离实体实例。
    不过,如果你确实需要分离实体,那么调用ChangeTracker.Clear会比逐个分离实体更效率。

    2. 实体状态

    每个实体都会关联一个实体状态(EntityState)中,有以下几种实体状态:

    • Detached(分离的),表示实体没有被DbContext跟踪。
    • Added(添加的),表示实体是全新的(程序中新new出来的对象,还未存在数据库中)并且还未插入到数据库中。当调用SaveChanges时,它们被会被插入数据库。
    • Unchanged(不变的),实体从数据库中查询获取后,还没有被更改。所有从数据库查询返回的实体最初都是这种状态。
    • Modified(修改的),实体从数据库查询获取后,已经发生更改。当调用SaveChanges时,它们被更新。
    • Deleted(删除的),实体原本存在数据库中,当调用SaveChanges时,被标记为已删除的。

    EF Core跟踪改变是在属性层面的。例如,如果只修改单个属性值,那么数据库就只更新该值。不过,只有当实体本身处于Modified状态时,属性才能被标记为modified。(或者,换个角度看,Modified状态意味着至少有一个属性值被标记为Modified。)

    1. 跟踪改变是在属性层面(property level)指context是可以跟踪到属性的变化(或者属性是可跟踪的最小单位?)。
    2. 只有当实体本身处于Modified状态,属性才能被标记为Modified。大概指的是像added这种情况?全新的实体就没有Modified的一说。

    下表概述了不同状态的特性(下表Modified对应的SaveChanges时的动作是Update,可以很好说明什么是属性层面的跟踪):

    实体状态是否被DbContext跟踪存在数据库中属性修改调用SaveChanges时的动作
    Detached-(分离后就是程序里的东西了)--
    Added否(新new出来的,还未加入数据库)-Insert
    Unchanged-
    ModifiedUpdate
    Deleted-Delete

    ❗注意
    为了更清晰的描述,本文使用关系数据库的术语。
    NoSQL数据库通常支持类似的操作,但可能使用的名称不同。

    3. 跟踪查询

    EF Core变更跟踪在使用同一个DbContext实例来查询实体和调用SaveChanges来更新实体时效果最好。这是因为EF Core会自动跟踪被查询实体的状态,然后在调用SaveChanges时检测这些实体的变化。

    与显式跟踪实体实例相比,这种方法有几点优势:

    • 非常简易。实体状态几乎不需要显式操作——EF Core会负责状态的变更。
    • 更新仅限于实际更改的值。
    • 🔺影子属性的值会被保留,并在需要时使用。当外键存储在影子状态时,这尤其重要。
    • 🔺属性的原始值会自动保留,并用于有效更新。

    4. 简单的查询和更新

    例如,考虑下面这个简单的 blog/posts模型(博客/帖子):

    public class Blog
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        public IList<Post> Posts { get; } = new List<Post>();
    }
    
    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public int? BlogId { get; set; }
        public Blog Blog { get; set; }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们可以使用该模型来查询blogs和posts,然后做一些变更并更新到数据库:

    using var context = new BlogsContext();
    
    var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
    
    blog.Name = ".NET Blog (Updated!)";
    
    foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
    {
        post.Title = post.Title.Replace("5", "5.0");
    }
    
    context.SaveChanges();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    调用SaveChanges会导致下列数据库更新,以SQLite为例:

    -- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
    UPDATE "Blogs" SET "Name" = @p0
    WHERE "Id" = @p1;
    SELECT changes();
    
    -- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
    UPDATE "Posts" SET "Title" = @p0
    WHERE "Id" = @p1;
    SELECT changes();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    变更跟踪器调试视图(change tracker debug view)是一种不错的方法,它用于查看哪些实体被跟踪以及它们的状态是什么。例如,在上面示例中插入(在调用SaveChanges之前)以下代码:

    context.ChangeTracker.DetectChanges();
    Console.WriteLine(context.ChangeTracker.DebugView.LongView);
    
    • 1
    • 2

    生成以下输出:

    Blog {Id: 1} Modified
      Id: 1 PK
      Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
      Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
    Post {Id: 1} Unchanged
      Id: 1 PK
      BlogId: 1 FK
      Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
      Title: 'Announcing the Release of EF Core 5.0'
      Blog: {Id: 1}
    Post {Id: 2} Modified
      Id: 2 PK
      BlogId: 1 FK
      Content: 'F# 5 is the latest version of F#, the functional programming...'
      Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
      Blog: {Id: 1}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    特别注意:

    • Blog.Name属性被标记为Modified(Name: ‘.NET Blog (Updated!)’ Modified Originally ‘.NET Blog’),并且这会导致blog变成Modified状态。
    • post 2的Post.Title属性被标记为Modified(Title: ‘Announcing F# 5.0’ Modified Originally ‘Announcing F# 5’),这会导致该post变成Modified状态。
    • post 2的其他属性没有改变,因此没被标记成Modified。这就是这些值没有包含在数据库更新中的原因。
    • 另一篇post没有发生任何更改。这就是为什么它仍然处于Unchanged状态以及没有被包含在数据库更新中。

    5. 查询,然后插入、更新和删除

    在同一个工作单元中,像前面示例那样的更新是可以与插入和删除合并的。例如:

    using var context = new BlogsContext();
    
    var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
    
    // Modify property values
    blog.Name = ".NET Blog (Updated!)";
    
    // Insert a new Post
    blog.Posts.Add(
        new Post
        {
            Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
        });
    
    // Mark an existing Post as Deleted
    var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
    context.Remove(postToDelete);
    
    context.ChangeTracker.DetectChanges();
    Console.WriteLine(context.ChangeTracker.DebugView.LongView);
    
    context.SaveChanges();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    本例中:

    • 一个blog和与其相关的posts都从数据库中查询得到,并且被跟踪
    • Blog.Name属性发生改变
    • 一个新的post添加到了blog现有的posts集合中
    • 一个现有的post通过调用DbContext.Remove被标记为删除

    在调用SaveChanges之前再看看变更跟踪器调试视图,可以看到EF Core是如何跟踪这些变更的:

    Blog {Id: 1} Modified
      Id: 1 PK
      Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
      Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
    Post {Id: -2147482638} Added
      Id: -2147482638 PK Temporary
      BlogId: 1 FK
      Content: '.NET 5.0 was released recently and has come with many...'
      Title: 'What's next for System.Text.Json?'
      Blog: {Id: 1}
    Post {Id: 1} Unchanged
      Id: 1 PK
      BlogId: 1 FK
      Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
      Title: 'Announcing the Release of EF Core 5.0'
      Blog: {Id: 1}
    Post {Id: 2} Deleted
      Id: 2 PK
      BlogId: 1 FK
      Content: 'F# 5 is the latest version of F#, the functional programming...'
      Title: 'Announcing F# 5'
      Blog: {Id: 1}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    注意到:

    • blog被标记为Modified。这会生成一次数据库更新操作。
    • Post2被标记为Deleted。这会生成一次数据库删除操作。
    • 一篇带有临时ID的新post与blog 1关联,并被标记为Added。这将生成一次数据库插入操作。

    当调用SaveChanges时,这会导致以下数据库命令(使用SQLite):

    -- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
    UPDATE "Blogs" SET "Name" = @p0
    WHERE "Id" = @p1;
    SELECT changes();
    
    -- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
    DELETE FROM "Posts"
    WHERE "Id" = @p0;
    SELECT changes();
    
    -- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
    INSERT INTO "Posts" ("BlogId", "Content", "Title")
    VALUES (@p0, @p1, @p2);
    SELECT "Id"
    FROM "Posts"
    WHERE changes() = 1 AND "rowid" = last_insert_rowid();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    💡小提示
    调用ChangeTracker.HasChanges()以确定是否发生了任何会导致SaveChanges对数据库进行更新的更改。如果HasChanges返回false,SaveChanges将不执行任何操作。


    三、结尾

    从这篇文可以知道上下文中实体变更是由变更跟踪器跟踪记录的。并且实体是有状态的,不同状态在SaveChanges回写数据库时会生成不同的数据库操作。但是文章一开始提到的那个错误,似乎并没有得到明确解答。

  • 相关阅读:
    c# 将excel导入 sqlite
    做室内装修设计需要什么资质,办理装修设计资质办理标准是怎样的
    [Vue3]Vue3基本概述
    线程同步之互斥量
    c/c++语言比较大小结果与类型有关。
    golang同步原语——sync.Mutex
    SpringCloud微服务
    PowerBI 8月更新,数据标签条件格式
    SAST-数据流分析方法-理论
    iconfont 中图标以及字体库在页面中的引用
  • 原文地址:https://blog.csdn.net/BadAyase/article/details/126780969