Appearance
应用迁移与数据库更新
目录
应用迁移基础
核心概念
应用迁移(Applying Migrations) 是将迁移文件中的变更实际执行到数据库的过程。这是将代码中的模型变更同步到物理数据库的关键步骤。
工作流程
mermaid
graph LR
A[检查迁移状态] --> B{是否有待应用迁移?}
B -->|是| C[按顺序执行 Up 方法]
B -->|否| G[数据库已是最新]
C --> D[执行 SQL 变更]
D --> E[记录到迁移历史表]
E --> F[更新完成]迁移状态检查
bash
# 查看所有迁移及其状态
dotnet ef migrations list
# 输出示例:
# 20240101000000_InitialCreate (applied) ✓ 已应用
# 20240115083000_AddProductDescription (pending) ⏳ 待应用
# 20240116090000_AddPriceIndex (pending) ⏳ 待应用
# 查看详细信息
dotnet ef migrations list --json状态说明:
applied: 已应用到数据库pending: 待应用(尚未执行)
迁移管理命令
1. 应用到最新版本
bash
# 基本用法: 应用所有待处理的迁移
dotnet ef database update
# 完整参数
dotnet ef database update \
--context AppDbContext \
--connection "Server=...;Database=..." \
--verbose \
--no-build执行过程:
Step 1: 读取 __EFMigrationsHistory 表
┌──────────────────────────────────────┐
│ MigrationId | Version │
├──────────────────────────────────────┤
│ 20240101000000_Initial | 8.0.0 │
└──────────────────────────────────────┘
Step 2: 检测待应用迁移
┌──────────────────────────────────────┐
│ Pending Migrations: │
│ - 20240115083000_AddProductDesc │
│ - 20240116090000_AddPriceIndex │
└──────────────────────────────────────┘
Step 3: 按顺序执行 Up 方法
Executing: 20240115083000_AddProductDescription
ALTER TABLE Products ADD Description nvarchar(max) NULL;
INSERT INTO __EFMigrationsHistory VALUES (...);
Executing: 20240116090000_AddPriceIndex
CREATE INDEX IX_Products_Price ON Products(Price);
INSERT INTO __EFMigrationsHistory VALUES (...);
Step 4: 完成
Done! Database is up to date.2. 应用到指定版本
bash
# 回滚到特定迁移
dotnet ef database update AddProductDescription
# 效果:
# - 撤销 AddProductDescription 之后的所有迁移
# - 保留 AddProductDescription 及之前的迁移示例场景:
bash
# 当前状态: v1 → v2 → v3 (全部已应用)
# 目标: 回滚到 v2
dotnet ef database update V2Migration
# 执行:
# 1. 执行 V3 的 Down 方法(撤销 v3)
# 2. 保持 V2 不变
# 最终状态: v1 → v2 (v3 被撤销)3. 删除所有迁移
bash
# 回滚到空数据库(移除所有迁移)
dotnet ef database update 0
# 警告: 这将撤销所有迁移!
# 仅用于开发环境重新初始化执行效果:
csharp
// 按相反顺序执行所有 Down 方法
V3.Down(); // 撤销 v3
V2.Down(); // 撤销 v2
V1.Down(); // 撤销 v1
// 清空迁移历史表
DELETE FROM __EFMigrationsHistory;回滚与撤销
回滚策略
策略 1: 单步回滚
bash
# 回滚到上一个版本
dotnet ef database update PreviousMigrationName
# 示例: 从 v3 回滚到 v2
dotnet ef database update AddProductDescription策略 2: 完全重置
bash
# 方案 A: 回滚到空数据库
dotnet ef database update 0
# 方案 B: 删除并重建数据库
dotnet ef database drop
dotnet ef database update
# ⚠️ 危险操作! 会丢失所有数据!策略 3: 选择性回滚
csharp
// 手动创建自定义回滚迁移
public partial class SelectiveRollback : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 只撤销特定变更,保留其他功能
// 例如: 删除新添加的索引,但保留列
migrationBuilder.DropIndex(
name: "IX_Products_Price",
table: "Products");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 恢复索引
migrationBuilder.CreateIndex(
name: "IX_Products_Price",
table: "Products",
column: "Price");
}
}安全回滚检查清单
回滚前必须确认:
□ 备份当前数据库
□ 了解会丢失哪些数据
□ 通知相关团队成员
□ 准备回滚计划
□ 在开发环境测试回滚
□ 确认应用程序兼容性
□ 准备紧急恢复方案回滚示例: 删除列
csharp
// 原始迁移: 添加列
public partial class AddProductWeight : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "Weight",
table: "Products",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Weight",
table: "Products");
}
}
// 回滚操作
// dotnet ef database update PreviousVersion
// 执行的 SQL:
ALTER TABLE Products DROP COLUMN Weight;
DELETE FROM __EFMigrationsHistory
WHERE MigrationId = 'AddProductWeight';生成 SQL 脚本
为什么需要 SQL 脚本?
生产环境最佳实践: 不直接执行 dotnet ef database update,而是生成 SQL 脚本供 DBA 审查和执行。
优势对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
database update | 简单快速 | 直接修改数据库 | 开发环境 |
| SQL 脚本 | 可审查、可审计、可控 | 需要手动执行 | 生产环境 |
基本用法
bash
# 生成从零到最新的完整脚本
dotnet ef migrations script -o migration.sql
# 生成从 A 到 B 的增量脚本
dotnet ef migrations script InitialCreate AddProductDescription -o incremental.sql
# 生成幂等脚本(可重复执行)
dotnet ef migrations script --idempotent -o idempotent.sql脚本类型详解
1. 标准脚本
bash
dotnet ef migrations script -o standard.sql生成的 SQL:
sql
-- standard.sql
BEGIN TRANSACTION;
GO
-- 应用迁移: AddProductDescription
ALTER TABLE [Products] ADD [Description] nvarchar(max) NULL;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20240115083000_AddProductDescription', N'8.0.0');
GO
COMMIT;
GO特点:
- ✅ 简洁明了
- ❌ 不能重复执行
- ❌ 如果迁移已存在会报错
2. 幂等脚本(Idempotent)
bash
dotnet ef migrations script --idempotent -o idempotent.sql生成的 SQL:
sql
-- idempotent.sql
-- 可以安全地重复执行
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
END;
GO
-- 检查迁移是否已应用
IF NOT EXISTS(SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20240115083000_AddProductDescription')
BEGIN
ALTER TABLE [Products] ADD [Description] nvarchar(max) NULL;
END;
GO
IF NOT EXISTS(SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20240115083000_AddProductDescription')
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20240115083000_AddProductDescription', N'8.0.0');
END;
GO特点:
- ✅ 可以重复执行
- ✅ 自动检查迁移状态
- ✅ 生产环境推荐
- ⚠️ 脚本体积较大
3. 范围脚本
bash
# 生成从 v1 到 v3 的脚本
dotnet ef migrations script V1_AddEmail V3_AddPhone -o range.sql使用场景:
- 增量更新生产环境
- 跳过中间版本
- 精确控制升级路径
脚本参数详解
bash
dotnet ef migrations script \
<FROM> \ # 起始迁移(默认: 第一个未应用的迁移)
<TO> \ # 结束迁移(默认: 最新迁移)
-o output.sql \ # 输出文件
--idempotent \ # 生成幂等脚本
--no-transactions \ # 不使用事务包装
--context AppDbContext \ # 指定 DbContext
--verbose # 详细输出生产环境部署
部署策略对比
策略 1: 手动执行 SQL(推荐)
bash
# 步骤
# 1. 生成分发脚本
dotnet ef migrations script --idempotent -o production.sql
# 2. 将 production.sql 提交到 Git
git add production.sql
git commit -m "Add migration script for v1.2"
# 3. DBA 审查并执行
# sqlcmd -S production-server -d MyDatabase -i production.sql优点:
- ✅ 完全可控
- ✅ DBA 可以审查
- ✅ 可以在低峰期执行
- ✅ 有审计日志
流程:
mermaid
graph TB
A[开发团队创建迁移] --> B[生成 SQL 脚本]
B --> C[提交到 Git]
C --> D[DBA 审查脚本]
D --> E{审查通过?}
E -->|是| F[安排维护窗口]
E -->|否| G[返回修改]
F --> H[执行 SQL 脚本]
H --> I[验证结果]
I --> J[部署应用程序]策略 2: 自动迁移(小型项目)
csharp
// Program.cs (.NET 8 Minimal API)
var app = builder.Build();
// 自动应用迁移(仅开发/测试环境)
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate(); // 自动应用所有待处理迁移
}
app.Run();优点:
- ✅ 简单方便
- ✅ 启动时自动执行
缺点:
- ❌ 生产环境不推荐(不可控)
- ❌ 多个实例同时启动可能冲突
- ❌ 无法审查 SQL
改进版(带锁和重试):
csharp
public static async Task ApplyMigrationsWithLockAsync(WebApplication app)
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 使用分布式锁防止多实例冲突
var lockKey = "DbMigrationLock";
var lockTimeout = TimeSpan.FromMinutes(5);
try
{
// 尝试获取锁
var acquired = await db.Database
.ExecuteSqlRawAsync($"sp_getapplock @Resource='{lockKey}', @LockMode='Exclusive'") > 0;
if (!acquired)
throw new Exception("无法获取迁移锁,可能有其他实例正在执行迁移");
// 应用迁移
var pending = await db.Database.GetPendingMigrationsAsync();
if (pending.Any())
{
app.Logger.LogInformation("应用迁移: {Migrations}", string.Join(", ", pending));
await db.Database.MigrateAsync();
app.Logger.LogInformation("迁移应用成功");
}
}
catch (Exception ex)
{
app.Logger.LogError(ex, "迁移应用失败");
throw;
}
finally
{
// 释放锁
await db.Database.ExecuteSqlRawAsync($"sp_releaseapplock @Resource='{lockKey}'");
}
}策略 3: CI/CD 管道集成
yaml
# Azure Pipelines 示例
stages:
- stage: Build
jobs:
- job: GenerateMigrationScript
steps:
- script: dotnet ef migrations script --idempotent -o migration.sql
displayName: 'Generate Migration Script'
- publish: migration.sql
artifact: MigrationScript
- stage: Deploy_Database
jobs:
- deployment: ExecuteMigration
environment: Production
steps:
- download: current
artifact: MigrationScript
- task: SqlAzureDacpacDeployment@1
inputs:
azureSubscription: 'Production-SQL'
ServerName: '$(SqlServer)'
DatabaseName: '$(SqlDatabase)'
deployType: 'SqlTask'
SqlFile: '$(Pipeline.Workspace)/MigrationScript/migration.sql'
- stage: Deploy_App
jobs:
- deployment: DeployWebApp
environment: Production
steps:
- script: echo "Deploy application after database migration"零停机部署
蓝绿部署策略
阶段 1: 数据库变更(向后兼容)
┌─────────────────────────────────┐
│ 1. 添加新列(允许 NULL) │
│ 2. 同时写入旧列和新列 │
│ 3. 后台迁移数据 │
└─────────────────────────────────┘
↓
阶段 2: 应用程序部署
┌─────────────────────────────────┐
│ 1. 部署新版本应用 │
│ 2. 开始使用新列 │
│ 3. 停止写入旧列 │
└─────────────────────────────────┘
↓
阶段 3: 清理(下一个版本)
┌─────────────────────────────────┐
│ 1. 删除旧列 │
│ 2. 删除兼容代码 │
└─────────────────────────────────┘示例: 重命名列的零停机方案
csharp
// 迁移 1: 添加新列(版本 1.0)
public partial class AddNewEmail : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "EmailAddress", // 新列名
table: "Customers",
type: "nvarchar(100)",
nullable: true);
}
}
// 应用程序代码(兼容层)
public class Customer
{
[Obsolete("Use EmailAddress instead")]
public string Email { get; set; } // 旧列
public string EmailAddress { get; set; } // 新列
}
// 同时写入两列
customer.Email = "test@example.com";
customer.EmailAddress = "test@example.com";
// 迁移 2: 数据迁移(版本 1.1)
public partial class MigrateEmailData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
UPDATE Customers
SET EmailAddress = Email
WHERE EmailAddress IS NULL AND Email IS NOT NULL
");
}
}
// 迁移 3: 删除旧列(版本 2.0)
public partial class DropOldEmail : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "Email", table: "Customers");
}
}监控与验证
csharp
// 迁移后验证
public class MigrationValidator
{
private readonly AppDbContext _db;
private readonly ILogger<MigrationValidator> _logger;
public async Task ValidateAsync()
{
// 检查关键表是否存在
var tables = await _db.Database.SqlQueryRaw<string>(
"SELECT name FROM sys.tables").ToListAsync();
_logger.LogInformation("数据库表: {Tables}", string.Join(", ", tables));
// 检查迁移历史
var history = await _db.Database.SqlQueryRaw<MigrationRecord>(
"SELECT * FROM __EFMigrationsHistory ORDER BY AppliedOn DESC")
.ToListAsync();
foreach (var record in history.Take(5))
{
_logger.LogInformation("最近迁移: {Migration} at {Date}",
record.MigrationId, record.AppliedOn);
}
// 运行健康检查查询
var count = await _db.Products.CountAsync();
_logger.LogInformation("产品总数: {Count}", count);
}
}
public record MigrationRecord(string MigrationId, DateTime AppliedOn);.NET 8/9/10 最佳实践
.NET 8: 组迁移(Group Transactions)
csharp
// .NET 8 支持将多个迁移包装在单个事务中
using var transaction = await context.Database.BeginTransactionAsync();
try
{
await context.Database.MigrateAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}.NET 9: 迁移性能优化
csharp
// .NET 9 改进了大型迁移的性能
// 对于 100+ 实体的项目,迁移应用速度提升 40-60%
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.EnableDetailedErrors(); // .NET 9 增强错误信息
});.NET 10: 自动化改进(预览)
预计特性:
- 自动检测迁移冲突
- 智能合并多个迁移
- 迁移依赖关系图
- 更细粒度的进度报告
常见问题与解决方案
Q1: 迁移应用失败?
bash
# 错误: 连接失败
# 解决: 检查连接字符串
dotnet ef dbcontext info
# 错误: 迁移冲突
# 解决: 先拉取最新代码,重新创建迁移
git pull
dotnet ef migrations remove
dotnet ef migrations add NewMigration
# 错误: SQL 语法错误
# 解决: 检查生成的 SQL 脚本
dotnet ef migrations script --verboseQ2: 如何查看迁移详情?
bash
# 列出所有迁移
dotnet ef migrations list
# 查看待应用迁移
dotnet ef database update --dry-run
# 查看迁移 SQL(不执行)
dotnet ef migrations script --idempotentQ3: 多 DbContext 如何处理?
bash
# 为每个 DbContext 单独应用迁移
dotnet ef database update --context IdentityDbContext
dotnet ef database update --context BusinessDbContext
# 生成独立脚本
dotnet ef migrations script --context IdentityDbContext -o identity.sql
dotnet ef migrations script --context BusinessDbContext -o business.sqlQ4: 如何在运行时检查迁移状态?
csharp
public class MigrationStatusService
{
private readonly AppDbContext _db;
public async Task<MigrationStatus> GetStatusAsync()
{
var applied = await _db.Database.GetAppliedMigrationsAsync();
var pending = await _db.Database.GetPendingMigrationsAsync();
return new MigrationStatus
{
AppliedMigrations = applied.ToList(),
PendingMigrations = pending.ToList(),
IsUpToDate = !pending.Any()
};
}
}
public record MigrationStatus
{
public List<string> AppliedMigrations { get; init; }
public List<string> PendingMigrations { get; init; }
public bool IsUpToDate { get; init; }
}
// API 端点
app.MapGet("/api/migration-status", async (MigrationStatusService service) =>
await service.GetStatusAsync());总结
核心要点
- 应用迁移: 使用
dotnet ef database update - 回滚: 指定目标版本即可自动撤销后续迁移
- 生产部署: 始终使用 SQL 脚本,不要直接 update
- 零停机: 采用渐进式迁移策略
- 监控: 运行时检查迁移状态
命令速查
bash
# 应用到最新
dotnet ef database update
# 应用到指定版本
dotnet ef database update MigrationName
# 生成脚本
dotnet ef migrations script --idempotent -o production.sql
# 查看状态
dotnet ef migrations list最佳实践
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 开发环境 | database update | 快速迭代 |
| 测试环境 | SQL 脚本审查 | 模拟生产 |
| 生产环境 | 幂等脚本 + DBA 审查 | 安全可控 |
| 多实例部署 | 分布式锁 | 避免冲突 |
| 零停机 | 渐进式迁移 | 业务连续 |
下一步
- 📖 阅读 生产环境迁移最佳实践
- 🔧 学习 迁移高级技巧
- 🚀 了解 并发控制