数据点

Entity Framework 6 中 Code First 的好处

Julie Lerman

下载代码示例

在我 2013 年 12 月的文章“Entity Framework 6:专家版本”(msdn.microsoft.com/magazine/dn532202) 中,我介绍了 Entity Framework 6 (EF6) 中的许多新功能。不过,我没能深入介绍各个功能,因此,我将在本月的文章中深入探讨 EF6 特定于 Code First 的部分增强功能。在我要讨论的功能中,有两个功能与 Code First 映射有关,其他功能与 Code First 迁移相关。

一次加载许多个 Fluent API 映射

可通过两种方法来指定模型的 Fluent 映射(从类到数据库)。一种方法是直接在 DbContext 类的 OnModel­Creating 方法中进行映射,如下所示:

 

modelBuilder.Entity<Casino>()
  .Property(c=>c.Name).IsRequired().HasMaxLength(200);
modelBuilder.Entity<PokerTable>()
  .Property(c => c.SerialNo).HasColumnName("SerialNumber");

如果有很多映射,可按类型将它们组织到各个 EntityTypeConfiguration 类中,再使用代码将这些类添加到模型生成器中,如下所示:

modelBuilder.Configurations.Add(new CasinoConfiguration());
modelBuilder.Configurations.Add(new PokerTableConfiguration());

但是,如果对于许多实体有大量映射,则 OnModelCreating 中会有许多重复的 modelBuilder.Configurations.Add 方法。 为了从这种枯燥的工作中解放出来,您现在可以只用一个方法从给定程序集加载所有 EntityTypeConfiguration。 这里,我使用新的 AddFromAssembly 方法来加载在正在运行的应用程序的执行程序集中指定的配置:

modelBuilder.Configurations
  .AddFromAssembly(Assembly.GetExecutingAssembly())

这种方法的一个出色功能是,它不受将要加载的配置的范围限制。 甚至可以将自定义 EntityTypeConfiguration 类标记为私有,该方法会找到这些类。 此外,AddFromAssembly 也理解 EntityTypeConfiguration 中的继承层次结构。

AddFromAssembly 是来自 Unai Zorrilla 的众多社区贡献之一。 请参阅 Zorrilla 的博客文章“EF6:自动设置配置”( bit.ly/16OBuJ5) 了解详细信息,您可以在那里向他留言致谢。

定义自己的默认架构

在我 2013 年 3 月的数据点专栏文章“EF6 Alpha 初探”(msdn.microsoft.com/magazine/jj991973) 中,对 Code First 的架构支持做了一些探讨。 一个新功能是可以在 OnModelCreating 中配置的映射:HasDefaultSchema。 这样,您可以指定上下文映射到的所有表的数据库架构,而不是使用 dbo 的 EF 默认设置。 在文章“Entity Framework 6:专家版本”中,我在有关 DbTransactions 的讨论中执行了某种原始 SQL:

("Update Casino.Casinos set rating= " + (int) Casino.Rating)

您可能已注意到,我在 SQL 中指定了 Casino 架构。 我将架构命名为 Casino 的原因是,我在 DbContext (CasinoSlotsModel) 的 OnModelCreating 方法中指定了它:

modelBuilder.HasDefaultSchema("Casino");

我还谈到 EF6 中对 Code First 迁移的新支持(这些迁移针对具有不同架构的数据库运行)。 由于这种情况自 EF6 Alpha 以来未发生改变,请您阅读我以前的专栏文章加以了解。

用于从任意点重新生成数据库的迁移脚本

规范中(以及对规范进行重申的每篇文章中)列出的 Code First 迁移的新功能之一是“幂等迁移脚本”。您现在可能已经拥有计算机科学学位,或者您可能是一名工商管理学博士。 我既不想也不是必须要查出“幂等”的含义。根据 Wikipedia 引用一位 IBM 工程师的说法 (bit.ly/9MIrRK):“在计算机科学中,‘幂等’这一术语用于 … 描述一个在执行一次或多次之后产生相同结果的操作”。我还得查一查它怎样发音。 它的发音为 eye-dem-poe-tent。

在数据库领域中,幂等可用来描述无论数据库的状态如何,始终对数据库具有同样影响的 SQL 脚本。 对于 Code First 迁移,在运行迁移之前,这种脚本将检查该迁移是否已在运行。 此功能是 Update-Database 的 -script 参数所特有的。

通过 EF,始终能够创建将在从特定起点(源)到明确的端点(目标)的所有迁移步骤中运行的脚本,可自行选择是否以明确的端点结束。 此 NuGet 命令会引起该行为:

Update-Database -Script
  –SourceMigration:NameOfStartMigration
  –TargetMigration:NameOfEndMigrationThatIsntLatest

新的特点是,在您用特定方式调用该命令时,所生成的脚本现在要智能得多:

Update-Database -Script -SourceMigration $InitialDatabase

如果您用 0 替代 $InitialDatabase,也会是这种情况,但是不能保证将来的版本支持这种替代方法。

