数据点

Entity Framework 预览:代码优先,ObjectSet 和 DbContext

Julie Lerman

Entity Framework (EF) 4 仍处于测试阶段,但开发团队已开始通过另一种方式使用它了。我们具备用于创建模型的数据库优先方式(将数据库反向工程为实体数据模型)和新的 EF 4 模型优先功能(定义实体数据模型,然后从中创建数据库)。此外,团队决定创建某种称为“代码优先”的功能。

顾名思义,这将从代码开始 — 既不是数据库也不是模型。事实上,使用代码优先功能,则无需可视模型和描述该模型的 XML。您只需为您的应用程序域创建类,代码优先将会使其能够参与到 EF 中。您可以使用上下文,编写并执行 LINQ to Entities 查询,以及利用 EF 更改跟踪功能。但无需在设计器中处理模型。如果不是要构建高度结构化的应用程序,您可能几乎已经忘记了一直是 EF 在为您处理数据库交互和更改跟踪。

域驱动开发 (DDD) 迷们似乎比较喜欢代码优先。事实上,正是许多 DDD 行家帮助 EF 团队了解了代码优先对于 DDD 程序员的意义。请查看 Danny Simmons 有关数据可编程性咨询委员会的博客文章,了解此方面的更多信息 (blogs.msdn.com/b/dsimmons/archive/2008/06/03/dp-advisory-council.aspx)。

但是,代码优先目前还无法包含在 Microsoft .NET Framework 4 和 Visual Studio 2010 中。实际上,其仍处在发展阶段,Microsoft 为开发人员提供了对此部分的访问,以供他们使用并通过 Entity Framework Feature CTP (EF Feature CTP) 提供反馈。此社区技术预览 (CTP4) 的第四个小版本发布于 2010 年七月中旬。此小版本中有着重大更改。

除了在 CTP4 中包含代码优先程序集外,Microsoft 还对下一小版本的 EF 提出了一些很不错的新创意。这些创意也包括在 CTP4 中,以便用户可以对其进行体验并提供反馈。更为重要的是,代码优先可以利用其中的一些新功能。

您可以通过 EF 团队 (tinyurl.com/297xm3j)、Scott Guthrie (tinyurl.com/29r3qkb) 和 Scott Hanselman (tinyurl.com/253dl2g) 的博客文章了解有关此 CTP 的一些详细演练信息。

在本专栏中,我将重点介绍 CTP4 中的一些独特功能和改进,这些功能和改进大幅简化了使用 EF(通常和 .NET Framework 一起)进行的开发,从而引起了一场有关 EF 的极大轰动。

核心 API 的改进

在 CTP4 添加到 EF 运行时的改进中,值得一提的是两个主力类(ObjectContext 和 ObjectSet)的轻型版本。ObjectContext 支持查询、更改跟踪和保存回数据库。ObjectSet 用于封装类似对象集。

新类 DbContext 和 DbSet 具有相同的基本功能,但不会让您接触到 ObjectContext 和 ObjectSet 的整个功能集。并不是每一个编码方案都需要访问更强大类中的所有功能。DbContext 和 DbSet 并非派生自 ObjectContext 和 ObjectSet,但 DbContext 通过一个属性简化了对功能完备版本的访问:DbContext.ObjectContext。

ObjectSet 基础知识与简化的 DbSet

ObjectSet 是一组实体类型的类似泛型集合的表示。例如,您可以将一个 ObjectSet<Customer> 称为 Customers,这是上下文类的一个属性。LINQ to Entities 查询是依据 ObjectSet 编写的。以下是一个依据 Customers ObjectSet 编写的查询:

from customer in context.Customers 
where customer.LastName=="Lerman" 
select customer

ObjectSet 具有内部构造函数,但没有无参数构造函数。ObjectSet 是通过 ObjectContext.CreateObjectSet 方法创建的。在典型的上下文类中,Customers 属性将定义为:

public ObjectSet< Customer> Customers {
  get { 
    return _ customers??
(_customers = 
      CreateObjectSet< Customer >(" Customers"));
  }
}

