2016 年 11 月

第 31 卷,第 11 期

前沿 - Code First 和数据库初始化

作者 Dino Esposito | 2016 年 11 月

Dino Esposito尽管“DevOps”是相对较新的术语,最近该术语已日益丰富发展起来,包含更多的活动—其中最值得注意的是自动测试和部署—我相信自动开发人员操作的第一个示例与软件本身一样已经过时了。我指的是在应用程序安装期间创建和初始化数据库的特定功能。许多软件公司开发垂直系统,然后将其出售给各种客户并适应他们的需求。

可自定义的部分取决于产品和业务域的特征,但是我敢说,任何垂直软件应用程序至少都需要具有客户特定的数据库。因此,必须使用上下文所需的表和架构创建数据库,并使用临时数据对其填充。

并非所有必需的任务都可以自动完成,并内置到产品本身。例如,假设导入现有数据。无论要导入的数据是驻留在 Excel 文件中,还是驻留在旧数据库中,可能都必须以某种方式构建导入程序以便处理数据并将其加载到新存储中。但是,如果在应用程序的数据访问层部署了 Entity Framework 6.x Code First,则至少可以轻松地进行数据库架构和表的自动化创建,并可在应用程序第一次运行时以透明方式执行该操作。

在本专栏中,我将从多客户应用程序的角度出发,总结 Code First 自早期以来提供的一些功能。我将特别重点介绍如何创建和填充数据库,以及如何以编程方式定义其名称和连接字符串。 

奠定表架构的基础

假设有一个全新的 Visual Studio 项目,该项目已链接到 Entity Framework 6.x NuGet 包。想要执行的下一步步骤可能是创建数据访问类库或在当前项目中创建至少一个不同的文件夹,以保存在某种程度上与数据访问功能的工作原理一致的所有文件。根据 Entity Framework 规则,需要具有一个 DbContext 类,该类表示应用程序的数据管理子系统中的入口点。以下是这种应用程序特定类的一个示例:

public class RegionsContext : DbContext
{
  ...
}

对应用程序来说,DbContext 派生类只不过是数据库。该类预计会公开一些 DbSet<T> 属性,一个属性对应于由应用程序管理的实体的每个集合。DbSet<T> 属性与物理数据库中的表是逻辑相关的:

public class RegionsContext : DbContext
{
  public DbSet<Region> Regions { get; set; }
}

此处,代码片段的最终结果是让应用程序使用包含名为 Regions 的表的关系数据库。表的架构是如何决定的? 架构是由 Region 类的公共布局决定的。Code First 提供一系列属性和流利语法,以定义基础表的类属性和列之间的映射。使用同一方法,还可以定义索引、主键、标识列、默认值以及其他可在基础表的列和表中配置的内容。以下是 Region 类的最小版本:

public class Region
{
  public Region()
  {
    Languages = "EN";
  }
  [Key]
  public string RegionCode { get; set; }
  public string RegionName { get; set; }
  public string Languages { get; set; }
  ...
}

即,Regions 表中的所有记录都将有三个 nvarchar­(MAX) 列(RegionCode、RegionName 和 Languages),而 RegionCode 将被设置为主键列。此外,会将任何新建 Region 类的实例的语言属性值设置为“EN”。这是确保每次通过代码添加新记录时,该列的默认值始终是“EN”的方法。但是需要注意的一点是,在 Code-First 解决方案的构造函数中设置值(如此处完成的操作)不会在基础数据库配置中自动添加任何默认值绑定。

命名数据库

在 Code-First 解决方案中,到数据库的所有连接都会通过 DbContext 派生类,这在正确打开和关闭连接的同时仍将连接作为属性公开,以使代码完全控制打开和关闭操作。连接字符串的详细信息是什么,更重要的是,如何以参数化的方法提供详细信息?

创建 DbContext 派生类时,必须提供构造函数。以下是一个很常见的示例:

public RegionsContext(string conn) : base(conn)
{
  ...
}

