2016 年 8 月

第 31 卷,第 8 期

数据点 - EF Core 更改-跟踪行为: Unchanged、Modified 和 Added

作者 Julie Lerman

Julie Lerman你看到我在该列的标题中进行了什么操作吗? 你可能已经识别出 Entity Framework (EF) 中作为枚举用于 Entity­State 的 Unchanged、Modified 和 Added。它们也帮助我描述 EF Core 中更改跟踪的行为(与 Entity Framework 早期版本相比)。更改跟踪在 EF Core 中已变得更一致,因此当你在使用断开连接的数据时,对预期结果会更有自信。

请记住,虽然 EF Core 尝试保留 Entity Framework 早期版本的范例和大部分语法,但 EF Core 仍是一组新的 API,它是从头编写的全新的基本代码。因此,请不要想当然地认为一切都会完全按照以前的行为运行,知道这一点很重要。更改跟踪器恰好是一个临界案例。

因为 EF Core 的第一次迭代旨在符合 ASP.NET Core,很多的工作集中在在断开的状态,也就是说,确保 Entity Framework 可以处理来自带外对象的状态,即 EF 未跟踪这些对象的情况。典型的应用场景是接受来自客户端应用程序的对象以保留到数据库的 Web API。

在我的 2016 年 3 月的数据点专栏中,我撰写了“处理 EF 中断开连接的实体的状态”(msdn.com/magazine/mt694083)。文章的重点是向断开连接的实体分配状态信息,并在向 EF 的更改跟踪器传回这些对象时,与 EF 共享该信息。尽管我使用了 EF6 来展示示例,该模式对 EF Core 仍然适用,因此在讨论完 EF Core 行为后,我将演示如何在 EF Core 中实现该模式的示例。

使用 DbSet 跟踪: Modified

DbSet 始终包括 Add、Attach 和 Remove 方法。对于单个对象来说,这些方法的结果很简单,它们将对象的状态设置为相关的 EntityState。Add 产生的结果是 Added,Attach 产生的结果是 Unchanged,而 Remove 将状态更改为 Deleted。有一个例外:如果你删除一个实体(已知为 Added),则它将从 DbContext 分离,因为不再需要跟踪新实体。  在 EF6 中,当你借助图形使用这些方法时,在相关对象上取得的效果并不那么一致。以前未跟踪的对象无法删除,并引发一个错误。已跟踪的对象可能更改了状态,也可能没有更改,具体取决于这些状态的内容。我在 EF6 中创建了一组测试,以便理解各种行为(你可以在 bit.ly/28YvwYd 中的 GitHub 上找到)。

创建 EF Core 时,EF 团队在整个测试版中对这些方法的行为进行了试验。EF Core RTM 中的方法与其在 EF6 和早期版本中是不同的。在大多数情况下,对这些方法的更改产生更一致的行为,你可以依赖于此。但是理解它们如何进行了更改是很重要的。

 当你对附加了图形的对象使用 Add、Attach 和 Remove 时,图形中每个对象的状态(更改跟踪器未知的状态)都将设置为由方法识别的状态。让我使用我最喜爱的来自“Seven Samurai”电影的 EF Core 模型(附加了电影引述和其他相关信息的 samurais),对此进行解释。

如果 samurai 是新的且未被跟踪,则 Samurais.Add 将 samurai 的状态设置为 Added。当调用 Add 时,如果 samurai 附加了引述,则其状态将被设置为 Added。这正是所希望的行为,实际上与 EF6 中的行为是相同的。

如果你将新的引述添加到现有的 samurai,并且你没有按照我的建议将 newQuote.SamuraiId 设置为 Samurai.Id 的值,而是设置导航属性,newQuote.Samurai=oldSamurai。在断开的情况下,即引述和 oldSamurai 均未被 EF 跟踪,则 Quotes.Add(newQuote) 将执行与之前相同的操作。它将 newQuote 标记为 Added,并将 oldSamurai 标记为 Added。SaveChanges 将两个对象都插入数据库,而你会在数据库中拥有重复的 oldSamurai。

如果你处于客户端应用程序中,例如,Windows Presentation Foundation (WPF),并且使用上下文查询 samurais,然后使用同一上下文实例调用 context.Quotes.Add(newQuote),则上下文已经知道 oldSamurai,不会将其 Unchanged 状态更改为 Added。这即是我所说的不更改已跟踪对象的状态。

