数据点

适用于多个模型的 EF6 Code First 迁移

Julie Lerman

下载代码示例

Julie LermanEntity Framework 6 引入了对 Code First 迁移的支持以更好地处理在单个数据库中存储多个模型的数据。但这种支持非常具体,可能并不如您所想的那样。在本文中,您将了解此功能可以与不可以执行的任务,以及如何使用它。

不同的模型、不同的表、不同的数据:相同的数据库

EF6 迁移支持多个相互之间完全独立的模型的迁移。此功能的两个实施(使用一个密钥标识一组迁移或通过单个模型的表使用数据库架构分组迁移历史记录)允许您在同一个数据库中存储独立的、不同的模型。

不适用于跨模型共享数据,也不适用于多租户数据库

这很容易让人误解此功能的好处,因此有必要在此澄清下此功能不支持的方面。

这一新的多模型支持并不是为了复制单个模型以及跨多个架构来获得多租户数据库。

当我们看到此功能时,我们当中的很多人希望的是另一个模式,即能够通过其在多个模型之间共享一个公共实体(及其数据)并将实体映射到单个数据库。然而,这是一个完全不同的问题,并且使用实体框架并不能轻松解决。我曾经尝试过,但是放弃了。我之前在此专栏中就跨数据库共享数据撰写过一些文章(参见“跨域驱动设计界定的上下文共享数据的模式,第 2 部分”bit.ly/1817XNT)。我还在 TechEd 欧洲盛会上主讲过名为“域驱动设计界定的上下文中的实体框架模型分区”的讲座,您可通过 bit.ly/1AI6xPa 下载相关记录。

模式一:ContextKey 至关重要

新工具 EF6 提供的支持此功能的新工具之一是 ContextKey。这是数据库中跟踪每个迁移的 MigrationHistory 表中的新字段。其与 DbMigrations­Configuration<TContext> 类中名称相同的新属性进行合作。

默认情况下,ContextKey 将继承与该上下文关联的 DbMigrationsConfiguration 的强类型名称。例如,下面是使用 Doctor 和 Episode 类型的 DbContext 类:

namespace ModelOne.Context
{
  public class ModelOneContext:DbContext
  {
    public DbSet<Doctor> Doctors { get; set; }
    public DbSet<Episode> Episodes { get; set; }
  }
}

一如往常,enable-migrations 命令的默认行为是创建一个具有 [YourNamespace].Migrations.Configuration 名称的 DbMigrationsConfiguration 类。

我在应用特定迁移时(即,当我在 Visual Studio 程序包管理器控制台中调用 Update-­Database 时),实体框架不但会应用该迁移,而且还会向 __MigrationHistory 表添加新行。下面是进行该操作的 SQL:

INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model],
  [ProductVersion])
VALUES (N'201501131737236_InitialModelOne',
  N'ModelOne.Context.Migrations.Configuration',
  [hash of the model], N'6.1.2-31219')

请注意,ContextKey 字段所用的值是 Model­One.Context.Migrations.Configuration,这是我的 DbMigrationsConfiguration <TContext> 类的强类型名称。

您可以通过在类构造函数中指定 DbMigrationsConfiguration 类的 Context­Key 属性来控制 ContextKey 名称。我会将其重命名为 ModelOne:

public Configuration()
{
  AutomaticMigrationsEnabled = false;
  ContextKey = "ModelOne";
}

现在,执行迁移会将 ModelOne 用于迁移表的 ContextKey 字段。但是,如果已经采用默认设置执行了迁移,则事情不会太顺利。EF 将尝试重新应用所有迁移,包括那些创建了表和其他数据库对象的迁移,从而导致数据库由于重复对象而引发错误。因此,我的建议是,在应用任何迁移前更改该值,否则您需要手动更新 __MigrationsHistory 表中的数据。

我已确保我的 DbContext 类型指向我已命名为 MultipleModelDb 的连接字符串。我想要一个可用于面向此数据库的任何模型的单个连接字符串,而不是依赖于 Code First 约定来查找与上下文同名的连接字符串。我通过指定上下文构造函数继承 DbContext 重载来实现此目的,这将使用连接字符串名称。下面是 ModelOneContext 的构造函数:

public ModelOneContext()
       : base("MultipleModelDb") {
}

add-migration 和 update-database 都将能够找到该连接字符串,因此我可保证迁移正确的数据库。

两个上下文,两个 ContextKey