DbContext 类具有接受连接字符串作为参数的构造函数,因此,最简单的操作就是通过派生类的构造函数反映基础构造函数的功能。DbContext 类智能化地包含一些逻辑以处理传递的字符串。假设以 name=XXX 的形式传递的任何字符串均表示可在应用程序的配置文件(即 Web 项目的 web.config)的 connectionstrings 部分内的 XXX 条目中找到实际连接字符串。否则,假设传递的任何字符串为要创建的数据库的名称。在这种情况下,连接字符串的更多详细信息(如凭据和服务器位置)应位于配置文件中的 entityFramework 部分的 defaultConnectionFactory 块中。请注意,每次向 Visual Studio 项目添加 Entity Framework 包时,都会默认修改配置文件,以支持 entityFramework 部分。图 1 显示相关列表(为清楚起见稍作修正)。

图 1 修改示例 Web.config 文件以支持 Entity Framework Code First

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="entityFramework"
      type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,..." />
  </configSections>
    <startup>
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
  <connectionStrings>
    <!-- Your explicit connection strings -->
  </connectionStrings>
  <entityFramework>
    <defaultConnectionFactory type=
      "System.Data.Entity.Infrastructure.SqlConnectionFactory,
      EntityFramework">
    <parameters>
    <parameter value="Data Source=(local); Integrated Security=True;" />
    </parameters>
  </defaultConnectionFactory>
  <providers>
    <provider invariantName="System.Data.SqlClient"
      type="System.Data.Entity.SqlServer.SqlProviderServices, ..." />
    </providers>
  </entityFramework>
</configuration>

所能查找到的有关 Code First 的大部分示例都基于固定连接字符串,该字符串从配置文件引用或显式传递到上下文类。最终结果是,Code First 使用应用程序首次运行时所提供的连接字符串创建数据库。让我们对这一方面进行更深入的研究。

DbContext 类支持四种初始化策略,如图 2 中所列。

图 2 Code-First 数据库初始化策略

策略 说明
CreateDatabaseIfNotExists

检查数据库是否存在,如果找不到数据库,则创建一个数据库。如果数据库存在,但架构不兼容,则引发异常。

注意: 这是默认初始化表达式。

DropCreateDatabaseIfModelChanges 如果数据库不存在,则创建一个数据库。如果数据库存在,但架构不兼容,则会删除现有的数据库,并创建新数据库。
DropCreateDatabaseAlways 在每次运行应用程序时删除现有的数据库,并重新创建数据库。
自定义初始化表达式

自定义所编写的初始化表达式类,以使其按照需要的行为执行操作,任何其他选项均不能提供这种行为。

注意: 必须使用此选项向数据库添加某些母版内容。

根据默认行为 CreateDatabaseIfNotExists,每次创建上下文类后,该行为将检查引用数据库是否存在以及是否可访问。如果该数据库不存在且不可访问,则创建数据库。如果数据库存在且可访问,但其架构与实体类的公共布局不兼容,则会引发异常。若要删除异常,必须编辑实体类,或者更可行的操作是通过数据库编程接口或 Entity Framework 迁移脚本来编辑数据库的架构。

我发现,这个方法对于达到生产级别的应用程序来说是理想的选择。但在开发阶段,我更青睐 DropCreateDatabaseIfModelChanges 方法,因为该方法从本质上使你免受数据库维护的琐事的烦扰: 只需适当地调整实体类,下次当你在 Visual Studio 中按 F5 后,Entity Framework 即会修复数据库。为了激活所选的初始化策略,请向自定义 DbContext 类的构造函数添加以下行:

Database.SetInitializer<YourDbContext>(
  new DropCreateDatabaseIfModelChanges<YourDbContext>());

请注意,也可以在配置文件中设置数据库初始化表达式,如果计划在生产和开发过程中使用不同策略,这可能是一个理想的选择。

总体而言,借助 Code First 可编写这样一个应用程序:可以在第一次运行应用程序时自动创建其所有的数据库表。换言之,只需复制文件和二进制文件,然后启动它。不过,此种行为在针对单个客户构建的系统中最为适用。在具有多客户系统时,最佳做法是使用安装实用工具。

