2016 年 4 月

第 31 卷,第 4 期

数据点 - 处理 EF 中断开连接的实体的状态

作者 Julie Lerman

Julie Lerman断开连接的数据是在 Entity Framework 推出之前就已存在的老问题,就此而言,对于大多数数据访问工具来说亦是如此。它从来都不是一个很容易就能解决的问题。服务器通过线缆发送数据,但不知道在请求发送数据的客户端应用中可能发生什么,更不知道其是否将返回数据。然后请求中突然重新冒出一些数据。但是是相同的数据吗? 缺少这些数据是怎么回事? 这些数据是否发生了什么? 是全新的数据吗? 有这么多需要担心的问题!

作为 .NET 开发者,您可能见过许多解决此问题的办法模式。还记得 ADO.NET 数据集吗? 它不仅包含您的数据,而且封装了每行每列的所有更改状态信息。这不局限于“修改过的数据”或“新数据”,并且也保留了原始数据。当我们开始构建 ASMX Web 服务时,很容易就能将数据集序列化并通过线缆将其发送出去。如果消息传输到 .NET 客户端,该客户端可以将数据集反序列化并继续跟踪更改。等到是时候将数据返回服务时,您只需再次将其序列化,然后在服务器端,将其反序列化到一个数据集,同时确保所有更改跟踪信息完好无损以轻松地持久化到数据库。成功了!并且很容易就能做到。但是这涉及如此大量的来回通过线缆的数据。不仅是数据位,还有序列化的数据集结构创建了庞大的 XML。

唯一一个问题就是来回通过线缆的序列化消息的大小。Web 服务的好处在于您可以为各种平台提供服务,但是消息本身仅对另一个 .NET 应用程序有意义。2005 年,Scott Hanselman 编写的一篇标题特为“Returning DataSets from WebServices Is the Spawn of Satan and Represents All That Is Truly Evil in the World”(从 WebServices 返回数据集是撒旦的产物,代表了世界上一切真正邪恶的东西)(bit.ly/1TlqcB8) 的文章引发了人们对此问题的极大关注。

在 .NET 中,当 Entity Framework 取代 DataSets 作为主数据访问工具时,所有线缆上的状态信息全都消失。更改跟踪信息(原始值、当前值、状态)作为 ObjectContext 的一部分被 EF 存储,而非连同数据一同存储。但在 EF 的首次迭代中,由于序列化实体需要从 EF EntityObject 类型继承,所以是繁琐的消息。但是与实体数据一起来回通过线缆的消息已经丢失了其对状态的理解。曾习惯于重载数据集的我们这些人都被吓坏。那些已经熟悉处理断开状态的人因另一原因感到不安,那就是 EntityObject 基类要求。最终这个问题引起了 EF 团队的注意(事件出现转机),对于下一迭代,EF4、EF 已进化为支持普通旧 CLR 对象 (POCO)。这意味着 ObjectContext 可以维持简单类的状态,并且该类无需从 EntityObject 继承。

但是 EF4 仍未摆脱断开状态这个问题。EF 不知道实体状态,也无法跟踪。熟悉数据集的人们希望 EF 提供相同的神奇解决方案,并对不得不在轻量级消息和断开连接的更改跟踪之间做出选择感到不开心。与此同时,开发者(包括我)摸索出了许多通知服务器数据在传输时发生了什么的方法。您可以重新读取数据库中的数据,让 EF 进行比较,以弄清哪些已更改(如有)。您可以通过假设进行推测,比如“如果标识键值为 0,则它一定是新的”。 您可以围绕低级 API 编写查找状态并对其进行操作的代码。我之前做了很多尝试,但是没有一个解决方案令人满意。

当 EF4.1 推出时,具备轻量级 DbContext,而且 EF 团队赋予了它轻松通知上下文实体状态的能力。通过从 DbContext 继承的类,您可以编写如下代码:

myContext.Entity(someEntity).State=EntityState.Modified;

如果某个实体对于上下文来说是新的,它会强制上下文开始跟踪该实体,同时指定其状态。对于让 EF 知道基于 SaveChanges 构成了哪类 SQL 命令,这些足够了。在前面的示例中,结果会是 UPDATE 命令。Entry().State 对于了解某些数据通过线缆时的状态这一问题并没有帮助,但它能让您实施现今开发者通过 Entity Framework 普遍使用的绝佳模式,我将在本文中进行进一步说明。

