使用 FluentMigrator 和 MSBuild





5.00/5 (9投票s)
使用 MSBuild 进行数据库迁移/升级,并提供备份和还原功能
引言
对于我所有与数据库相关的项目,我希望能够以某种方式使我的数据库模式与我的代码保持同步,并普遍维护数据库。在过去的 4-5 年和 10-15 个项目中,我一直使用一个自定义编写的工具(名称很有创意 - dbtool),它松散地基于 Ash Tewari 的 DbUpdater。
对于新项目,我想尝试一种新方法。我选择使用 FluentMigrator,而不是执行 SQL 增量更改脚本。现在,由于我所有的同事和我自己都习惯了 dbtool 的功能,我需要使用 FluentMigrator 重新实现一些东西。这是所需的功能
- 升级数据库 - 迁移到数据库的最新可用版本
- 备份数据库 - 将数据库备份到 zip 文件
- 还原数据库 - 从 zip 文件还原数据库
所有功能都应该通过 MSBuild 脚本实现,使其易于在 TeamCity、CruiseControl.NET、Hudson 等构建环境中使用。
免责声明: 这不会是一篇关于 FluentMigrator 或 MSBuild 的全面文章。有关这方面的信息,请查看文章末尾的链接,或进行 Google 搜索。本文组织成我在实现所述功能时遇到的一系列问题及其解决方案。
那么,我们开始吧...
准备解决方案
在运行迁移之前,我们需要有一个包含实际迁移代码的项目。您可以使用文章顶部的链接下载源代码,或在 GitHub 上浏览它。它包含一个空的 MVC Web 应用程序、迁移项目、MSBuild 脚本和一些支持工具。
迁移项目非常简单。它包含一个带有 [Migration(0)]
属性的 Baseline
类和一个带有 [Migration(1)]
属性的 Version001
类。正如我之前提到的,我不会在这里解释 FluentMigrator API。有关这方面的信息,请查看 官方文档。
FluentMigrator 和 FluentMigrator.Tools NuGet 包已安装到解决方案中。
您还需要一个名为 FluentMigration
的空数据库在您的 SQL Server Express 实例中,或者修改 src/Migration.Web/web.config
中的 Database
连接字符串,如果您想使用另一个数据库名称或另一个数据库实例。
在 \src
文件夹中有两个 MSBuild 脚本。Migration-initial.msbuild
是构建脚本的初始(几乎)空版本,而 Migration.msbuild
是完成的版本。
一旦我们的解决方案准备就绪,就可以开始处理这些功能了。
功能 #1 - 升级数据库(运行迁移)
FluentMigrator 有各种迁移运行程序工具,用于命令行、MSBuild 和 Nant。我们将使用它的 MSBuild 任务,名为 <Migrate>
。我们 MSBuild 脚本的第一个版本如下所示
<?xml version="1.0"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="Migrate" ToolsVersion="4.0">
<PropertyGroup>
<MigratorTasksDirectory>
$(MSBuildProjectDirectory)\packages\FluentMigrator.Tools.1.0.2.0\tools\AnyCPU\40\
</MigratorTasksDirectory>
<ConnectionString>
Data Source=.\SQLEXPRESS;Initial Catalog=FluentMigration;Integrated Security=True;
</ConnectionString>
</PropertyGroup>
<UsingTask
TaskName="FluentMigrator.MSBuild.Migrate"
AssemblyFile="$(MigratorTasksDirectory)FluentMigrator.MSBuild.dll" />
<Target Name="Migrate">
<Message Text="Starting FluentMigrator migration" />
<!-- Important: Target must be your Migrations assembly name, not your dll file name -->
<Migrate Database="SqlServer2008"
Connection="$(ConnectionString)"
Target="Migration" />
</Target>
</Project>
很好,我们现在拥有开始所需的一切,对吗?也许吧,但我看到了潜在的问题。
问题 #1
由于我们正在使用 NuGet(我选择使用 NuGet 安装 FluentMigrator 和 FluentMigrator.Tools),我们在脚本中硬编码了 FluentMigrator 的当前版本(上面的脚本中为 1.0.2.0)。当我们更新 FluentMigrator 到新版本时,我们还必须更新 MSBuild 文件以指向正确的文件夹。
我不喜欢这样,所以我选择采用老式方法,将 FluentMigrator.Tools 放在 root\tools
下的一个单独子文件夹中。现在让我们修改脚本以指向正确的文件夹
<MigratorTasksDirectory>
$(MSBuildProjectDirectory)\..\tools\FluentMigrator\
</MigratorTasksDirectory>
问题 #2
连接字符串是硬编码的。每当我们更改应用程序连接字符串时,都需要记得在 MSBuild 脚本中也进行更改。如果我们能直接从 web.config
读取当前的应用程序连接字符串并使用它,那就太好了。是的,我们可以做到。MSBuild 4 中有一个 <XmlPeek>
任务。另一种选择是 MSBuild Community Tasks 库 的 <XmlQuery>
任务。
我们来添加这个,并且稍微整理一下脚本
<?xml version="1.0"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="Migrate" ToolsVersion="4.0">
<PropertyGroup>
<MigratorTasksDirectory>
$(MSBuildProjectDirectory)\packages\FluentMigrator.Tools.1.0.2.0\tools\AnyCPU\40\
</MigratorTasksDirectory>
<MainProjectDirectory>
$(MSBuildProjectDirectory)\..\Migration.Web\
</MainProjectDirectory>
</PropertyGroup>
<UsingTask
TaskName="FluentMigrator.MSBuild.Migrate"
AssemblyFile="$(MigratorTasksDirectory)FluentMigrator.MSBuild.dll" />
<Target Name="Common">
<XmlPeek XmlInputPath="$(MainProjectDirectory)\web.config"
Query="/configuration/connectionStrings/add[@name='Database']/@connectionString"
Condition="'$(ConnectionString)' == ''">
<Output TaskParameter="Result" PropertyName="ConnectionString" />
</XmlPeek>
</Target>
<Target Name="Migrate" DependsOnTargets="Common">
<Message Text="Starting FluentMigrator migration" />
<!-- Important: Target must be your Migrations assembly name, not your dll file name -->
<Migrate Database="SqlServer2008"
Connection="$(ConnectionString)"
Target="MyMigration" />
</Target>
</Project>
由于我们的目标(Migrate
、Backup
、Restore
)之间可能会共享更多通用内容,因此添加了一个 Common
目标。ConnectionString
属性已从 PropertyGroup
中移除,并在 Common
目标中从 web.config
读取。请注意,Migrate
目标现在依赖于 Common
目标。
好的,这看起来好多了,但仍有改进空间。
问题 #3
连接字符串的团队管理。如果一名团队成员需要使用不同的数据库名称怎么办?如果一些团队成员使用 SQL Server Express Edition,而其他团队成员使用 SQL Server Standard 或 Developer 版本,该怎么办?请注意,构建服务器也可以被视为一个团队成员。在所有这些情况下,连接字符串都会不同,因此每当工作副本更新时,团队成员都需要检查连接字符串并在必要时进行修改。
为了避免这些问题,我的团队使用了独立的 ConnectionStrings.config
文件,这些文件不在源代码控制之外,因此每个团队成员都有自己的 ConnectionString.config
版本。然后,该文件将像这样从 web.config
或 app.config
引用
<connectionStrings configSource="ConnectionStrings.config" />
这对我们来说效果相当不错,但最近我偶然发现了另一个 解决方案。Ayende 的概念很简单。您有一个默认连接字符串和特定于计算机的覆盖。
<add name="Database" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=FluentMigration;Integrated Security=True;"/>
<add name="Database-MiroslavI5" connectionString="Data Source=.;Initial Catalog=FluentMigration;Integrated Security=True;"/>
<add name="Database-MiroslavLaptop" connectionString="Data Source=.;Initial Catalog=FluentMigration;Integrated Security=True;"/>
应用程序将首先查找特定于计算机的连接字符串,如果找不到特定于计算机的键,则回退到默认值。这对于拥有几台不同计算机的小团队来说是可以的。对于较大的团队来说,这些特定于计算机的连接字符串很快就会累积起来。在这种情况下,我建议您使用一个独立的 ConnectionStrings.config
文件,因为它更容易维护。
由于我们在 web.config
中使用了这种特定于计算机的技巧,因此 MSBuild 脚本也需要足够智能,能够确定特定于计算机的连接字符串或默认连接字符串。
<!-- Locate machine specific connection string first -->
<XmlPeek XmlInputPath="$(MainProjectDirectory)\web.config"
Query="/configuration/connectionStrings/add[@name='Database-$(ComputerName)']/@connectionString"
Condition="'$(ConnectionString)' == ''">
<Output TaskParameter="Result" PropertyName="ConnectionString" />
</XmlPeek>
<!-- If machine specific connection string doesn't exist, fallback to default connection string -->
<XmlPeek XmlInputPath="$(MainProjectDirectory)\web.config"
Query="/configuration/connectionStrings/add[@name='Database']/@connectionString"
Condition="'$(ConnectionString)' == ''">
<Output TaskParameter="Result" PropertyName="ConnectionString" />
</XmlPeek>
如您所见,我们首先尝试使用 $(ComputerName)
MSBuild 属性来查找特定于计算机的版本,如果找不到,则使用默认版本。Condition="'$(ConnectionString)' == ''"
用于检查连接字符串是否已找到。结果将保存到 ConnectionString
属性。
就是这样。现在我们的 MSBuild 脚本看起来更健壮了。但如果我们现在尝试运行它,我们会收到一个错误,因为 Migrate
任务找不到我们的迁移项目 dll。
问题 #4
因此,我们需要将迁移项目 dll 复制到 FluentMigrator 工具文件夹。我们可以通过几种方式来实现...
- 我们可以直接使用 MSBuild 的
<Copy>
任务将 dll 复制到 FluentMigrator 文档建议的位置,但在这种情况下,我们还需要硬编码其源位置。源可以是\bin\debug
或\bin\release
,具体取决于当前的构建配置。 - 使用项目的后置生成事件将输出复制到某个
root\build
文件夹,然后我们的 MSBuild 脚本中可以有一个<Copy>
任务?是的,这会奏效。 - 相反,我直接使用了迁移项目的后置生成事件将迁移项目 dll 复制到 FluentMigrator 工具文件夹,如下所示
copy "$(TargetDir)Migration.dll" "$(SolutionDir)..\tools\FluentMigrator\"
在那之后,我们的 MSBuild 脚本运行并将迁移代码应用于数据库(前提是您至少有一个与连接字符串中指定的名称相同的空数据库)。
msbuild.exe /target:Migrate Migration.msbuild
功能 #2 - 数据库备份
现在我们的迁移过程已经准备就绪并正在运行,让我们继续数据库备份功能。
问题 #5
MSBuild 中没有 SQL 运行任务。幸运的是,有许多开源项目提供了额外的任务。我们将使用 MSBuild Community Tasks。
<!-- This goes to our PropertyGroup section -->
<!-- Fixing some problems with referencing community tasks -->
<MSBuildCommunityTasksPath>$(MSBuildProjectDirectory)\..\tools\MSBuild Community Tasks\Build\</MSBuildCommunityTasksPath>
<MSBuildCommunityTasksLib>$(MSBuildCommunityTasksPath)MSBuild.Community.Tasks.dll</MSBuildCommunityTasksLib>
<BackupFileDirectory>$(MSBuildProjectDirectory)\..\backup\</BackupFileDirectory>
<!-- /PropertyGrop -->
<!-- Backup target -->
<Target Name="Backup" DependsOnTargets="Common">
<Message Text="Backing up the database to temp folder" />
<MSBuild.Community.Tasks.SqlExecute
ConnectionString="$(ConnectionString)"
Command="BACKUP DATABASE [Migration] TO DISK = N'$(BackupFileDirectory)\Migration.bak' WITH NOFORMAT, INIT, NAME = N'Migration - Full Backup - $(BuildDate)', SKIP, NOREWIND, NOUNLOAD, STATS = 10" />
</Target>
在上面的代码中,我们为 MSBuild Community Tasks 修复了一些属性(因为它在我们的 \tools
文件夹中而不是默认位置),并且还在我们的 PropertyGroup
中添加了一个新属性 BackupFileDirectory
。但是数据库名称在脚本中是硬编码的。而且我希望有一个基于当前时间的唯一备份文件名。
问题 #6
要从连接字符串中提取数据库名称,我们需要某种正则表达式。同样,MSBuild 默认不支持正则表达式,但可以使用 属性函数 来实现,这是 MSBuild 4.0 的一项新功能。
<!-- In Common target -->
<!-- Strip database name from connection string -->
<CreateProperty Value="$([System.Text.RegularExpressions.Regex]::Match($(ConnectionString), `Initial Catalog=([^;])*`))">
<Output TaskParameter="Value" PropertyName="DatabaseName" />
</CreateProperty>
<CreateProperty Value="$(DatabaseName.Replace('Initial Catalog=', ''))">
<Output TaskParameter="Value" PropertyName="DatabaseName" />
</CreateProperty>
这部分新代码通过从 ConnectionString
属性中提取数据库名称来创建一个 DatabaseName
属性。我无法使用纯正则表达式函数获取纯数据库名称,因此有一个第二个 <CreateProperty>
任务,它只是从 DatabaseName
中删除 Initial Catalog=
字符串。
好了,现在我们获取时间戳并将其添加到混合中。
<!-- Prepare backup file name and path from current time -->
<MSBuild.Community.Tasks.Time Format="yyyy-MM-dd-HH-mm-ss">
<Output TaskParameter="FormattedTime" PropertyName="BuildDate" />
</MSBuild.Community.Tasks.Time>
<CreateProperty Value="$(BackupFileDirectory)$(DatabaseName)-$(BuildDate).bak">
<Output TaskParameter="Value" PropertyName="BackupFilePath" />
</CreateProperty>
再次,我们使用 MSBuild Community Tasks 中的一个名为 <Time>
的任务,并将格式化的时间放入 BuildDate
属性。BackupFilePath
是一个新属性,它包含新备份文件的完整路径。
我们的 <SqlExecute>
任务现在如下所示
<MSBuild.Community.Tasks.SqlExecute
ConnectionString="$(ConnectionString)"
Command="BACKUP DATABASE [$(DatabaseName)] TO DISK = N'$(BackupFilePath)' WITH NOFORMAT, INIT, NAME = N'$(DatabaseName) - Full Backup - $(BuildDate)', SKIP, NOREWIND, NOUNLOAD, STATS = 10" />
现在我们可以尝试进行备份
msbuild.exe /target:Backup Migration.msbuild
然后... 它失败了。
问题 #7
我们正在尝试备份我们正在连接的同一个数据库。SQL Server 拒绝这样做。我们应该连接到 master
表。为此,我们需要修改连接字符串。这里有一些更多的正则表达式技巧。
<CreateProperty Value="$([System.Text.RegularExpressions.Regex]::Replace($(ConnectionString), `Initial Catalog=([^;])*`, `Initial Catalog=master`))">
<Output TaskParameter="Value" PropertyName="MasterConnectionString" />
</CreateProperty>
现在我们为 <SqlExecute>
任务使用新的 $(MasterConnectionString)
属性。
<MSBuild.Community.Tasks.SqlExecute
ConnectionString="$(MasterConnectionString)"
Command="BACKUP DATABASE [$(DatabaseName)] TO DISK = N'$(BackupFilePath)' WITH NOFORMAT, INIT, NAME = N'$(DatabaseName) - Full Backup - $(BuildDate)', SKIP, NOREWIND, NOUNLOAD, STATS = 10" />
如果现在运行它,它可能会成功... 但也可能失败
问题 #8
根据您存放项目文件的地方,备份可能会成功也可能失败。请记住,我们将 BackupFileDirectory
放在项目根目录的子文件夹 \backup
下?如果您将项目放在您的用户文件夹下(例如 C:\Users\Miroslav
),那么 SQL Server 没有权限在那里创建文件。SQL Server 服务通常不是以当前用户帐户运行的,因此无法访问用户文件夹。
好的,所以我们不能直接备份到用户文件夹,但我们可以创建一个备份到其他地方,然后简单地将其复制到我们的备份目标文件夹。我们现在就来做这件事
<BackupFileDirectory>$(ALLUSERSPROFILE)\CodeProject\Migration\</BackupFileDirectory>
<BackupOutputDirectory>$(MSBuildProjectDirectory)\..\backup\</BackupOutputDirectory>
上面的代码放入 PropertyGroup
。我们将旧的 BackupFileDirectory
属性替换为新值。这次备份将备份到“所有用户”配置文件文件夹(在 Windows 7 上是 C:\ProgramData)。还有一个属性 - BackupOutputDirectory
,其中包含我们旧的 BackupFileDirectory
值 - 我们的实际备份文件夹。现在我们可以使用这些属性进行备份了。
<!-- Create database backup to temp file out of current user's profile folder -->
<MSBuild.Community.Tasks.SqlExecute
ConnectionString="$(MasterConnectionString)"
Command="BACKUP DATABASE [$(DatabaseName)] TO DISK = N'$(BackupFilePath)' WITH NOFORMAT, INIT, NAME = N'$(DatabaseName) - Full Backup - $(BuildDate)', SKIP, NOREWIND, NOUNLOAD, STATS = 10" />
<Message Text="Database backup created" />
<Copy SourceFiles="$(BackupFilePath)" DestinationFolder="$(BackupOutputDirectory)" />
<!-- Delete temporary backup file -->
<Delete Files="$(BackupFilePath)" />
<SqlExecute>
任务保持不变。我们只需将备份文件从临时“所有用户”文件夹复制到我们的目标备份文件夹,然后在最后删除临时文件。
为确保我们的备份文件夹存在,请将此添加到 Common
任务中
<MakeDir Directories="$(BackupFileDirectory)" />
<MakeDir Directories="$(BackupOutputDirectory)" />
我们现在拥有了一个相当健壮的备份过程。还可以做一件事。备份文件通常非常大,而且通常非常“可压缩”。
问题 #9
现在我们已经准备好了一切,压缩备份应该不是问题。我们将使用 MSBuild Community Tasks 及其 <Zip>
任务。由于我们需要为 zip 文件命名,让我们在 Common
任务中创建另一个属性。
<CreateProperty Value="$(BackupOutputDirectory)$(DatabaseName)-$(BuildDate).zip">
<Output TaskParameter="Value" PropertyName="BackupOutputPath" />
</CreateProperty>
现在我们可以在 <Zip>
任务中使用 BackupOutputPath
属性。只需将上一步中的 <Copy>
任务替换为 <Zip>
任务即可。
<MSBuild.Community.Tasks.Zip
Files="$(BackupFilePath)"
WorkingDirectory="$(BackupFileDirectory)"
ZipFileName="$(BackupOutputPath)"
ZipLevel="9" />
就是这样。我们现在拥有备份所需的一切。您可以尝试一下
msbuild.exe /target:Backup Migration.msbuild
如何在每次数据库升级/迁移之前进行备份?让您的 Migrate
目标依赖于 Backup
目标。
<Target Name="Migrate" DependsOnTargets="Backup">
太棒了。两个功能已完成。还有一个要完成。
功能 #3 - 数据库还原
这不是您经常会做的事情,但当您需要时,自动还原可能会派上用场。我们已经拥有了进行还原所需的所有原料,所以让我们写下目标。
<Target Name="Restore" DependsOnTargets="Common">
<Message Text="Restoring the database from /backup folder" />
<!-- Unzip the given backup file -->
<Message Text="Unzipping the backup file $(RestoreFileName).zip" />
<MSBuild.Community.Tasks.Unzip
ZipFileName="$(BackupOutputDirectory)$(RestoreFileName).zip"
TargetDirectory="$(BackupFileDirectory)" />
<CreateProperty Value="$(BackupFileDirectory)$(RestoreFileName).bak">
<Output TaskParameter="Value" PropertyName="BackupFilePath" />
</CreateProperty>
<MSBuild.Community.Tasks.SqlExecute
ConnectionString='$(MasterConnectionString)'
Command="DECLARE @SQL varchar(max); SET @SQL = ''; SELECT @SQL = @SQL + 'Kill ' + Convert(varchar, SPId) + ';' FROM MASTER..SysProcesses WHERE DBId = DB_ID('$(DatabaseName)') AND SPId <> @@SPId; EXEC(@SQL); RESTORE DATABASE [$(DatabaseName)] FROM DISK = N'$(BackupFilePath)' WITH FILE = 1, NOUNLOAD, REPLACE, STATS = 10;"
/>
<!-- Delete temporary backup file -->
<Delete Files="$(BackupFilePath)" />
<Message Text="Database restored successfully from $(RestoreFileName).zip" />
</Target>
要执行还原,我们只是反转备份步骤。注意我们有一个 RestoreFileName
属性,该属性未在我们的 MSBuild 文件中定义?调用 MSBuild 时需要定义该属性,如下所示
msbuild.exe /property:Configuration=Release;RestoreFileName="CleanDatabase" Migration.msbuild /target:Restore
请注意,仅支持 \backup 文件夹内的文件。这对于我们简单的案例来说已经足够了。
再次将解压缩操作到“所有用户”配置文件文件夹 - 以允许 SQL Server 选择备份,即使 zip 文件本身在用户配置文件文件夹下。
数据库还原也使用 <SqlExecute>
任务。这次,SQL 命令有点长,因为它包含了在尝试执行还原操作之前终止到特定数据库的所有连接的代码。
还原数据库后,我们从“所有用户”配置文件文件夹中删除临时备份文件。现在我们可以运行它了
msbuild.exe /property:Configuration=Release;RestoreFileName="<ExistingBackupName>" Migration.msbuild /target:Restore
整理
下载的代码还包含三个批处理文件 dbupgrade
、dbbackup
和 dbrestore
,以便于手动运行这些目标。这将为我们节省一些输入(无需调用 msbuild.exe 并带上附加参数)。只有 dbrestore
有一个附加参数 - 要还原的备份文件名。
呼,这比预期的要长,但我们现在拥有维护数据库所需的一切。除了能够轻松地手动运行升级、备份和还原之外,这些脚本还可以用于持续集成环境。构建服务器可以在构建任务中运行它们,并准备测试和/或暂存数据库。
关注点
经历并解决/规避所有这些问题,有时很有趣,有时很令人沮丧,但最终结果看起来非常可用。这个组合已经在我的两个项目中使用了。与我们从自定义 dbtool 迁移到 FluentMigrator
和 MSBuild 相比,我们没有损失任何东西,而且我们获得了更简单的迁移代码(C# 代码而不是 SQL 脚本)。这并不适用于所有人或所有规模的项目,但对于小型项目和小型数据库来说,这是一个不错的选择。
再次,源代码可在 GitHub 上获取,并通过本文顶部的下载链接获取。
FluentMigrator
MSBuild
历史
初始版本
- 2012-06-12: 原始文章