采用稍有不同的方法的原因之一是,你可能想要采用其他名称命名数据库;例如,你可能想要在名称前添加客户特定的前缀。图 3 介绍此命令行实用工具的框架。程序使用来自命令行的客户前缀,恰当设置数据库名称的格式,然后触发 DbContext 派生类,该操作将重新创建数据库并使用合适的初始数据对其进行填充。

图 3 客户特定的数据库名称

class Program
{
  static void Main(string[] args)
  {
    var prefix = args[0];
                // boundary checks skipped
    var dbName = String.Format("yourdb_{0}", prefix);
    using (var db = new YourDbContext(dbName))
    {
      // Fill the database in some way
    }
  }
}

数据库的初始填充

任何旨在满足同一业务域中的多客户需求的系统必须具有多个表,专门用来存储各个客户不同的选项和首选项。此信息必须在软件安装时提供。实际上,数据库初始加载的一部分由所有安装共享,而另一部分是客户特定的。依赖于客户数据的部分通常从外部资源导入,并需要临时例程(某类脚本或已编译的代码)。根据上下文,甚至可以考虑使用某些依赖项注入机制来对初始化数据库的安装实用工具内的导入程序结构实施通用化。但就静态数据库内容而言,Code First 提供临时服务。

自定义初始化表达式

若要在初始化过程中向数据库填充数据,需要创建自定义数据库初始化表达式,如图 2 中所示。自定义初始化表达式是从某个预定义初始化表达式(如 DropCreateDatabaseIfModel­Changes)继承的类。对该类唯一严格的要求是覆盖 Seed 方法:

public class YourDbInitializer : DropCreateDatabaseAlways<YourDbContext>
{
  protected override void Seed(YourDbContext context)
  {              
    ...
  }
}

在 Seed 方法的实施过程中,使用提供的 DbContext 执行填充数据库表的任何代码,以对其进行访问。这就是所需的全部操作。

如果计划安装多客户应用程序,定义自定义初始化表达式是一个好办法,因为它提供单个点,以专注于以每个客户为基础来定制数据库的初始形式。初始化表达式是纯 C# 类,因此可使用依赖项注入工具提供支持,以将其连接到特定逻辑部分,以便从数据所在的位置导入数据。

最后但同样重要的是,可完全禁用数据库初始化表达式,因此,数据库的安装仍然是一个完全不同的操作—甚至可能由不同的 IT 或 DevOps 团队管理。若要指导 Code-First 框架忽略任何初始化表达式,需要遵循自定义 DbContex 类的构造函数中的代码:

Database.SetInitializer<YourDbContext>(null);

例如在向现有系统发布更新时,这是一个安全的方法。禁用初始化表达式以确保在任何情况下都不会丢失现有的数据。

总结

最后,借助 Code First,编写多租户和多客户应用程序的操作并不比为已知配置和客户端专门编写应用程序的操作更困难。只需对连接字符串的分配和初始化过程具有一定的了解。在 Entity Framework Core 中,即使最终工作方式的细节有所不同,其核心原则仍保持不变。特别是,新的 DbContext 类包括 OnConfiguring 覆盖方法,通过该方法可将上下文连接到所选的数据库提供程序,并向其传递凭据等任何信息。


Dino Esposito是《Microsoft .NET: 构建面向企业的应用程序》(Microsoft Press,2014 年)和《使用 ASP.NET 构建新型 Web 应用程序》(Microsoft Press,2016 年)的作者。作为 JetBrains 的 .NET 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents@wordpress.com 上以及 Twitter @despos 上的推文中分享他对于软件的愿景。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Andrea Saltarello (rea.saltarello@manageddesigns.it)
Andrea Saltarello 来自意大利米兰,是一名企业家和软件架构师,他还喜欢为实际项目编写代码并获取有关设计决策的反馈。作为培训师和演讲者,他在欧洲有一些课程和会议的演讲安排,例如 TechEd Europe、DevWeek 和 Software Architect。他自 2003 年以来一直是 Microsoft MVP,并在近期被任命为 Microsoft 区域主管。他热爱音乐,钟情于 Depeche Mode,自从第一次听到该乐队的歌曲“Everything Counts”时,就爱上了这个乐队。