作为响应,该脚本先进行初始迁移,直至最新迁移完成。 这就是该语法不显式提供目标或源迁移的名称的原因。

但在使用这一特定命令时,EF6 向脚本添加逻辑,检查在针对特定迁移执行 SQL 之前已应用哪些迁移。 下面是将在脚本中看到的代码示例:

    IF @CurrentMigration < '201310311821192_AddedSomeNewPropertyToCasino'
    BEGIN
      ALTER TABLE [Casino].[Casinos] ADD [AgainSomeNewProperty] [nvarchar](4000)
      INSERT [Casino].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
      VALUES (N'201310311821192_AddedSomeNewPropertyToCasino',
        N'CasinoModel.Migrations.Configuration', HugeBinaryValue , 
        N'6.1.0-alpha1-21011')
    END

该代码检查 ­_MigrationHistory 表以查看 AddedSomeNewPropertyToCasino 脚本是否已针对数据库运行。如果已运行,则不执行该迁移的 SQL。在 EF6 之前,脚本只是运行 SQL,而不检查它是否已运行。

提供程序友好的迁移历史记录表

通过 EF6,您可以使用一个称为“可自定义迁移历史记录表”的功能来自定义 _MigrationHistory 表的定义方式。如果要使用要求与默认要求不同的第三方数据提供程序,则此功能十分重要。图 1 是该表的默认架构。

下面是该表如何使用的示例。在 CodePlex 上,一名开发人员注意到,因为两个 PK 中的每个 char 都可能大于 1 字节,所以会出现从 MigrationId 和 ContextKey 创建的复合键的长度超出 MySQL 表的允许键长度的错误:767 字节 (bit.ly/18rw1BX)。为了解决此问题,MySQL 团队在开发 ADO.NET 的 EF6 版 MySQL Connector 时,在内部使用 HistoryContext 来修改两个键列的长度 (bit.ly/7uYw2a)。

请注意,在图 1 中,__MigrationHistory 表获得了我使用 HasSchema 为上下文定义的架构:Casino。您可能已约定非数据表应由具有有限权限的架构使用,因此,您可能需要更改该表的架构名称。您可以使用 HistoryContext 执行此操作,例如,用于指定使用“admin”架构。

Default Schema for the __MigrationHistory Table图 1 __MigrationHistory 表的默认架构

HistoryContext 派生自 DbContext,因此,如果您过去使用过 DbContext 和 Code First,就应该对该代码有一定了解。图 2 是我定义来指定 admin 架构的 HistoryContext 类。

图 2 自定义 HistoryContext 以重新定义 Redefine __MigrationHistory 表

 

public class CustomHistoryContext : HistoryContext
{
  public CustomHistoryContext
   (DbConnection dbConnection, string defaultSchema)
     : base(dbConnection, defaultSchema)
  {
  }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<HistoryRow>()
      .ToTable("__MigrationHistory", "admin");
  }
}

您还可以使用熟悉的 API 调用,如 Property().HasColumnType、HasMaxLength 或 HasColumnName。 例如,如果您需要更改 ContextKey 的长度,可以执行以下操作:

modelBuilder.Entity<HistoryRow>()
  .Property(h => h.ContextKey).HasMaxLength(255);

如果您已阅读上月的文章,就应该熟悉 EF6 DbConfiguration。 您可以使用它让模型知道 CustomHistory­Context 文件。 在您的自定义 DbConfiguration 的构造函数中,需要指定要使用的 HistoryContext。 这里,我将 SQL Server 提供程序的上下文设置为使用 CustomHistoryContext:

SetHistoryContext(
  SqlProviderServices.ProviderInvariantName,
     (connection, defaultSchema) =>
  new CustomHistoryContext(connection,
     defaultSchema));

数据库初始化和迁移功能会识别这一附加的上下文并相应构造 SQL。 图 3 中的表是使用自定义 HistoryContext 创建的,用于将 __MigrationHistory 表的架构名称更改为 admin。 (我没有将用于更改列长度的示例代码包括进来。)

Customized __MigrationHistory Table
图 3 自定义 __MigrationHistory 表

HistoryContext 是一个强大的功能,但使用时需要小心。 但愿您使用的数据库提供程序已使用它指定与目标数据库相关的 __MigrationHistory 表,您甚至无需考虑这一点。 否则,我建议查看有关此功能的 MSDN 文档,注意阅读其指南 (bit.ly/16eK2pD)。

创建自定义迁移操作

如果您以前使用过迁移(并非自动进行,而是通过从程序包管理器控制台窗口显式创建并执行迁移),那么您可能已研究过通过 add-migration 创建的迁移文件。 如果是这样,您可能已发现,Code First 迁移有一个强类型 API,用于描述对数据库架构进行的每项更改:System.Data.Entity.Migrations.DbMigration。

图 4 显示了设置多个属性的 Create­Table 方法的示例。

图 4 DbMigrations.CreateTable 方法