现在,您了解了 ContextKey 的工作原理,让我们来添加另一个具有其自己 ContextKey 的模型。我将此模型放在一个单独的项目中。在将多个模型放于同一项目中时,执行此任务的模式略有不同;我将在本文稍后部分进一步说明。下面是我的新模型 ModelTwo:

namespace ModelTwo.Context
{
  public class ModelTwoContext:DbContext
  {
    public DbSet<BBCEmployee> BbcEmployees { get; set; }
    public DbSet<HiringHistory> HiringHistories { get; set; }
  }
}

ModelTwoContext 使用了完全不同的域类。下面是 DbConfiguration 类,我在其中指定将 ContextKey 称为 ModelTwo:

internal sealed class Configuration : DbMigrationsConfiguration<ModelTwoContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;
      ContextKey = "ModelTwo";
    }

当针对包含 ModelTwoContext 的项目调用 update-database 时,同一数据库中将创建一个新表并向 __MigrationHistory 表添加一个新行。此时,ContextKey 值是 ModelTwo,正如您在迁移运行的 SQL 代码段中所见:

INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
VALUES (N'201501132001186_InitialModelTwo', N'ModelTwo', [hash of the model], N'6.1.2-31219')

随着我的域、我的 DbContext 和我的数据库的不断演变,EF 迁移将始终使用相应的 ContextKey 检查 __MigrationHistory 表中相关的一组已执行迁移。通过这种方式,将能够根据对模型的更改确定要对数据库执行的更改。这使得 EF 可正确管理数据库中存储的多个 DbContext 模型的迁移。但请记住,此方法凑效是因为两个模型映射到的数据库表不存在重叠。

模式二:数据库架构分离模型和迁移

另一种您可以用于针对单个数据库中多个模型执行迁移的模式是通过数据库架构分离迁移和相关的表。这种模式尽可能让您只针对单独的数据库,而无需承担面对多个数据库时可能导致的一些资源开销(例如维护和支出)。

EF6 通过使用新的 DbModelBuilder.HasSchema 映射进行配置,使定义单个模型的数据库架构更加轻松。这将替代默认的架构名称,对于 SQL Server 而言,即 dbo。

请记住,即使未指定上下文键,也将使用默认名称。因此,删除上下文键属性没有意义,我将在此处演示说明 HasSchema 属性是如何影响迁移的。

我将为 OnModelCreating 方法中的两个上下文类分别设置架构。下面是 ModelTwoContext 的相关代码,我已为其指定了名为 ModelTwo 的架构:

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
    modelBuilder.HasDefaultSchema("ModelTwo");
  }

另一个上下文将获得 ModelOne 架构名。

结果是 Model­TwoContext 映射到的所有数据库对象都将位于 ModelTwo 架构中。此外,EF 还会将此模型的 __MigrationHistory 表放在 ModelTwo 架构中。

为了以简洁的方式进行演示,我将指向与之前示例不同的数据库,并应用所有迁移。请记住,设置 HasDefaultSchema 方法是映射更改,并要求您添加新的迁移以将该更改应用到数据库。图 1 显示了其单独架构中的迁移和数据表。

分组到数据库架构中的迁移和表
图 1 分组到数据库架构中的迁移和表

接下来,每当您与任一上下文的迁移进行交互时,因为它们各自归入自己的单个架构,因此 EF 单独对其进行维护不会有任何问题。提醒一下,请注意此处的关键模式,其中映射到两个模型的表没有重叠。

单个项目中的多个模型

到目前为止,您所见的两个示例(使用 ContextKey 或数据库架构分离模型迁移)都是设置为使用封装在其自己的项目中的各个模型。这是我构建自己的解决方案时喜欢使用的方法。但是,将模型置于同一个项目中也是可能的,而且在许多情况下,也是完全合乎情理的。无论您是使用 ContextKey 还是数据库架构来将迁移分门别类,您都可以通过向 NuGet 命令添加几个额外参数来实现此目的。

为了与这些示例清楚地分开,我将使用相同的类创建一个新的解决方案。我会尽可能将这些域类置于不同的项目中,但两个模型均在同一项目中,如图 2 所示。

将多个 DbContext 类置于单个项目中
图 2 将多个 DbContext 类置于单个项目中

