Skip to content

应用迁移与数据库更新 ​

目录 ​


应用迁移基础 ​

核心概念 ​

应用迁移(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 --verbose

Q2: 如何查看迁移详情? ​

bash
# 列出所有迁移
dotnet ef migrations list

# 查看待应用迁移
dotnet ef database update --dry-run

# 查看迁移 SQL(不执行)
dotnet ef migrations script --idempotent

Q3: 多 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.sql

Q4: 如何在运行时检查迁移状态? ​

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());

总结 ​

核心要点 ​

  1. 应用迁移: 使用 dotnet ef database update
  2. 回滚: 指定目标版本即可自动撤销后续迁移
  3. 生产部署: 始终使用 SQL 脚本,不要直接 update
  4. 零停机: 采用渐进式迁移策略
  5. 监控: 运行时检查迁移状态

命令速查 ​

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 审查安全可控
多实例部署分布式锁避免冲突
零停机渐进式迁移业务连续

下一步 ​

基于 MIT 许可发布