private ObjectSet< Customer > _ customers;

现在,您可以依据 Customers 编写查询,并操作该集,根据需要添加、附加和删除对象。

DbSet 的使用更加简单。 DbSet 不可公开构造(类似于 ObjectSet),但它将自动创建 DbSet 并将其分配至您在派生的 DbContext 上声明的任何属性(具有公共 setter)。

ObjectSet 集合方法中引入了 EF 术语 — AddObject、Attach 和 DeleteObject:

context.Customers.AddObject(myCust)

DbSet 只使用 Add、Attach 和 Remove,与其他集合方法名称更为一致,因此,您不必花费时间来解决“如何在 EF 中表述”这样的问题,如:

context.Customers.Add(MyCust)

使用 Remove 方法而非 DeleteObject 也更能清晰地描述该方法正在执行的操作。该方法将从集合中删除某项。DeleteObject 建议从数据库中删除数据,这会给很多开发人员造成混淆。

迄今为止,DbSet 让我喜欢的原因之一是派生类型的使用更为简单。ObjectSet 包括基本类型和从其继承的所有类型。如果您具有基本实体 Contact 和从其派生的 Customer 实体,则每次要处理客户事宜时,都必须从 Contacts ObjectSet 开始。例如,要查询客户,您需要编写 context.Contacts.OfType<Customer>。这不仅会造成混乱,而且还不清晰。DbSet 可以包含派生类型,因此,您现在可以创建一个属性 Customers,以返回 DbSet<Customer> 并支持与之直接交互而不是通过 Contacts 集。

DbContext — 简化的 Context

DbContext 公开了 ObjectContext 最常用的功能,并提供了一些确实有用的附加功能。到目前为止,我所喜欢的 DbContext 的两个功能是泛型 Set 属性和 OnModelCreating 方法。

我目前所指的所有那些 ObjectSet 都是 ObjectContext 实例的显式属性。比方说您有一个称为 PatientHistory 的模型,其中含有三个实体:Patient、Address 和 OfficeVisit。您将有一个继承 ObjectContext 的类 PatientHistoryEntities。此类包含一个 Patients 属性(即 ObjectSet<Patient>)、一个 Addresses 属性以及一个 OfficeVisits 属性。如果希望使用泛型编写动态代码,您必须调用 context.CreateObjectSet<T>,其中 T 是您的实体类型之一。这依旧不是很清晰。

DbContext 的 Set 方法更加简单,通过该方法,您只需调用将会返回 DbSet<T> 的 context.Set<T> 即可。看起来好像只是少了 12 个字母,但对于我的编码大脑来说,使用此属性感觉棒极了,而调用工厂方法则不尽然。您还可以将派生实体与此属性配合使用。

另一个 DbContext 成员是 OnModelCreating,它在代码优先中很有用。您希望应用到域模型的任何附加配置都可在 EF 基于域类构建内存中元数据之前,在 OnModelCreating 中进行定义。这是以前版本的代码优先从未有过的重大改进。您将在后文中看到关于此改进的更多信息。

代码优先更智能、更方便

2009 年 6 月,代码优先首先作为 EF Feature CTP1 的一部分呈现给开发人员,命名为“仅代码”。这种使用 EF 的变体背后的基本前提是开发人员只希望定义其域类,而不用为物理模型费心。然而,EF 运行时依赖该模型的 XML 来根据模型将查询强制为数据库查询,然后将查询结果从数据库强制回由模型描述的对象。如果没有元数据,则 EF 无法完成此作业。但元数据不必位于物理文件中。EF 在应用程序进程期间读取这些 XML 文件一次,基于该 XML 创建强类型化元数据对象,然后完成所有与内存中 XML 之间的交互。

代码优先也将创建内存中元数据对象。但是,不是通过读取 XML 文件来创建,而是从域类推断元数据(请参阅图 1)。它使用约定实现此目的,然后提供可供您添加附加配置以进一步细化模型的方法。