更改跟踪器在断开连接的图形中影响相关对象的方式明显不同,在 EF Core 中使用这些方法时,应记住这些差别。

Rowan Miller 在 GitHub 问题 (bit.ly/295goxw) 中概述了新行为:

Add: 添加每个可访问的实体(该实体还未被跟踪)。

Attach: 附加每个可访问的实体,可访问的实体具有存储区生成的密钥且未被分配密钥值除外;这些都将被标记为已添加。

更新: 与 Attach 相同,但实体被标记为已修改。

删除: 与 Attach 相同,然后将根标记为已删除。由于级联删除现在发生在 SaveChanges 上,这可以使级联规则稍后流到实体。

在该列表中,你可能注意到对 DbSet 方法的另一处更改: DbSet 最终具有 Update 方法,它将未跟踪的对象设置为 Modified。万岁! 始终需要添加或附加,然后将状态显式设置为 Modified,这真是一个极佳的替换方法。

DbSet Range 方法: 也是 Modified

在 EF6 中引入了 DbSet 的两种 range 方法(AddRange 和 RemoveRange),使你能够传入类型数组。这带来了显著的性能提升,因为更改跟踪器仅使用一次(而不是在数组的每个元素上使用)。该方法的确调用前面详细介绍的 Add 和 Remove,因此,你需要考虑图形对象被影响的程度。

在 EF6 中,range 方法仅因 Add 和 Remove 存在,但现在,EF Core 引入了 UpdateRange 和 AttachRange。对传入 Range 方法的每个对象或图形单独调用的 Update 和 Attach 方法的行为与前面介绍的相同。

DbContext 更改跟踪方法: Added

如果你在 DbContext 引入前使用 EF ObjectContext,你可能会记得 ObjectContext 具有 Add、Attach 和 Delete 方法。因为上下文无法知道目标实体属于哪个 ObjectSet,所以你需要添加一个 ObjectSet 名称的字符串表示形式,将其作为参数。这非常复杂,我们大部分人只需使用 ObjectSet Add、Attach 和 Delete 方法,这样会更简单。DbContext 出现后,这些复杂的方法没有了,你只能通过 DbSet 进行 Add、Attach 和 Remove。

在 EF Core 中,Add、Attach 和 Remove 方法重新作为 DbContext 的方法,并与 Update 和四个相关的 Range 方法(AddRange 等)配合使用。但是现在,这些方法更智能。它们现在可以确定类型,并自动将实体与正确的 DbSet 相关联。这样的确很方便,因为你能够编写泛型代码,而无需实例化 DbSet。代码更简单,更重要的是,它更易于发现。以下是 EF6 和 EF Core 的对比:

private void AddToSetEF6<T>(T entity) where T : class {Pull
  using (var context = new SamuraiContext()) {
    context.Set<T>().Add(entity);
  }
}
private void AddToSetEFCore(object entity) {
  using (var context = new SamuraiContext()) {
    context.Add(entity);
   }
}

Range 方法将更有帮助,因为你可以传入各种类型,并且 EF 都可以识别它们:

private void AddViaContextEFCore(object[] entitiesOfVaryingTypes) {
  using (var context = new SamuraiContext()) {
     context.AddRange(entitiesOfVaryingTypes);
  }
}

DbContext.Entry: Modified—注意行为中的此更改

尽管我们已经警告过 - EF Core 并非 EF6,我们不应期望在 EF Core 中使用与 EF6 中相似的代码,但当大量行为执行时,对此不抱有期望仍然很难。而 DbContext.Entry 就是其中一例,了解它如何进行了更改是很重要的。

对我来说,这种更改是很受欢迎的,因为它使更改跟踪变得一致。在 EF6 中,DbSet Add(以及其他)方法以及 DbContext.Entry 方法与 State 属性结合,对实体和图形具有相同的影响。因此,使用 DbContext.Entry(object).State=EntityState.Added 将使图形中的所有对象(还未被跟踪)变为 Added。

此外,在将图形对象传入更改跟踪器前,从来没有直观的方法将其断开连接。

在 EF Core 中,DbContext.Entry 现在只影响传入的对象。如果该对象具有其他与之相连的关联对象,则 DbContext.Entry 将忽略它们。