虽然下个版本的 Entity Framework—EF Core(之前叫作 EF7 的框架)将更为一致地使用断开连接的图,但是您在本文中学到的模式仍能派上用场。

随着数据图来回传递,断开连接的数据问题随之升级。其中一个最大的问题是:当这些图表包含混合状态的对象时,服务器没有检测它收到的实体的各种状态的默认方式。如果您使用 DbSet.Add,默认情况下,实体将被全部标记。如果您使用 DbSet.Attach,实体将被标记为“Unchanged”。即使数据源自数据库并已填充键属性也是如此。EF 遵循指示,即“Add”或“Attach”。EF Core 将提供 Update 方法,并遵循“Attach”、“Attach”和“Delete”等的相同行为,但是将实体标记为“Modified”。需要注意的一个例外是:如果 DbContext 正在跟踪实体,将不覆盖实体的已知状态。但是对于断开连接的应用,我不对在连接从客户端返回的数据前对上下文进行跟踪抱有希望。

测试默认行为

我们来解释下默认行为,以突显此问题。为了演示,我制作了一个包含以下几个相关类的简单模型(可在“下载”处下载): Ninja、NinjaEquipment 和 Clan。Ninja 可以拥有 NinjaEquipment 集合并与单个 Clan 关联。接下来的测试需要一个使用新的 Ninja 和预存在且未编辑的 Clan 的图表。请记住,我通常会将值分配给 Ninja.ClanId 以避免与引用数据混淆。事实上,由于处理各关系间状态的 EF“魔力”,设置外键而非导航属性是可以帮助您避免诸多问题的做法。(参见我在 2013 年 4 月发布的专栏文章 [bit.ly/20XVxQi]“Why Does Entity Framework Reinsert Existing Objects into My Database?”(为什么 Entity Framework 将现有对象重新插入我的数据库?)了解详情)。 但是我这样编写代码是为了演示 EF 的行为。请注意,clan 对象填充了自己的 Id 键属性以指示它是来自数据库的预存在数据:

[TestMethod]
public void EFDoesNotComprehendsMixedStatesWhenAddingUntrackedGraph() {
  var ninja = new Ninja();
  ninja.Clan = new Clan { Id = 1 };
  using (var context = new NinjaContext()) {
    context.Ninjas.Add(ninja);
    var entries = context.ChangeTracker.Entries();
    OutputState(entries);
    Assert.IsFalse(entries.Any(e => e.State != EntityState.Added));
  }
}

我的 OutputState 方法迭代 DbEntityEntry 对象,其中上下文保留了每个被跟踪实体的状态信息并显示其状态的类型和值。

在测试中,我模拟了一个场景,在某处我创建了一个新的 Ninja 并将其与现有 Clan 关联起来。clan 仅仅是引用数据,尚未被编辑。然后我创建了一个新的上下文并使用 DbSet.Add 方法告诉 EF 跟踪此图表。我断定添加了全部被跟踪的实体。当测试通过后,结果证明上下文未能理解 Clan 的状态为“Unchanged”。测试输出表明 EF 认为这两类实体的状态均为“Added”:

Result StandardOutput:
Debug Trace:
EF6WebAPI.Models.Ninja:Added
EF6WebAPI.Models.Clan:Added

结果是,调用 SaveChanges 将插入 Ninja 和 Clan,从而产生 Clan 副本。如果我使用 Db­Set.Attach 方法,则实体将被标记为“Unchanged”,并且 SaveChanges 不会将新 Ninja 插入数据库,从而导致实际的数据持久性问题。

另一种常见方案是,从数据库检索 Ninja 及其 Equipment 并将其传递给客户端。然后客户端编辑其中一个 equipment 并添加新的 equipment 。实体的真实状态是:Ninja 是“Unchanged”,一个 Equipment 是“Modified”,另一个 Equipment 是“Added”。在没有帮助的情况下,DbSet.Add 和 DbSet.Attach 都不理解各种状态。所以现在是时候给予一些帮助了。