图 1 代码优先在运行时构建实体数据模型元数据

代码优先的另一个重要作业是使用元数据创建数据库架构,甚至创建数据库本身。代码优先自其最早的公共版本发布以来就一直提供这些功能。

在以下示例中,您需要用一个配置来替代一些无效的假设。我有一个称为 ConferenceTrack 的类,它具有一个称为 TrackId 的标识属性。代码优先约定将查找“Id”或类名 +“Id”(作为要用于实体的 EntityKey 的标识属性和数据库表的主键)。但 TrackId 不适合此模式,因此我必须告诉 EF 这是我的标识键。

新的代码优先 ModelBuilder 类基于之前描述的类构建内存中模型。您可以使用 ModelBuilder 进一步定义配置。我能够通过以下 ModelBuilder 配置指定 ConferenceTrack 实体应使用其 TrackId 属性作为其键:

modelBuilder.Entity<ConferenceTrack>().HasKey(
  ct => ct.TrackId);

现在,ModelBuilder 将在创建内存中模型并构建数据库架构时,将此附加信息考虑在内。

配置应用更具逻辑性

在这方面,DbContext.OnModelCreating 非常有用。您可以将配置置于此方法中,以便在 DbContext 创建模型时应用这些配置:

protected override void OnModelCreating(
  ModelBuilder modelBuilder) { 
  modelBuilder.Entity<ConferenceTrack>().HasKey(
    ct => ct.TrackId); 
}

CTP4 中新增的另一个功能是另一种通过类中的属性应用配置的方法。 这项技术称为数据批注。 通过将 Key 批注直接应用到 ConferenceTrack 类中的 TrackId 属性,可以得到相同的配置:

[Key] 
public int TrackId { get; set; }

确实更加简单,但是,我个人喜好使用编程配置,这样,类不必在其中具有任何 EF 特定的内容。

使用此方法还意味着 DbContext 将负责缓存模型,因此进一步构造 DbContext 不会再次带来模型发现成本。

关系更加简单

代码优先中最值得一提的改进之一是,通过类进行假设更加智能。 对这些约定的改进有很多,但我发现,关系约定的增强对我的代码影响最大。

尽管关系是在类中进行定义的,但在之前的 CTP 中,必须提供配置信息以在模型中定义这些关系。 此外,这些配置既乱又无逻辑性。 现在,代码优先可以正确解释类中定义的多数关系的用途。 在需要通过一些配置调整模型时,语法更加简单。

我的域类通过属性定义了很多关系。 例如,ConferenceTrack 具有此种一对多的关系:

public ICollection<Session> Sessions { get; set; }

Session 具有相反的关系以及多对多关系:

public ConferenceTrack ConferenceTrack { get; set; }
public ICollection<Speaker> Speakers { get; set; }

使用我的类和单一配置(将 TrackId 定义为会议的密钥),模型生成器创建了具有所有关系和继承的数据库,如图 2 所示。

图 2 建模创建的数据库结构

请注意,Sessions_Speakers 表是针对多对多关系创建的。我的域具有一个从 Session 继承的 Workshop 类。默认情况下,代码优先假定“每个层次结构一张表”继承,因此它在 Sessions 表中创建了 Discriminator 列。我可以使用配置将其名称更改为 IsWorkshop,甚至指定它应创建一个“每个类型一张表”继承。

这些新功能的事先计划

这些可以在 CTP4 中抢先使用的引人注目的新功能可能会使那些表示“我现在就想要!”的开发人员倍感沮丧。这仅仅是个预览版,目前还无法运用到生产实践中,当然,如果能够向 EF 团队提供反馈以帮助其进行完善,您将有机会试用这些新功能。您可以事先计划如何在您即将推出的应用程序中使用代码优先以及其他新的 EF 功能。

Julie Lerman* 是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。Lerman 是《Programming Entity Framework》(O'Reilly Media,2009)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。请通过 Twitter.com 关注她:julielerman。*

衷心感谢以下技术专家对本文的审阅:Rowan Miller