如果你已习惯使用 Entry 方法将图形连接到 DbContext 实例,你就会发现这样的更改为何如此显著。这意味着你可以面向单独对象(即使它是图形的一部分)。

更重要的是,现在可以显式使用 DbSet 和 DbContext 跟踪方法(Add 和类似方法)以显式使用图形,并可使用 DbContext.Entry 方法以专门使用单个对象。这与我将解释的下一更改相结合,意味着现在将对象图形传入 EF Core 更改跟踪器时会具有明确的选项供选择。

DbContext.ChangeTracker.TrackGraph: Added

TrackGraph 是 EF Core 中的一个全新概念。它为想要用 DbContext 开始跟踪的对象图形中的每个对象提供最大限度的控制。

TrackGraph 遍历图形(也就是说,它遍历图形中的每个对象)并将指定的函数应用到每个对象。该函数是 TrackGraph 方法的第二个参数。

最常见的例子是将每个对象的状态设置为通用状态。在下面的代码中,TrackGraph 将遍历 newSword 图形中的所有对象,并将其状态设置为 Added:

context.ChangeTracker.TrackGraph(newSword, e => e.Entry.State = EntityState.Added);

DbSet 和 DbContext 方法应用到 TrackGraph 的同一注意事项是:如果实体已被跟踪,则 TrackGraph 将忽略它。尽管此 TrackGraph 的特定用法与 DbSet 和 DbContext 跟踪方法的行为相同,但它的确为编写可重用代码提供了更多机会:

Lambda(该代码中的“e”)代表 EntityEntryGraphNode 类型。EntityEntryGraphNode 类型也显示一个称为 NodeType 的属性,当你键入 lambda 时,可能通过 IntelliSense 遇到它。这看起来像是供内部使用,且不会有 e.Entry.State 提供的效果,所以请小心,不要无意中使用它。

在断开连接的情况下,忽略的已跟踪对象的注意事项可能不相关。这是因为 DbContext 实例将是新的和空的,所以所有图形对象对 DbContext 来说应该是新的。但是,请考虑将图形集合传入 Web API 的可能性。现在,有机会对通用对象进行多个引用,且 EF 的更改跟踪将检查标识,以确定实体是否已被跟踪。这是一个理想的情形,因为它不会再次向更改跟踪器添加对象。

此默认行为设计用于涵盖最常见的方案。但是我能想象到,和我一样,你可能已经想到该模式可能失败的极端案例。

我将回到 2016 年 3 月的文章和我分享的模式,该模式用于在你的类上设置对象状态,然后读取该状态以向更改跟踪器告知对象的 Entity­State 应该是什么。现在,我可以通过让函数 TrackGraph 调用基于对象的 State 方法执行设置 EntityState 的任务,将该模式与 TrackGraph 方法结合在一起。

域类上的工作不会更改我在 3 月份的文章中所做的工作。我从定义本地跟踪的 ObjectState 的枚举开始:

public enum ObjectState {
    Unchanged,
    Added,
    Modified,
    Deleted
  }

然后我构建一个 IEntityObjectWithState 界面,它基于枚举显示 State 属性:

public interface IObjectWithState
{
  ObjectState State { get; set; }
}

现在,我修复我的类以实现该界面。例如,这是 Location 的一个小类,在这里有界面:

using SamuraiTracker.Domain.Enums;
using SamuraiTracker.Domain.Interfaces;
namespace SamuraiTracker.Domain
{
  public class Location : IObjectWithState
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public ObjectState State { get; set; }
  }
}

在 3 月份的文章中,我演示了如何构建能够管理其本地状态的智能类。我在此示例中没有重复该操作,意味着在我的示例中,我把 setter 保留为公用,并需要手动设置该状态。在完备的解决方案中,我将加强这些类以使其更像之前文章中所述。

对于 DbContext,我在帮助程序类中有一些静态方法,称为 ChangeTrackerHelpers,如图 1 中所示。

图 1 ChangeTrackerHelpers 类

