Entity Framework

ADO.NET Entity Framework 4.1 中的代码优先

Rowan Miller

ADO.NET Entity Framework 4.1 已在四月发布,包括一系列在现有 Entity Framework 4(在 Microsoft .NET Framework 4 和 Visual Studio 2010 中发布)功能之上构建的新功能。

Entity Framework 4.1 既有独立的安装程序 (msdn.microsoft.com/data/ee712906)、也有“EntityFramework”NuGet 程序包,而且在您安装 ASP.NET MVC 3.01 时也会安装它。

Entity Framework 4.1 包括两项主要的新功能:DbContext API 和代码优先。 在本文中,我将说明如何利用这两项功能来开发应用程序。 我们将快速了解代码优先的入门知识,然后深入了解一些更高级的功能。

DbContext API 是对现有 ObjectContext 类型以及 Entity Framework 早期版本中包含的很多其他类型的简化抽象。 DbContext API 外围应用针对常见任务和编码模式进行了优化。 常用功能在根级别即可获得,更高级的功能则随着您深入挖掘 API 而逐渐呈现。

代码优先是 Entity Framework 的一种新开发模式,可取代现有的数据库优先和模型优先模式。 代码优先让您使用 CLR 类定义模型,然后可以将这些类映射到现有数据库或使用这些类生成数据库架构。 其他的配置可以使用数据注释或通过 Fluent API 提供。

开始使用

代码优先已经推出一段时间了,因此我不会深入介绍入门知识。 如果您还不熟悉基本知识,可以先完成代码优先演练 (bit.ly/evXlOc)。 图 1 是完整的代码列表,可帮助您建立并运行代码优先应用程序。

图 1 代码优先入门

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System;

namespace Blogging
{
  class Program
  {
    static void Main(string[] args)
    {
      Database.SetInitializer<BlogContext>(new BlogInitializer());

      // TODO: Make this program do something!
}
  }

  public class BlogContext : DbContext
  {
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
      // TODO: Perform any fluent API configuration here!
}
  }

  public class Blog
  {
    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Abstract { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
  }

  public class RssEnabledBlog : Blog
  {
    public string RssFeed { get; set; }
  }

  public class Post
  {
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public byte[] Photo { get; set; }

    public virtual Blog Blog { get; set; }
  }

  public class BlogInitializer : DropCreateDatabaseIfModelChanges<BlogContext>
  {
    protected override void Seed(BlogContext context)
    {
      context.Blogs.Add(new RssEnabledBlog
      {
        Name = "blogs.msdn.com/data",
        RssFeed = "http://blogs.msdn.com/b/data/rss.aspx",
        Posts = new List<Post>
        {
          new Post { Title = "Introducing EF4.1" },
          new Post { Title = "Code First with EF4.1" },
        }
      });

      context.Blogs.Add(new Blog { Name = "romiller.com" });
      context.SaveChanges();
    }
  }
}

为简洁起见,我选择让代码优先生成数据库。 数据库将在我首次使用 BlogContext 保存和查询数据时创建。 本文的其余内容也适用于代码优先映射到现有数据库架构的情况。 您将看到,我使用数据库初始化表达式来断开和重建数据库,因为我们将在这篇文章中更改模型。

使用 Fluent API 进行映射

代码优先首先检查您的 CLR 类,以推断模型的状况。 然后利用一系列约定来检测主键等内容。 您可以使用数据注释或 Fluent API 覆盖或增加约定检测到的内容。 由于很多文章都介绍了如何使用 Fluent API 完成常见任务,因此我就不再赘述,而是介绍一些可以执行的高级配置。 具体来说我将重点介绍 API 的“映射”部分。 映射配置可用来映射现有的数据库架构,或用来影响生成的架构。 Fluent API 通过 DbModelBuilder 类型提供,最简便的访问方法是覆盖 DbContext 上的 OnModelCreating 方法。

实体拆分 实体拆分允许实体类型的属性分散到多个表中。 例如,我希望将寄出的照片数据拆分到独立的表中,以便可以存储到不同的文件组中。 实体拆分使用多个 Map 调用,将属性的子集映射到特定的表。 在图 2 中,我将 Photo 属性映射到“PostPhotos”表,将其余属性映射到“Posts”表。 您会注意到,我并未在属性列表中包含主键。 每个表始终都需要主键,其实我可以包含它,但代码优先将自动为我添加。

图 2 实体拆分

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Post>()
    .Map(m =>
      {
        m.Properties(p => new { p.Title, p.Content });
        m.ToTable("Posts");
      })
    .Map(m =>
      {
        m.Properties(p => new { p.Photo });
        m.ToTable("PostPhotos");
      });
}