CreateTable(
  Casino.SlotMachines",
  c => new
    {
      Id = c.Int(nullable: false, identity: true),
      SlotMachineType = c.Int(nullable: false),
      SerialNumber = c.String(maxLength: 4000),
      HotelId = c.Int(nullable: false),
      DateInService = c.DateTime(nullable: false),
      HasQuietMode = c.Boolean(nullable: false),
      LastMaintenance = c.DateTime(nullable: false),
      Casino_Id = c.Int(),
    })
  .PrimaryKey(t => t.Id)
  .ForeignKey("Casino.Casinos", t => t.Casino_Id)
  .Index(t => t.Casino_Id);

提供程序随后将这些 API 调用转换为数据库特定 SQL。

有一些方法可用于创建表和索引、创建或更改属性、删除对象等。 从图 5 所列的可能性可以看出,这是一种功能相当丰富的 API,包括能够直接执行某种 SQL。 但在某些情况下,它的功能又不够丰富,不足以满足您的需要。 例如,没有方法可用于创建数据库视图、指定权限或执行许多其他操作。

DbMigrations Database Schema Operations
图 5 DbMigrations 数据库架构操作

同样,社区帮助解决了此问题。 在 EF6 中,您现在能够创建自定义迁移操作,可通过自定义由 add-migration 生成的迁移类来调用这些操作。 这归功于 CodePlex 社区的另一位开发人员 Iñaki Elcoro(也称 iceclow)。

若要创建自己的操作,您必须执行几个步骤。 我将介绍每个步骤的核心操作。 在本文的下载内容中,您可以查看完整的代码以及步骤的组织方式。

  • 定义操作。 这里,我定义了一个 CreateView­Operation,如图 6 中所列。
  • 创建指向该操作的扩展方法。 这会使从 DbMigration 进行的调用变得简单:
public static void CreateView(this DbMigration migration,
  string viewName, string viewqueryString)
{
  ((IDbMigration) migration)
    .AddOperation(new CreateViewOperation(viewName,
       viewqueryString));
}
  • 为自定义 SqlServerMigrationSqlGenerator 类的 Generate 方法中的操作定义 SQL,如图 7 所示。
  • 让 DbConfiguration class 类使用自定义 SqlServerMigrationSqlGenerator 类:
SetMigrationSqlGenerator("System.Data.SqlClient",
  () => new CustomSqlServerMigrationSqlGenerator());

图 6 用于创建数据库视图的自定义迁移操作

public class CreateViewOperation : MigrationOperation
{
  public CreateViewOperation(string viewName, string viewQueryString)
    : base(null)
  {
    ViewName = viewName;
    ViewString = viewQueryString;
  }
  public string ViewName { get; private set; }
  public string ViewString { get; private set; }
  public override bool IsDestructiveChange
  {
    get { return false; }
  }
}

图 7 自定义 SqlServerMigrationSqlGenerator 类

public class CustomSqlServerMigrationSqlGenerator
  : SqlServerMigrationSqlGenerator
{
  protected override void Generate(MigrationOperation migrationOperation)
  {
    var operation = migrationOperation as CreateViewOperation;
    if (operation != null)
    {
      using (IndentedTextWriter writer = Writer())
      {
        writer.WriteLine("CREATE VIEW {0} AS {1} ; ",
                          operation.ViewName,
                          operation.ViewString);
        Statement(writer);
      }
    }
  }
}

一切就绪之后,现在可以在迁移文件中使用新的操作,Update-Database 知道如何使用该操作。 图 8 显示了使用的 CreateView 操作,并提供了还需要创建用于删除该视图的操作的提醒,如果需要展开此迁移,可通过 Down 方法调用该操作。

图 8 使用新的 CreateView 操作

public partial class AddView : DbMigration
  {
    public override void Up()
    {
      this.CreateView("dbo.CasinosWithOver100SlotMachines",
                      @"SELECT  *
                        FROM    Casino.Casinos
                        WHERE  Id IN  (SELECT   CasinoId AS Id
                        FROM     Casino.SlotMachines
                        GROUP BY CasinoId
                        HAVING COUNT(CasinoId)>=100)");
    }
    public override void Down()
    {
      this.RemoveView("dbo.CasinosWithOver100SlotMachines");
    }
  }

在调用 Update-Database 之后,可以在图 9 中我的数据库里看到新视图。

The Newly Created View Generated by Update-Database
图 9 由 Update-Database 生成的新创建视图

Code First 继续发展

有了 Code First 的这些核心功能,Microsoft 和社区开发人员抓住机会,对 EF6 进行了一些改进,增加了一些更加灵活的新功能。 这种发展不会随 EF6 的发布而停止。 如果您对 6.0.1 以上版本 ( bit.ly/1dA0LZf) 的 CodePlex 工作项进行筛选,会发现 EF6 和 Code First 的将来版本将增加更多改进。 这些项处于各种状态。 也许您会找到一个喜欢使用的工作项。

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

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