通知 EF 每个实体的状态

帮助 EF 理解图表中每个实体的正确状态的简单方法是以下一个包含四部分的解决方案:

  1. 定义一个表示可能对象状态的枚举。
  2. 创建一个具有枚举定义的 ObjectState 属性的接口。
  3. 在域实体中实施此接口。
  4. 覆盖 DbContext SaveChanges 以读取对象状态并通知 EF。

EF 拥有一个使用枚举器创建的 EntityState 枚举,其中实体状态为“Unchanged”、“Added”、“Modified”和“Deleted”。我将创建另一个枚举以供我的域类使用。这个枚举模仿了这四种状态,但是与 Entity Framework API 无关:

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

“Unchanged”位于第一位,所以它将是默认状态。如果您想指定值,请务必将“Unchanged”的值设置为零 (0)。

接下来,我将使用此枚举创建一个暴露属性以跟踪对象状态的接口。您可能偏爱创建一个基类或将此接口添加到您正在使用的基类:

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

该 State 属性仅限内存中使用并不需要存留到数据库。我已经更新了 NinjaContext 以确保实施它的所有对象忽略该属性。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
  modelBuilder.Types<IObjectWithState>().Configure(c => c.Ignore(p=>p.State));
}

通过定义的接口,我可以在我的类中实施它,例如在图 1 所示的 Ninja 类中实施。

图 1 实施 IObjectState 的 Ninja 类

public class Ninja : IObjectWithState
{
  public Ninja() {
    EquipmentOwned = new List<NinjaEquipment>();
  }
  public int Id { get; set; }
  public string Name { get; set; }
  public bool ServedInOniwaban { get; set; }
  public Clan Clan { get; set; }
  public int ClanId { get; set; }
  public List<NinjaEquipment> EquipmentOwned { get; set; }
  public ObjectState State { get; set; }
}

通过我的被定义为“Unchanged”的默认 ObjectState 枚举,每个 Ninja 都将变为“Unchanged”并且使用 Ninja 类编码的任何人都将负责根据需要设置 State 值。

如果依靠客户端设置状态是个问题,另一个受域驱动设计做法影响的方法可以确保 Ninja 对象更多参与到其行为和状态中。 2 显示了更丰富定义的 Ninja 类版本。注意:

  • Create factory 方法都会将 State 设置为“Added”。
  • 我已经隐藏了属性资源库。
  • 我创建了更改属性的方法,如果不是新 Ninja,State 会被设置为“Modified”(也就是说状态未被设置为“Added”)。

图 2 更智能的 Ninja 类

public class Ninja : IObjectWithState
{
  public static RichNinja CreateIndependent(string name, 
   bool servedinOniwaban) {
    var ninja = new Ninja(name, servedinOniwaban);
    ninja.State = ObjectState.Added;
    return ninja;
  }
  public static Ninja CreateBoundToClan(string name,
    bool servedinOniwaban, int clanId) {
    var ninja = new Ninja(name, servedinOniwaban);
    ninja.ClanId = clanId;
    ninja.State = ObjectState.Added;
    return ninja;
  }
  public Ninja(string name, bool servedinOniwaban) {
    EquipmentOwned = new List<NinjaEquipment>();
    Name = name;
    ServedInOniwaban = servedinOniwaban;
  }
  // EF needs parameterless ctor for queries
  private Ninja(){}
  public int Id { get; private set; }
  public string Name { get; private set; }
  public bool ServedInOniwaban { get; private set; }
  public Clan Clan { get; private set; }
  public int ClanId { get; private set; }
  public List<NinjaEquipment> EquipmentOwned { get; private set; }
  public ObjectState State { get; set; }
  public void ModifyOniwabanStatus(bool served) {
    ServedInOniwaban = served;
    SetModifedIfNotAdded();
  }
  private void SetModifedIfNotAdded() {
    if (State != ObjectState.Added) {
      State = ObjectState.Modified;
    }
  }
  public void SpecifyClan(Clan clan) {
    Clan = clan;
    ClanId = clan.Id;
    SetModifedIfNotAdded();
  }
  public void SpecifyClan(int id) {
    ClanId = id;
    SetModifedIfNotAdded();
  }
  public NinjaEquipment AddNewEquipment(string equipmentName) {
    return NinjaEquipment.Create(Id, equipmentName);
  }
  public void TransferEquipmentFromAnotherNinja(NinjaEquipment equipment) {
    equipment.ChangeOwner(this.Id);
  }
  public void EquipmentNoLongerExists(NinjaEquipment equipment) {
    equipment.State = ObjectState.Deleted;
  }
}