每个层次结构一张表 (TPH) 继承 TPH 是指将继承层次结构的数据存储在一个表中,并使用鉴别器列来标识每行的类型。 如果未提供配置,代码优先默认将使用 TPH。 鉴别器列巧妙地命名为“Discriminator”,每个类型的 CLR 类型名称将用作鉴别器值。

但是,您可能希望自定义 TPH 映射的执行方式。 为此,请使用 Map 方法配置基本类型的鉴别器列值,然后使用 Map<TEntityType> 配置每个派生的类型。 我在这里使用“HasRssFeed”列来存储 true/false 值,以区分“Blog”和“RssEnabledBlog”实例:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m => m.Requires("HasRssFeed").HasValue(false))
    .Map<RssEnabledBlog>(m => m.Requires("HasRssFeed").HasValue(true));
}

在前面的示例中,我仍然使用单独的列来区分不同类型,但是我知道 RssEnabledBlog 具有 RSS 源,这是很明显的标志。 我可以重写映射,让 Entity Framework 知道它应该使用存储“Blog.RssFeed”的列来区分不同类型。 如果该列具有非空值,则它必然是 RssEnabledBlog:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map<RssEnabledBlog>(m => m.Requires(b => b.RssFeed).HasValue());
}

每个类型一张表 (TPT) 继承 TPT 将基本类型的所有属性都存储到一个表中。 派生类型的所有其他属性都将存储到独立的表中,这些独立的表具备指回基本表的外键。 TPT 映射使用 Map 调用指定基本表的名称,然后使用 Map<TEntityType> 为每个派生类型配置表。 在下面的示例中,我将所有博客通用的数据存储到“Blogs”表,将启用了 RSS 的博客的数据存储到“RssBlogs”表:

modelBuilder.Entity<Blog>()
  .Map(m => m.ToTable("Blogs"))
  .Map<RssEnabledBlog>(m => m.ToTable("RssBlogs"));

每个具体类型一张表 (TPC) 继承 TPC 将每个类型的数据存储到完全独立的表中,这些表之间不存在外键约束。 这种配置与 TPT 映射类似,区别是配置每种派生类型时,要用到“MapInheritedProperties”调用。 MapInheritedProperties 让代码优先把从基本类继承的所有属性重新映射到派生类的表中的新列:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m => m.ToTable("Blogs"))
    .Map<RssEnabledBlog>(m =>
      {
        m.MapInheritedProperties();
        m.ToTable("RssBlogs");
      });
}

按照约定,代码优先将使用标识列作为整数主键。 但是,在 TPC 中,不存在一个包含所有博客、可用于生成主键的表。 因此,代码优先将在您使用 TPC 映射时关闭标识。 如果您打算映射到已经设置好、用于在多个表中生成唯一值的现有数据库,可以通过 Fluent API 的属性配置部分重新启用标识。

混合映射 当然,您的架构未必总是正好符合我列举的模式,特别是当您映射到现有数据库时。 好消息是映射 API 是可改写的,您可以综合使用多个映射策略。 图 3 中的示例综合使用了实体拆分和 TPT 继承映射。 博客的数据拆分到“Blogs”和“BlogAbstracts”表中,启用了 RSS 的博客的数据存储到单独的“RssBlogs”表中。

图 3 综合使用实体拆分和 TPT 继承映射

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m =>
      {
        m.Properties(b => new { b.Name });
        m.ToTable("Blogs");
      })
    .Map(m =>
      {
        m.Properties(b => new { b.Abstract });
        m.ToTable("BlogAbstracts");
      })
    .Map<RssEnabledBlog>(m =>
      {
         m.ToTable("RssBlogs");
      });
}

更改跟踪器 API

配置数据库映射之后,我要花点时间来处理数据。 我将直接深入探讨一些较为复杂的情况,如果您还不熟悉基本的数据访问方式,请花些时间通读前文提到的“代码优先演练”。

单个实体的状态信息 在很多时候(例如日志记录),能够访问实体的状态信息会很有用。 这些信息包括实体状态以及修改了哪些属性等内容。 DbContext 通过“Entry”方法让您可以访问各个实体的这些信息。 图 4 中的代码片段从数据库加载一个“Blog”,修改属性,然后向控制台输出每项属性的当前值和原始值。

