在 EFCore 进行对象映射的时候,它必须知道我们对实体对象做出了什么修改,以便生成 SQL 语句。实体类其实就是一个简单的 C# 类型,它并没有实现属性更改的通知机制,EFCore 最终是通过快照来跟踪实体改变的。

所谓快照跟踪,是 EFCore 将某些实体对象列入跟踪列表,记录它们的初始状态,然后在 SaveChanges 方法执行时将它们目前的状态与记录进行对比,这样 EFCore 就得知这些对象发生了什么改变,以便生成对应的 SQL 语句。

重要的是知道哪些实体会被跟踪,默认情况下,通过 DbContext 查询到的所有实体都会被跟踪。在 EFCore 的跟踪机制中,所有实体可以分为以下五种状态:

  • Added:实体被跟踪,但未添加进数据库。
  • Unchanged:实体被跟踪,但未改变,即它已在数据库中,但当前状态与数据库中的数据一致。
  • Modified:实体被跟踪,且已改变,即它已在数据库中,且当前状态与数据库中的数据不同。
  • Deleted:实体被跟踪,且将被删除,即它已在数据库中,但下次 SaveChanges 时要删除它。
  • Detached:实体未被跟踪。

在执行 SaveChanges 方法时,处于 Detached 和 Unchanged 状态的实体对象会被忽略,处于 Added 状态的实体对象会被添加进数据库,处于 Modified 状态的实体对象会在数据库进行修改,处于 Deleted 状态的实体对象会在数据库中进行删除。

如何查看一个实体对象当前的状态呢?只需要获取它的 Entry 即可。代码如下:

var book = ctx.Books.Single(e => e.Id == 3073);
Console.WriteLine(ctx.Entry(book).State); //输出 Unchanged

从这里可以看出,查询出来的 book 已经被跟踪了,实际上,一般默认情况下通过 DbContext 查询出来的所有实体对象都被会跟踪。在某些情况下,我们如只需要读取这些数据,而不需要写入的话,跟不跟踪就无所谓了,这个时候为了更好的性能,我们可以使用 AsNoTracking 方法关闭跟踪:

var book = ctx.Books.AsNoTracking().Single(e => e.Id == 3073);
Console.WriteLine(ctx.Entry(book).State); //输出 Detached

关闭跟踪后,即便对查询出来的实体对象有修改,在 SaveChanges 时依旧不会被更新到数据库。

如果查询时使用了投影,可能返回的类型并不是实体类,而可能是我们自定义的匿名类型,那么 EFCore 会不会跟踪这些匿名类型对象呢?答案是视情况而定。如果该对象的属性包含实体类型引用(例如下面的 Book = e),那么 EFCore 依旧会跟踪被引用的实体类型,如果该对象的属性不含实体类型引用(即使该对象某些属性的值来自于某个实体类型,例如下面代码中的 Title = e.Title ),EFCore 不会进行任何跟踪。

var book = ctx.Books.Select(e => new { Title = e.Title, Book = e }).Single(e => e.Book.Id == 3073);
Console.WriteLine(ctx.Entry(book.Book).State); //输出 Unchanged

在之前关于 IQueryable 的文章里说到, EFCore 绝大部分的默认情况下采用 Server Evaluation,但在顶层投影的时候可能采用 Client Evaluation,在顶层投影时,我们可能会将实体对象传入某个自定义方法,这个实体对象也会被 EFCore 跟踪。

由上面所说的规则可以知道,在 SaveChanges 之前,我们对实体对象的修改都只是在内存中修改,并不会对数据库造成影响,最终对数据库的修改都集中在 SaveChanges 执行时。EFCore 只是保存了开始跟踪时的初始状态,在 SaveChanges 时也只关注实体的最终状态,那我们在这两个状态间作出的其它修改会有影响吗?看似没有,实际上还是会有的,这主要发生在重复查询同个实体的时候:

var book = ctx.Books.Single(e => e.Id == 3073);
book.Price = 2022;
var books = ctx.Books.Where(e => e.Id > 3000);
foreach (var b in books)
{
    if (b.Id == 3073)
    {
        Console.WriteLine(b.Price);
    }
}

在这段代码中,先对 Id == 3073 的 book 进行了修改,但是没有 SaveChanges,然后继续查询 Id > 3073 的对象,这个时候一定会再次查询到 Id == 3073 的这个对象,然后在迭代中输出它的价格,最终输出 2022。

这说明,EFCore 查询到实体的时候,会先检测当前跟踪列表中是否包含这个实体对象,如果没有,EFCore 创建对象、将它跟踪、将它返回,如果有,EFCore 不会创建新的对象,也不会对已跟踪的对象进行任何修改,而是直接返回这个已跟踪对象的引用,而这个已跟踪的对象可能已经处于 Modified 状态了。即在没有 SaveChanges 的情况下,对实体对象的修改不会被清除,即便这个实体已被重复查询。这样的设计保证了程序员对被跟踪的实体对象的修改在逻辑上是持续存在且有效的,只是在 SaveChanges 前尚未影响到数据库而已。