我修改了 NinjaEquipment 类型使其更丰富,另外,您可以看到在 AddNew、Transfer 和 NoLongerExists equipment 方法中我从中受益。修改确保了指回 Ninja 的外键被正确存留,如果 equipment 损坏,则需按照此特殊域的业务规则将其从数据库中完全删除。当将图表重新连接到 EF 时跟踪关系更改是个小技巧,我希望我可以在域级别密切控制此关系。例如,ChangeOwner 方法将 State 设置为“Modified”:

public NinjaEquipment ChangeOwner(int newNinjaId) {
  NinjaId = newNinjaId;
  State = ObjectState.Modified;
  return this;
}

现在,不论客户端是明确设置了状态,还是在客户端上使用了此等类(或用客户端语言类似编码的类),传递回 API 或服务的对象将定义其状态。

现在是时候在服务器代码中利用客户端状态了。

一旦我将对象或对象图表连接到上下文,上下文将需要读取每个对象的状态。此 ConvertState 方法将采用 ObjectState 枚举,返回匹配的 EntityState 枚举:

public static EntityState ConvertState(ObjectState state) {
  switch (state) {
    case ObjectState.Added:
      return EntityState.Added;
    case ObjectState.Modified:
      return EntityState.Modified;
    case ObjectState.Deleted:
      return EntityState.Deleted;
    default:
      return EntityState.Unchanged;
  }
}

接下来,在 NinjaContext 类中我需要一个方法来迭代实体(仅在 EF 保存数据前)并按照对象的 State 属性更新上下文对每个实体状态的理解。此处显示的这个方法称为 FixState:

public class NinjaContext : DbContext
{
  public DbSet<Ninja> Ninjas { get; set; }
  public DbSet<Clan> Clans { get; set; }
  public void FixState() {
    foreach (var entry in ChangeTracker.Entries<IObjectWithState>()) {
      IObjectWithState stateInfo = entry.Entity;
      entry.State = DataUtilities.ConvertState(stateInfo.State);
    }
  }
}

我考虑从 SaveChanges 内部调用 FixState 以实现完全自动化,但是在很多场景中有副作用。例如,如果您在连接的不用设置本地状态的应用程序中使用 IObjectState 实体,则 FixState 将始终将实体还原为“Unchanged”。最好将其留作要明确执行的方法。在我与 Rowan Miller 合著的“Programming Entity Framework: DbContext”《Entity Framework 编程:DbContext》这本书中,我们讨论了一些大家可能感兴趣的一些其他边缘情况。

现在我将创建一个使用这些新功能的新版先前测试,测试中包括我的更丰富版本的类。此新测试断定 EF 理解与现有 Clan 相关的全新 Ninja 的混合状态。在调用 NinjaContext.FixState 之前和之后,测试方法显示了 EntityState:

