目录
介绍
示例迁移
一些最佳实践
编码
包依赖
程序启动
应用程序设置
控制器
服务
了解Fluent Migrator的实际应用
向上迁移
查看迁移
向下迁移
错误报告
结论
下载 FluentMigrator 示例 - 6.9 KB
介绍我发现FluentMigrator是修改数据库的好工具。它是一种易于学习的“流利”语法,可以满足我需要做的90%的工作,并且如果需要做一些独特的事情,它提供了一种自定义SQL操作的Execute方法。我发现为执行迁移和检查迁移版本提供Web API端点很有用,本文介绍如何在.NET 6中执行此操作。相同的控制器和服务也适用于.NET Core 3.1,唯一的区别将在程序/启动配置中。
示例迁移Fluent Migrator的语法在他们的介绍页面上进行了描述,因此我将在此处仅提供“向上”和“向下”迁移的简单示例。
using FluentMigrator;
namespace Clifton
{
[Migration(202201011201)]
public class _202201011201_CreateTables : Migration
{
public override void Up()
{
Create.Table("Test")
.WithColumn("Id").AsInt32().PrimaryKey().Identity().NotNullable()
.WithColumn("IntField").AsInt32().Nullable()
.WithColumn("StringField").AsString().Nullable()
.WithColumn("DateField").AsDate().Nullable()
.WithColumn("DateTimeField").AsDateTime().Nullable()
.WithColumn("TimeField").AsTime().Nullable()
.WithColumn("BitField").AsBoolean().Nullable()
.WithColumn("Deleted").AsBoolean().NotNullable();
}
public override void Down()
{
Delete.Table("Test");
}
}
}
- 使用yyyyMMddhhmm格式对迁移版本进行编号,因为这样可以使您的迁移保持顺序。
- 为了帮助组织具有较长生命周期且随着时间推移不断改进的产品的迁移,请考虑为一年中的每个月添加一个文件夹和子文件夹。
- 理想情况下,迁移应该只对一个表或视图进行操作。当然可以完成多个操作,例如创建列,但考虑将多表迁移编写为单独的迁移。这样做的主要原因是它有助于隔离哪些迁移失败。
- 我并不热衷于写下“向下”迁移——我很少,如果有的话,不得不恢复到迁移。但是,您的用例可能会有所不同。
- 为您的迁移文件指定一个描述迁移原因的独特名称。这是保持多表迁移分开的另一个很好的理由,因为接触一个表的原因可能与其他表不同。
添加Fluent Migrator需要做一些工作。我更喜欢在单独的程序集中进行迁移,而不是在主Web API应用程序中。我还想捕获任何错误,奇怪的是,Fluent Migrator并没有让这件事变得容易——我无法弄清楚如何添加与Fluent Migrator提供的不同的记录器,人们会认为他们会至少提供一个流记录器!Fluent Migrator缺少的另一件事是创建数据库的能力,因此您将看到它是如何单独实现的。
包依赖使用以下软件包:
Dapper、System.Data.SqlClient和Newtonsoft.Json本质上是一次性的,原因如下:
- Dapper——只是为了方便检查数据库是否已经存在,如果不存在则创建它
- System.Data.SqlClient——因为这是Dapper使用
- Newtonsoft.Json——因为Newtonsoft.Json比System.Text.Json更好
我需要一段时间才能习惯.NET 6。我做的第一件事是禁用.csproj中的可空噩梦:
disable
而且我仍然习惯于隐式using而没有namespace和Main。也就是说,这里是Program.cs文件:
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using FluentMigrator.Runner;
using Newtonsoft.Json;
using Clifton;
using Interfaces;
var builder = WebApplication.CreateBuilder(args);
var appSettings = new AppSettings();
builder.Configuration.Bind(appSettings);
builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore;
options.SerializerSettings.Formatting = Formatting.Indented;
});
var connection = builder.Configuration.GetConnectionString(appSettings.UseDatabase);
builder.Services.AddDbContext(options => options.UseSqlServer(connection));
builder.Services.AddScoped();
string migrationAssemblyPath = Path.Combine
(appSettings.ExecutingAssembly.Location.LeftOfRightmostOf("\\"), appSettings.MigrationAssembly);
Assembly migrationAssembly = Assembly.LoadFrom(migrationAssemblyPath);
builder.Services.AddFluentMigratorCore()
.ConfigureRunner(rb => rb
.AddSqlServer()
.WithGlobalConnectionString(connection)
.ScanIn(migrationAssembly).For.Migrations())
.AddLogging(lb => lb.AddFluentMigratorConsole());
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
app.Run();
除了样板之外,我们在这里看到我正在添加NewtonsoftJson控制器,我这样做的目的是为了设置几个选项,包括缩进格式,因此为了本文的目的,返回的JSON在浏览器中的格式很好。
我们还看到添加了MigratorService ,以及FluentMigratorCore服务及其配置。
注意ScanIn调用——这很重要,因为它告诉Fluent Migrator为实现Migration属性和基类的类扫描哪个程序集。
应用程序设置配置来自appsettings.json文件,因此我们有一个JSON配置绑定到的AppSettings类:
using System.Reflection;
namespace Clifton
{
public class AppSettings
{
public static AppSettings Settings { get; set; }
public string UseDatabase { get; set; }
public string MigrationAssembly { get; set; }
public Assembly ExecutingAssembly => Assembly.GetExecutingAssembly();
public AppSettings()
{
Settings = this;
}
}
}
在appsettings.json中,我们有以下声明:
"UseDatabase": "DefaultConnection",
"MigrationAssembly": "Migrations.dll",
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=Test;Integrated Security=True;",
"MasterConnection": "Server=localhost;Database=master;Integrated Security=True;"
}
- UseDatabase:如果您想支持不同的数据库连接进行测试、开发、生产等。
- MigrationAssembly:持有迁移的程序集的名称。
- MasterConnection:这是在迁移器服务中硬编码的,用于检查数据库是否存在,如果不存在则创建它。
控制器实现:
- 向上迁移端点
- 向下迁移端点
- 列出所有迁移的端点
- 一个端点来获取我们的控制器/服务的版本,我发现它对确保API正常工作很有用
using Microsoft.AspNetCore.Mvc;
using Interfaces;
namespace Clifton
{
[ApiController]
[Route("[controller]")]
public class MigratorController : ControllerBase
{
private readonly IMigratorService ms;
private readonly AppDbContext context;
public MigratorController(IMigratorService ms, AppDbContext context)
{
this.ms = ms;
this.context = context;
}
[HttpGet]
public ActionResult Version()
{
return Ok(new { Version = "1.00" });
}
[HttpGet("VersionInfo")]
public ActionResult VersionInfo()
{
var recs = context.VersionInfo.OrderByDescending(v => v.Version);
return Ok(recs);
}
[HttpGet("MigrateUp")]
public ActionResult MigrateUp()
{
var resp = ms.MigrateUp();
return Ok(resp);
}
[HttpGet("MigrateDown/{version}")]
public ActionResult MigrateDown(long version)
{
var resp = ms.MigrateDown(version);
return Ok(resp);
}
}
}
该服务实现了向上迁移和向下迁移端点的迁移行为。
using System.Data.SqlClient;
using System.Text;
using Dapper;
using FluentMigrator.Runner;
using Interfaces;
namespace Clifton
{
public class MigratorService : IMigratorService
{
private IMigrationRunner runner;
private IConfiguration cfg;
public MigratorService(IMigrationRunner runner, IConfiguration cfg)
{
this.runner = runner;
this.cfg = cfg;
}
public string MigrateUp()
{
EnsureDatabase();
var errs = ConsoleHook(() => runner.MigrateUp());
var result = String.IsNullOrEmpty(errs) ? "Success" : errs;
return result;
}
// Migrate down *to* the version.
// If you want to migrate down the first migration,
// use any version # prior to that first migration.
public string MigrateDown(long version)
{
var errs = ConsoleHook(() => runner.MigrateDown(version));
var result = String.IsNullOrEmpty(errs) ? "Success" : errs;
return result;
}
private void EnsureDatabase()
{
var cs = cfg.GetConnectionString(AppSettings.Settings.UseDatabase);
var dbName = cs.RightOf("Database=").LeftOf(";");
var master = cfg.GetConnectionString("MasterConnection");
var parameters = new DynamicParameters();
parameters.Add("name", dbName);
using var connection = new SqlConnection(master);
var records = connection.Query
("SELECT name FROM sys.databases WHERE name = @name", parameters);
if (!records.Any())
{
connection.Execute($"CREATE DATABASE [{dbName}]");
}
}
private string ConsoleHook(Action action)
{
var saved = Console.Out;
var sb = new StringBuilder();
var tw = new StringWriter(sb);
Console.SetOut(tw);
try
{
action();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
tw.Close();
// Restore the default console out.
Console.SetOut(saved);
var errs = sb.ToString();
return errs;
}
}
}
上面代码的有趣之处在于:
- 查询系统表databases以查看数据库是否存在并在不存在时创建它的EnsureDatabase方法。
- 控制台hook,它将控制台输出捕获到写入到StringBuilder中。
- 奇怪的是,一些错误由Fluent Migrator处理并且不会引发异常,其他错误确实会引发异常,至少从我所看到的情况来看是这样。因此异常处理程序将异常消息写入控制台以被StringBuilder流捕获。在旧版本的Fluent Migrator中,曾经有一种方法可以抑制异常,但我找不到该配置选项的去向。
使用本文开头的示例迁移,我们可以使用端点(您的端口在Visual Studio中可能不同)将数据库更新为最新的迁移(好吧,我们只有一个):
localhost:5000/migrator/migrateup
我们看到:
我们可以使用以下方法检查迁移(同样,只有一个):
localhost:5000/migrator/versioninfo
我们看到:
是的,我们看到创建了Test数据库和Test表:
另请注意,该表VersionInfo是由 Fluent Migrator 自动创建的。
是的,这些列也是在Test表中创建的:
我们也可以向下迁移到特定版本。如果我们想迁移到第一次迁移之前,我们只需使用较早的迁移版本号:
http://localhost:5000/migrator/migratedown/202101011201
刷新SSMS中的表,我们看到表Test已被删除:
错误不会作为异常报告,而只是作为返回字符串报告。比如这里我删除了VersionInfo记录,让Fluent Migrator认为迁移没有运行,但是表已经存在,这就强制报错:
您可能希望将成功和错误状态包装在实际的JSON对象中。
结论将数据库迁移实现为Web API中的端点可以轻松运行迁移,而不是运行单独的迁移应用程序。这在所有环境中都很有用——本地托管的开发环境以及测试、QA和生产环境。应该注意的是,可能会向控制器端点添加身份验证/授权——您当然不希望有人无意中将生产数据库一直迁移到第0天!
https://www.codeproject.com/Articles/5324366/A-FluentMigrator-Controller-and-Service-for-NET-Co