如您所知,默认情况下,enable-­migrations 将在您的解决方案中为发现的 DbContext 创建一个名 Migrations 的文件夹。如果您像我现在这样有多个 Dbcontext,enable-migrations 将不只是随机选择 DbContext 以创建迁移;而是将返回一条非常有帮助的消息,指示您使用 ContextTypeName 参数来明示要使用的 DbContext。该消息非常有用,使您只需从其中执行复制和粘贴操作即可运行必要的命令。下面是对我的项目返回的消息:

PM> enable-migrations
More than one context type was found in the assembly 'ModelOne.Context'.
To enable migrations for 'ModelOne.Context.ModelOneContext', use
 Enable-Migrations -ContextTypeName ModelOne.Context.ModelOneContext.
To enable migrations for 'ModelTwo.Context.ModelTwoContext', use
 Enable-Migrations -ContextTypeName ModelTwo.Context.ModelTwoContext.

除了 –ContextTypeName 参数之外,我还将添加 MigrationsDirctory 参数以显式命名文件夹,从而使我更易于管理项目资产:

Enable-Migrations
-ContextTypeName ModelOne.Context.ModelOneContext
-MigrationsDirectory ModelOneMigrations

图 3 显示了新的文件夹及其针对各个迁移创建的 Configuration 类。

指定启用迁移时 DbContext 和目录名的结果
图 3 指定启用迁移时 DbContext 和目录名的结果

运行 Enable-Migrations 还会将代码添加到 DbConfiguration 类中,以使它们知晓目录名称。下面是 ModelOneContext 的配置类示例(ModelTwoContext 的 DbConfiguraton 文件按指定将其目录名称设置为 ModelTwoMigrations):

internal sealed class Configuration : DbMigrationsConfiguration<ModelOne.Context.ModelOneContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;
      MigrationsDirectory = @"ModelOneMigrations";
    }

因为我现在具有两个名为 Configuration 的类,所以无论何时我想使用它们,我都将不得不对其进行完全限定。因此,我将第一个重命名为 ModelOneDbConfig(如下面的代码所示),将第二个重命名为 ModelTwoDbConfig:

internal sealed class ModelOneDbConfig : DbMigrationsConfiguration<ModelOneContext>
  {
    public ModelOneDbConfig()
      {
        AutomaticMigrationsEnabled = false;
        MigrationsDirectory = @"ModelOneMigrations";
      }
  }

如果您想要覆盖默认值(但我将保留不动),还可以指定 ContextKey。请记住,我在我的 DbContext 类中指定了 HasDefaultSchema 映射方法,因此迁移历史记录表和其他数据库对象将可以位于其自己的架构中。

现在,可以为这两个模型添加迁移并将其应用于我的数据库了。同样,我需要向 EF 说明要使用的模型和迁移。通过指向迁移配置文件,EF 会知道要使用的模型以及存储迁移文件的目录。

下面是我为 ContextOne 添加迁移的命令(请记住,我已更改了配置类名称,因此不必针对 ConfigurationTypeName 使用其完全限定名称):

add-migration Initial
  -ConfigurationTypeName ModelOneDbConfig

生成的迁移文件在 ModelOne­Migrations 目录中进行创建。在为 ModelTwo 执行了相同的操作后,我在 ModelTwoMigrations 目录中还有一个迁移文件。

现在,可以应用这些迁移了。我将需要再次指定 ConfigurationTypeName 以便 EF 知晓要使用的迁移。下面是用于 ModelOne 的命令:

update-database
  -ConfigurationTypeName ModelOneDbConfig

然后,我将为 ModelTwo 运行相关的命令:

update-database
  -ConfigurationTypeName ModelTwoDbConfig

运行这些命令后,我的数据库看起来与图 1 所示相同。

因为我修改了我的模型并添加和应用了迁移,所以我只需记住要在每个命令中指定正确的配置类作为参数即可。

可与域驱动设计建模完美配合

在最近名为“跨域驱动设计界定的上下文共享数据的模式”的两部分数据点专栏中,我探讨了在持久保存到单独数据库的域之间共享数据的相关内容。第一部分位于 bit.ly/1wolxz2,第二部分位于 bit.ly/1817XNT。很多开发人员已指出,本地维护单独数据库可能是一种负担,而在云中托管单独数据库的花费可能很昂贵。本文向您介绍了在单个数据库中托管多个模型的表和数据的技术,这些技术可帮助您模拟完整的数据库分离。EF6 迁移中的此项新支持为这些开发人员提供了一个很好的解决方案。


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

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