图 4 获取实体的状态信息

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Change the name of one blog
    var blog = db.Blogs.First();
    blog.Name = "ADO.NET Team Blog";

    // Print out original and current value for each property
    var propertyNames = db.Entry(blog).CurrentValues.PropertyNames;
    foreach (var property in propertyNames)
    {
      System.Console.WriteLine(
        "{0}\n Original Value: {1}\n Current Value: {2}", 
        property, 
        db.Entry(blog).OriginalValues[property],
        db.Entry(blog).CurrentValues[property]);
    }
  }

  Console.ReadKey();
}

运行图 4 中的代码后,控制台输出如下所示:

BlogId
 Original Value:1
 Current Value:1
 
Name
 Original Value:blogs.msdn.com/data
 Current Value:ADO.NET Team Blog
 
Abstract
 Original Value:
 Current Value:
 
RssFeed
 Original Value:http://blogs.msdn.com/b/data/rss.aspx
 Current Value:http://blogs.msdn.com/b/data/rss.aspx

多个实体的状态信息 DbContext 可让您通过“ChangeTracker.Entries”方法访问多个实体的信息。 既有给出特定类型的实体的泛型重载,也有给出所有实体的非泛型重载。 泛型参数不必是实体类型。 例如,您可以为实现特定接口的所有加载对象获取条目。 图 5 中的代码演示了将所有博客加载到内存、修改其中一个博客的属性,然后输出每个被跟踪博客的状态。

图 5 使用 DbContext 访问多个实体的信息

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load all blogs into memory
    db.Blogs.Load();

    // Change the name of one blog
    var blog = db.Blogs.First();
    blog.Name = "ADO.NET Team Blog";

    // Print out state for each blog that is in memory
    foreach (var entry in db.ChangeTracker.Entries<Blog>())
    {
      Console.WriteLine("BlogId: {0}\n State: {1}\n",
        entry.Entity.BlogId,
        entry.State);
    }
  }

运行图 5 中的代码后,控制台输出如下所示:

BlogId:1
  State:Modified
 
BlogId:2
  State:Unchanged

查询本地实例 无论何时您对 DbSet 运行 LINQ 查询,查询都将发送到数据库进行处理。 这可以保证您总是获得完整的最新结果,但是如果您知道所需的全部数据都已在内存中,就可以查询本地数据从而避免在数据库间往返。 图 6 中的代码将所有博客加载到内存中,然后对博客运行两个没有命中数据库的 LINQ 查询。

图 6 对内存中的数据运行 LINQ 查询

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load all blogs into memory
    db.Blogs.Load();

    // Query for blogs ordered by name
    var orderedBlogs = from b in db.Blogs.Local 
                       orderby b.Name
                       select b;

    Console.WriteLine("All Blogs:");
    foreach (var blog in orderedBlogs)
    {
      Console.WriteLine(" - {0}", blog.Name);
    }

    // Query for all RSS enabled blogs
    var rssBlogs = from b in db.Blogs.Local
                   where b is RssEnabledBlog
                   select b;

    Console.WriteLine("\n Rss Blog Count: {0}", rssBlogs.Count());
  }

  Console.ReadKey();
}

运行 图 6 中的代码后,控制台输出如下所示:

All Blogs:
 - blogs.msdn.com/data
 - romiller.com
 
Rss Blog Count:1

导航属性作为查询 DbContext 允许您使用代表给定实体实例的导航属性的内容的查询。 这样一来,您可以规定或过滤要加入内存的项目,避免返回不必要的数据。

例如,我有一个博客实例,希望知道其中有多少帖子。 我可以编写如图 7 中所示的代码,但这段代码要用到延迟加载才能将所有相关帖子返回内存中,以便我掌握数量。

图 7 使用延迟加载获得数据库项目的数量

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load a single blog
    var blog = db.Blogs.First();

    // Print out the number of posts
    Console.WriteLine("Blog {0} has {1} posts.",
      blog.BlogId,
      blog.Posts.Count());
  }

  Console.ReadKey();
}

我其实只需要一个整数值,而为此却要从数据库传输大量数据,占用大量内存。

幸运的是,我可以使用 DbContext 上的 Entry 方法优化我的代码,得到一个代表与该博客相关的帖子集合的查询。 因为 LINQ 可以改写,所以我可以连续使用“Count”运算符,整个查询被推入数据库,以便只返回一个整数结果(请参见图 8)。

图 8 使用 DbContext 优化查询代码并保存资源

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load a single blog
    var blog = db.Blogs.First();

    // Query for count
    var postCount = db.Entry(blog)
      .Collection(b => b.Posts)
      .Query()
      .Count();

    // Print out the number of posts
    Console.WriteLine("Blog {0} has {1} posts.",
      blog.BlogId,
      postCount);
  }

  Console.ReadKey();
}