public static class ChangeTrackerHelpers
    {
    public static void ConvertStateOfNode(EntityEntryGraphNode node) {
      IObjectWithState entity = (IObjectWithState)node.Entry.Entity;
      node.Entry.State = ConvertToEFState(entity.State);
    }
    private static EntityState ConvertToEFState(ObjectState objectState) {
      EntityState efState = EntityState.Unchanged;
      switch (objectState) {
        case ObjectState.Added:
          efState = EntityState.Added;
          break;
        case ObjectState.Modified:
          efState = EntityState.Modified;
          break;
        case ObjectState.Deleted:
          efState = EntityState.Deleted;
          break;
        case ObjectState.Unchanged:
          efState = EntityState.Unchanged;
          break;
      }
      return efState;
    }
  }

ConvertStateOfNode 是 TrackGraph 将调用的方法。它将对象的 EntityState 设置为由 ConvertToEFState 方法确定的值(将 IObjectWithState.State 值转换为 EntityState 值)。

完成此操作后,我现在可以使用 TrackGraph 开始跟踪我的对象以及它们正确分配的 EntityStates。例如,我传入称为 samurai 的对象图形(包括带有相关 Quote 和 Sword 的 Samurai):

context.ChangeTracker.TrackGraph(samurai, ChangeTrackerHelpers.ConvertStateOfNode);

在 EF6 解决方案中,我需要向更改跟踪器添加项,然后显式调用将在更改跟踪器中遍历所有实体以设置每个对象的相关状态的方法。EF Core 解决方案更为有效。请注意,我还未探索在单个事务中处理大量数据可能产生的性能影响。

如果你下载该专栏的示例代码,你将会看到我正在一个名为 Can­ApplyStateViaChangeTracker 的集成测试中使用这个新模式,我在其中创建该图形、向不同的对象分配各种状态,然后验证最终得到的 EntityState 值是否正确。

IsKeySet: Added

EntityEntry 对象保存每个实体的跟踪信息,它具有称为 IsKeySet 的新属性。IsKeySet 是 API 的一个很好的补充。它检查实体中的密钥属性是否具有值。这消除了有关对象密钥属性(或多个属性,如果有组合密钥)中的对象是否已有值(和相关代码)的猜测。IsKeySet 检查该值是否是你为密钥属性指定的特定类型的默认值。所以,如果它是 int,它是 0 吗? 如果它是 Guid,它是否等同于 Guid.Empty (00000000-0000-0000-0000-000000000000)? 如果该值不是类型默认值,则 IsKeySet 返回 true。

如果你知道,可以在你的系统中通过密钥属性值将新对象与预先存在的对象明确地区分开来,那么 IsKeySet 的确是确认实体状态的方便的属性。

EF Core 的注意事项

尽管 EF 团队已竭尽全力帮助你减少从 Entity Framework 早期版本转换到 EF Core 产生的种种困扰(复制了大量的语法和行为),它们仍是不同的 API,记住这一点很重要。移植代码将会很棘手,不推荐此操作(尤其是在早期,RTM 只有常用功能子集的时候)。但是,即使你创建新项目并确信 EF Core 中设置了你所需的功能,请不要想当然地认为一切会以同样的方式运行。我仍需就这一点提醒我自己。不过,我对 ChangeTracker 所做的更改很满意。它们可对处理断开连接的数据进行更清晰、更一致和更多的控制。

EF 团队在 GitHub 页上有路线图,我为此创建了一个方便的快捷方式:bit.ly/efcoreroadmap。这使你能够跟踪功能,尽管它不会列出诸如行为更改之类的细节信息。对此,我推荐进行测试,进行大量的测试,以确保一切按预期运行。如果你计划从 EF 的早期版本移植代码,你可能想要了解 Llewellyn Falco 的 Approval Tests (approvaltests.com),这些测试使你可以比较来自测试的输出,以确保输出继续匹配。


Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 .NET 主题的演示。她的博客地址是 thedatafarm.com/blog。她是“Entity Framework 编程”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。通过 Twitter 关注她:@julielerman 并在 juliel.me/PS-Videos 上观看其 Pluralsight 课程。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Erik Ejlskov Jensen
Erik Ejlskov Jensen 是 NNIT A/S 的 .NET 和数据库开发人员,他是 Microsoft 数据平台 MVP。他在 GitHub 上为 .NET 数据库开发人员提供了很多免费、开源的工具和库,包括广受欢迎的 Visual Studio 扩展“SQLite & SQL Server Compact Toolbox”。他还为 Entity Framework Core 创建了数据库提供程序。你可以关注他的 Twitter:@ErikEJ,以获取 .NET 数据访问开发的最新资讯。