Entity Framework (EF) 的特性可以在数据库迁移方面提供极大的帮助,尤其是它的小规模、独立可执行的、对 PR 友好的增量更新。理论上,它甚至可以在部署出现问题时提供回滚迁移的能力。如果使用得当,这项技术可以成为巨大的助力。
然而,在实践中,自动化执行这些增量更新的方法有很多种,每种方法都有其优缺点,对于一个项目适用的方法可能对另一个项目就不适用。
本文将展示六种运行 EF 数据库迁移的方法,解释在何种情况下每种方法最有帮助,然后展示如何在构建服务器上设置 Active Directory (AD) 认证以及如何正确设置连接字符串以运行迁移。
何时运行迁移是一个有趣的问题。实际上,使用 DbContext.Database.MigrateAsync() 命令在应用程序首次启动时运行迁移是非常容易的。
在启动时运行迁移既方便又节省时间,因为它可以利用现有的数据库连接和防火墙规则。但它也有一些缺点:
最后一点需要解释。它引用了以下安全最佳实践:
为了最小化安全事件造成的损害,系统应该被授予所需的最低访问权限级别。
换句话说,不要授予系统任何它不需要的权限。例如,作为日常操作的一部分,应用程序通常不需要删除数据库表,因此,它们不应该被授予该权限。然而,这正是应用程序运行数据库迁移时需要的权限类型。因此,通过让应用程序运行数据库迁移,无意中授予了攻击者在他们通过应用程序访问数据库时造成更多混乱的能力。
具体来说:如果应用程序连接到 SQL Server,那么授予应用程序运行账户 db_datareader 和 db_datawriter 权限是可以的,但不应该授予 db_ddladmin,更不应该授予 db_owner。这样做会在安全中心审计中产生一个标志。
如果还在开发阶段,有意推迟安全风险,或者对风险感到舒适,那么通过应用程序迁移可能已经足够好了。然而,当准备好时,另一种选择是让构建服务器运行 EF 迁移。有几种方法可以实现这一点,每种方法都有其优缺点。
作为开发者,可能已经熟悉 dotnet ef database update 命令。这个选项在 DevOps 世界中不方便,因为它需要源代码。源代码有很多问题,最不重要的是需要获取尝试运行的部署的确切版本的代码。下面的方法通常更可取。
如果构建服务器运行命令 dotnet ef migrations script [oldmigration] [newmigration],它将生成一个单一文件,可以将其保存为构建管道中的工件,然后在每个环境中执行。与其他一些选项不同,这种方法产生的资产可以由 DBA 审核。然而,确定 oldmigration 和 newmigration 的值会很棘手。更糟糕的是,如果每个环境都在不同的版本,就像不是每个 PR 都部署到生产环境时经常发生的那样,那么可能无法生成一个适用于所有环境的单一工件,因为这些值对于每个环境都是不同的。因此,通常不建议这种方法。
幸运的是,如果向 dotnet ef migrations script 传递 --idempotent 参数,它将生成一个脚本,只运行需要运行的迁移。生成的文件看起来像一堆 if 语句,如下所示:
SQL
BEGIN
CREATE
INDEX
[IX_AbpAuditLogActions_AuditLogId] _
ON
[AbpAuditLogActions] ([AuditLogId]);
END
;
GO
虽然这个选项不允许回滚不良部署,但它非常适合作为发布构建工件,可以由 DBA 审核,并且在每个环境处于不同版本时工作。然而,这种方法不是事务性的,所以失败的迁移可能会使数据库处于不一致的状态。
捆绑包很棒。它们解决了将事务应用于迁移的问题。简单地让构建服务器运行 dotnet ef migrations bundle --self-contained -r [linux-x64|win-x64],将得到一个单一文件的二进制文件(默认称为 efbundle.exe),可以将其作为工件发布,它在事务中运行迁移,并且只运行需要应用的迁移。甚至可以指定特定的迁移,这允许回滚迁移。生成的文件如下所示:
Entity Framework Core Migrations Bundle 7.0.1
Usage: efbundle [arguments] [options] [[--] ...]]
Arguments:
The target migration. If '0', all migrations will be reverted.
Defaults to the last migration.
Options:
--connection The connection string to the database.
Defaults to the one specified in AddDbContext or OnConfiguring.
--version Show version information
-h|--help Show help information
-v|--verbose Show verbose output.
--no-color Don't colorize output.
--prefix-output Prefix output with level.
这个选项几乎是最喜欢的。它唯一缺少的是运行迁移之外的自定义代码的能力。
曾经,一个项目在数据库的一个字段中存储加密数据。当然,在生产中之后意识到加密算法太强,导致性能问题。需要降低加密强度,这意味着需要执行一个复杂的数据迁移,涉及用 C# 编写的方法。
必须运行一个查询来读取每一行,用旧算法在 C# 中解密它,用新算法在 C# 中重新加密它,然后将其更新回数据库。对于 EF 迁移来说应该很容易,对吧?
令人惊讶的是,EF 迁移不能做那种工作。它们被设计为运行 insert 或 update SQL 语句或 create 或 drop DDL 语句,而不是检索数据。自己检查一下:看看能否在 MigrationBuilder 上找到一个检索数据的方法。
因此,由于算法位于 C# 方法中,而 EF 迁移无法访问它,不得不在 EF 迁移之外运行代码。
幸运的是,因为使用的是 ASP.NET Boilerplate(ABP Framework 的前身),它包含了一个运行迁移的命令行应用程序。这提供了在迁移之前或之后运行代码的灵活性,从而解决了问题。
从构建服务器运行 DbContext.Database.MigrateAsync() 的命令行应用程序(或带有命令行选项的主应用程序)可以在事务中运行,当使用 --self-contained -r [linux-x64|win-x64] 编译时几乎是单一文件(必须包括 Microsoft.Data.SqlClient.SNI.dll)。缺点是它们不允许回滚迁移。然而,它们是最喜欢的,因为它们在面对艰难迁移时提供了最大的灵活性。此外,它们在每个租户一个数据库的多租户场景中特别有效。
如果这种方法的细节引起兴趣,请查看创建的 DbMigrator 示例项目,它在多阶段构建管道的一个阶段中编译和发布命令行应用程序,然后在另一个阶段执行它。
是时候承认房间里的大象了。如果不在应用程序启动时运行迁移,是如何从构建服务器获取数据库连接的?另外,假设遵循最佳实践,只使用 AD 认证在 Azure 上使用 SQL Server,如何在无头环境中使用 Active Directory 帐户进行认证?
一般来说,有四个步骤。首先,创建一个应用程序注册。其次,将应用程序注册添加到数据库中:
CREATE USER [{userName}] FROM EXTERNAL PROVIDER;
并授予它运行 DDL 的权限:
EXEC sp_addrolemember 'ddl_admin', '{userName}'
第三,为应用程序注册创建一个秘密:
最后,添加一个防火墙规则,允许自定义构建代理访问数据库,或者如果使用的是托管代理,那么 "Allow Azure services and resources to access this server",在 Bicep 中看起来像这样:
resource firewallRule_AzureIps
'
Microsoft.Sql/servers/firewallRules@2021-11-01'
= {
name:
'
AllowAzureIps'
parent: sql
properties: {
startIpAddress:
'
0.0.0.0'
endIpAddress:
'
0.0.0.0'
}
}
最后,应该能够使用这样的连接字符串:
"Server=tcp:{sqlServerName}.database.windows.net,1433; "Database={dbName}; Encrypt=True; User Id={servicePrincipalAppId}; Password={servicePrinicpalSecret}; Authentication='Active Directory Service Principal';"
就这样。插入应用程序注册应用程序 ID 和生成的秘密,就可以开始了。