部署注意事项

到目前为止,我介绍了如何设置和运行数据访问。 现在让我们深入一步,了解一些在完善您的应用程序并作为产品发布时应考虑的事项。

**连接字符串:**截至目前,我已经让代码优先在 localhost\SQLEXPRESS 上生成数据库。 要部署我的应用程序,我可能需要更改代码优先指向的数据库。 对此,我们建议的方法是向 App.config 文件(如果是 Web 应用程序就是 Web.config 文件)中添加一个连接字符串条目。 这也是使用代码优先映射现有数据的推荐方法。 如果连接字符串的名称匹配上下文的完全限定类型名称,代码优先会在运行时自动提取它。 但是,建议的方法是使用 DbContext 构造函数,接受使用 name=<连接字符串名称> 语法的连接名称。 这可以确保代码优先始终使用配置文件。 如果找不到连接字符串条目,将抛出异常。 以下示例显示的连接字符串可用来影响示例应用程序针对的数据库:

<connectionStrings>
  <add 
    name="Blogging" 
    providerName="System.Data.SqlClient"
    connectionString="Server=MyServer;Database=Blogging;
    Integrated Security=True;MultipleActiveResultSets=True;" />
</connectionStrings>

下面是更新后的上下文代码:

public class BlogContext : DbContext
{
  public BlogContext() 
    : base("name=Blogging")
  {}

  public DbSet<Blog> Blogs { get; set; }
  public DbSet<Post> Posts { get; set; }
}

请注意,建议您启用“多个活动结果集”。 这可以让两个查询同时进行。 例如,如果要在查询某个博客的帖子的同时枚举所有博客,这就是必需的。

数据库初始化表达式 默认情况下,如果目标数据库不存在,代码优先将自动创建数据库。 对某些人来说,即使是在部署时这也是必需的功能,生产数据库将在应用程序第一次启动时创建。 如果您有 DBA 照顾您的生产环境,DBA 很有可能会为您创建生产数据库,一旦应用程序部署完毕,如果其目标数据库不存在,应用程序将失败。 在本文中,我也已经覆盖默认的初始化表达式逻辑,并且将数据库配置为在架构发生改变时中断连接并重新创建。 这绝对不是您部署到生产环境时希望发生的。

在部署时更改或禁用初始化表达式的推荐方法是使用 App.config 文件(如果是 Web 应用程序则是 Web.config 文件)。 在 appSettings 部分,添加一个键是 DatabaseInitializerForType 的条目,后跟上下文类型名称和定义它的程序集。 值可以是“Disabled”,也可以是初始化表达式类型名称,后跟定义它的程序集。

以下示例禁用了我在本文中使用的上下文的所有初始化表达式逻辑:

<appSettings>
  <add 
    key="DatabaseInitializerForType Blogging.BlogContext, Blogging" 
    value="Disabled" />
</appSettings>

以下示例将初始化表达式改回默认功能,即仅在数据库不存在时创建它:

<appSettings>
  <add 
    key="DatabaseInitializerForType Blogging.BlogContext, Blogging" 
    value="System.Data.Entity.CreateDatabaseIfNotExists EntityFramework" />
</appSettings>

用户帐户 如果您决定让生产应用程序创建数据库,应用程序初次执行时将需要使用相应帐户,该帐户具备创建数据库和修改架构的权限。 但如果此权限保留不变,则应用程序在安全方面的潜在威胁会大大增加。 因此我强烈建议应用程序以较低的权限运行,足够执行查询和保存数据集即可。

了解更多

总而言之,在本文中,我简单介绍了代码优先开发模式和新的 DbContext API,这两者都包含在 ADO.NET Entity Framework 4.1 中。 您已经了解 Fluent API 如何用来映射现有数据库,或者影响由代码优先生成的数据库架构。 然后,我介绍了变更追踪器 API,以及它如何用来查询本地实体实例和关于这些实例的更多信息。 最后,我介绍了一些在部署使用代码优先访问数据的应用程序时应该注意的事项。

如果您要详细了解 Entity Framework 4.1 中包含的功能,请访问 msdn.com/data/ef。 您还可以使用数据开发人员中心论坛获得有关使用 Entity Framework 4.1 的帮助:bit.ly/166o1Z

Rowan Miller 是 Microsoft 的“实体框架”团队的项目经理。您可以通过他的博客 romiller.com. 了解更多有关实体框架的信息。

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