[TestMethod]
public void EFComprehendsMixedStatesWhenAddingUntrackedGraph() {
  var ninja = Ninja.CreateIndependent("julie", true);
  ninja.SpecifyClan(new  Clan { Id = 1, ClanName = "Clan from database" });
  using (var context = new NinjaContext()) {
    context.Ninjas.Add(ninja);
    var entries = context.ChangeTracker.Entries();
    OutputState(entries);
    context.FixState();
    OutputState(entries);
    Assert.IsTrue(entries.Any(e => e.State == EntityState.Unchanged));
}

测试通过,输出表明 FixState 方法将正确状态应用到了 Clan。如果我调用的是 SaveChanges,则 Clan 将不被错误地重新插入到数据库:

Debug Trace:
Before:EF6Model.RichModels.Ninja:Added
Before:EF6Model.RichModels.Clan:Added
After:EF6Model.RichModels.Ninja:Added
After:EF6Model.RichModels.Clan:Unchanged

使用此模式也能解决我之前提过的 Ninja 图表问题,其中 Ninja 可能没有经过编辑并且 equipment 未经过任何次数的更改(插入、修改或删除)。图 3 显示验证 EF 是否正确识别某个条目被修改的测试。

图 3 测试图表中子对象的状态

[TestMethod]
public void MixedStatesWithExistingParentAndVaryingChildrenisUnderstood() {
  // Arrange
    var ninja = Ninja.CreateIndependent("julie", true);
    var pNinja =new PrivateObject(ninja);
    pNinja.SetProperty("Id", 1);
    var originalOwnerId = 99;
    var equip = Create(originalOwnerId, "arrow");
  // Act
    ninja.TransferEquipmentFromAnotherNinja(equip);
    using (var context = new NinjaContext()) {
      context.Ninjas.Attach(ninja);
      var entries = context.ChangeTracker.Entries();
      OutputState(entries);
      context.FixState();
      OutputState(entries);
  // Assert 
    Assert.IsTrue(entries.Any(e => e.State == EntityState.Modified));
  }
}

测试通过,输出表明所有对象中的原始 Attach 方法被标记为“Unchanged”。在调用 FixState 后,Ninja 的状态为“Unchanged”(仍正确),但是 equipment 对象的状态被正确设置为“Modified”:

Debug Trace:
Before:EF6Model.RichModels.Ninja:Unchanged
Before:EF6Model.RichModels.NinjaEquipment:Unchanged
After:EF6Model.RichModels.Ninja:Added
After:EF6Model.RichModels.NinjaEquipment:Modified

EF Core 怎么样?

即使我移至 EF Core,我也会将此模式保存到我的工具箱。我们在简化断开连接的图表问题上已取得很大进步,大多数按照提供的一致模式操作。在 EF Core 中,使用 DbContext.Entry().State 属性设置状态将仅设置图表根状态。这在很多场景中是很具优势的。此外,名为 TrackGraph 的新方法将“跟踪图表”、命中其中的每个实体并将特定函数应用于每个方法。最常用的函数就是只设置状态的那个函数:

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

假设让该函数成为使用前面提及的 FixState 函数基于在客户端上设置的 ObjectState 应用 EF 状态的函数。

丰富的域模型简化了客户端中的控制状态

虽然我偏爱构建更丰富的根据需要更新状态的域类,但是您可以使用简单的 CRUD 类达到相同的结果,只要使用类的客户端明确设置状态即可。虽然通过手动方法,但您将不得不更密切地关注已修改的关系,确保您的帐户可用于修改外键。

近些年来,我一直使用此模式,并在书中、会议上和我的 Pluralsight 课程中与客户进行分享。并且我知道这个方法在许多软件解决方案中得到了不错的应用。不论您使用的是 EF5 还是 EF6,亦或是准备使用 EF Core,这个方法应该能够为您减轻与断开连接的数据相关的负担和痛苦。

自我跟踪实体

EF4.1 的另一个功能是 T4 模板,该模板生成“自我跟踪实体”,会将这些新释放的 POCO 变回被打压的魔兽。自我跟踪实体专为 Windows Communication Foundation (WCF) 服务向 .NET 客户端馈送数据的场景而设计。我不喜欢自我跟踪实体,当它们从 EF 安静地消失后,我感到很开心。但是,有一些开发者依靠它们。有些 API 能够为您提供这些好处。例如,Tony Sneed 构建了一个轻量级实施,名为“可跟踪的实体”,您可以在 trackableentities.github.io 找到。IdeaBlade (ideablade.com) 拥有使用其旗舰产品 DevForce(包括 EF 支持)解决断开连接数据问题的丰富经验。IdeaBlade 利用相关知识创建了免费的开源 Breeze.js 和 Breeze# 产品,这些产品也提供客户端和服务器端状态跟踪功能。我之前在本专栏中介绍过 Breeze,分别位于 2012 年 12 月刊(bit.ly/1WpN0z3) 和 2014 年 4 月刊 (bit.ly/1Ton1Kg) 中。


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 技术专家对本文的审阅: Rowan Miller