Skip to content

迁移系统详解 ​

目录 ​


什么是数据库迁移 ​

概念理解 ​

数据库迁移(Migration)是 EF Core 管理数据库架构(schema)变更的机制。它允许开发者:

  1. 代码优先(Code First): 从 C# 实体类生成数据库表
  2. 版本控制: 将数据库架构变更作为代码提交到 Git
  3. 可重复应用: 在不同环境(开发/测试/生产)中一致地应用变更
  4. 回滚能力: 可以撤销已应用的迁移

为什么需要迁移? ​

传统方式的问题 ​

sql
-- 手动编写 SQL 脚本
-- V1_Initial.sql
CREATE TABLE Products (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    Price DECIMAL(18,2) NOT NULL
);

-- V2_AddDescription.sql
ALTER TABLE Products ADD Description NVARCHAR(500);

-- V3_AddIndex.sql
CREATE INDEX IX_Products_Name ON Products(Name);

问题:

  • ❌ 需要手动编写和维护 SQL 脚本
  • ❌ 不同数据库(SQL Server/PostgreSQL/MySQL)语法不同
  • ❌ 难以追踪哪些迁移已应用
  • ❌ 团队协作时容易出错

迁移方案的优势 ​

csharp
// C# 代码定义实体
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// EF Core 自动生成 SQL
// SQL Server: CREATE TABLE [Products] ([Id] int IDENTITY(1,1) ...)
// PostgreSQL: CREATE TABLE "Products" ("Id" serial ...)
// MySQL: CREATE TABLE `Products` (`Id` int AUTO_INCREMENT ...)

优势:

  • ✅ 使用 C# 代码,类型安全
  • ✅ 跨数据库兼容
  • ✅ 自动追踪迁移状态
  • ✅ 支持团队协作

迁移的工作原理 ​

核心组件 ​

┌─────────────────────────────────────────┐
│       应用程序 (C# 实体类)               │
│  ┌──────────────┬──────────────────┐   │
│  │  DbContext   │  Entity Classes  │   │
│  └──────┬───────┴────────┬─────────┘   │
└─────────┼────────────────┼─────────────┘
          │                │
          ▼                │
┌─────────────────────────┼─────────────┐
│      Migrations         │             │
│  ┌──────────────────┐   │             │
│  │ 20240101_Create  │   │             │
│  │ 20240102_AddDesc │   │             │
│  │ 20240103_AddIdx  │   │             │
│  └────────┬─────────┘   │             │
└───────────┼─────────────┘             │
            │                           │
            ▼                           │
┌───────────────────────────────────────┐
│     __EFMigrationsHistory             │
│  ┌──────────────────────────────────┐ │
│  │ MigrationId | ProductVersion     │ │
│  ├──────────────────────────────────┤ │
│  │ 20240101... | 8.0.0             │ │
│  │ 20240102... | 8.0.0             │ │
│  └──────────────────────────────────┘ │
└───────────────────────────────────────┘
            │
            ▼
┌───────────────────────────────────────┐
│        数据库 (实际表结构)              │
│  ┌──────────────────────────────────┐ │
│  │ Products                         │ │
│  │ - Id (PK)                        │ │
│  │ - Name                           │ │
│  │ - Description                    │ │
│  └──────────────────────────────────┘ │
└───────────────────────────────────────┘

工作流程 ​

mermaid
graph TB
    A[修改实体类] --> B[创建迁移]
    B --> C[生成迁移文件]
    C --> D[审查迁移代码]
    D --> E[应用迁移到数据库]
    E --> F[记录到迁移历史表]
    F --> G[数据库架构更新完成]

详细步骤示例 ​

步骤 1: 修改实体类 ​

csharp
// 初始实体
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// 修改后: 添加 Description 属性
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; } // 新增
}

步骤 2: 创建迁移 ​

bash
dotnet ef migrations add AddProductDescription

生成的文件:

Migrations/
├── 20240115083000_AddProductDescription.cs
├── 20240115083000_AddProductDescription.Designer.cs
└── AppDbContextModelSnapshot.cs

步骤 3: 查看生成的迁移代码 ​

csharp
// 20240115083000_AddProductDescription.cs
public partial class AddProductDescription : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 添加 Description 列
        migrationBuilder.AddColumn<string>(
            name: "Description",
            table: "Products",
            type: "nvarchar(max)",
            nullable: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // 回滚: 删除 Description 列
        migrationBuilder.DropColumn(
            name: "Description",
            table: "Products");
    }
}

步骤 4: 应用迁移 ​

bash
dotnet ef database update

执行的 SQL:

sql
-- 检查迁移历史
SELECT [MigrationId], [ProductVersion]
FROM [__EFMigrationsHistory];

-- 执行迁移
ALTER TABLE [Products] ADD [Description] nvarchar(max) NULL;

-- 记录迁移
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20240115083000_AddProductDescription', N'8.0.0');

迁移文件结构 ​

文件组成 ​

每个迁移包含三个关键文件:

Migrations/
├── 20240115083000_AddProductDescription.cs          # 迁移主文件
├── 20240115083000_AddProductDescription.Designer.cs # 设计器元数据
└── AppDbContextModelSnapshot.cs                     # 模型快照

1. 迁移主文件 ​

csharp
// 20240115083000_AddProductDescription.cs
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MyApp.Migrations
{
    /// <summary>
    /// 迁移类: 类名对应迁移名称
    /// 部分类(partial): 可以与自定义代码合并
    /// </summary>
    public partial class AddProductDescription : Migration
    {
        /// <summary>
        /// 升级方法: 应用此迁移时的操作
        /// </summary>
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<string>(
                name: "Description",
                table: "Products",
                type: "nvarchar(max)",
                nullable: true,
                comment: "产品描述"); // .NET 8+ 支持注释
            
            // 可以添加多个操作
            migrationBuilder.CreateIndex(
                name: "IX_Products_Description",
                table: "Products",
                column: "Description");
        }

        /// <summary>
        /// 降级方法: 回滚此迁移时的操作
        /// 必须是 Up 的逆操作
        /// </summary>
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropIndex(
                name: "IX_Products_Description",
                table: "Products");

            migrationBuilder.DropColumn(
                name: "Description",
                table: "Products");
        }
    }
}

2. Designer 文件 ​

csharp
// 20240115083000_AddProductDescription.Designer.cs
[DbContext(typeof(AppDbContext))]
[Migration("20240115083000_AddProductDescription")]
partial class AddProductDescription
{
    /// <summary>
    /// 构建目标模型
    /// 用于比较模型差异
    /// </summary>
    protected override void BuildTargetModel(ModelBuilder modelBuilder)
    {
#pragma warning disable 612, 618
        modelBuilder.Entity("MyApp.Models.Product", b =>
        {
            b.Property<int>("Id")
                .ValueGeneratedOnAdd()
                .HasColumnType("int");

            b.Property<string>("Name")
                .IsRequired()
                .HasColumnType("nvarchar(100)")
                .HasMaxLength(100);

            b.Property<string>("Description")
                .HasColumnType("nvarchar(max)");

            b.HasKey("Id");
            b.ToTable("Products");
        });
#pragma warning restore 612, 618
    }
}

3. 模型快照 ​

csharp
// AppDbContextModelSnapshot.cs
/// <summary>
/// 当前 DbContext 的完整模型快照
/// 每次创建新迁移时自动更新
/// 用于检测模型变化
/// </summary>
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
    protected override void BuildModel(ModelBuilder modelBuilder)
    {
#pragma warning disable 612, 618
        // 包含所有实体的完整配置
        modelBuilder.Entity("MyApp.Models.Product", b =>
        {
            b.Property<int>("Id")
                .ValueGeneratedOnAdd()
                .HasColumnType("int");

            b.Property<string>("Name")
                .IsRequired()
                .HasColumnType("nvarchar(100)")
                .HasMaxLength(100);

            b.Property<string>("Description")
                .HasColumnType("nvarchar(max)");

            b.HasKey("Id");
            b.ToTable("Products");
        });

        modelBuilder.Entity("MyApp.Models.Category", b =>
        {
            // ... 其他实体配置
        });
#pragma warning restore 612, 618
    }
}

迁移生命周期 ​

1. 创建阶段 ​

bash
# 基本语法
dotnet ef migrations add <MigrationName>

# 完整示例
dotnet ef migrations add AddProductDescription \
    --context AppDbContext \
    --output-dir Migrations \
    --verbose

参数说明:

  • <MigrationName>: 迁移名称(驼峰命名法)
  • --context: 指定 DbContext 类名
  • --output-dir: 迁移文件输出目录
  • --verbose: 显示详细日志

命名约定:

bash
# ✅ 推荐: 清晰描述变更
dotnet ef migrations add AddProductDescription
dotnet ef migrations add CreateOrdersTable
dotnet ef migrations add AddIndexToProductName

# ❌ 避免: 模糊不清
dotnet ef migrations add Update1
dotnet ef migrations add Change2
dotnet ef migrations add Fix

2. 审查阶段 ​

创建迁移后,必须审查生成的代码:

csharp
// 检查清单:
// □ 是否生成了预期的变更?
// □ Up 和 Down 方法是否对称?
// □ 数据类型是否正确?
// □ 是否有破坏性变更(删除列/表)?
// □ 是否需要数据迁移?

// 示例: 重命名列需要特殊处理
// ❌ 自动生成的迁移(会丢失数据)
migrationBuilder.DropColumn(name: "OldName", table: "Products");
migrationBuilder.AddColumn<string>(name: "NewName", table: "Products");

// ✅ 手动修正(保留数据)
migrationBuilder.RenameColumn(
    name: "OldName",
    table: "Products",
    newName: "NewName");

3. 应用阶段 ​

bash
# 应用到最新版本
dotnet ef database update

# 应用到指定版本
dotnet ef database update AddProductDescription

# 回滚到空数据库(删除所有迁移)
dotnet ef database update 0

# 生成 SQL 脚本(生产环境推荐)
dotnet ef migrations script -o migration.sql

4. 验证阶段 ​

bash
# 检查是否有未应用的迁移
dotnet ef migrations list

# 输出示例:
# 20240101000000_InitialCreate (applied)
# 20240115083000_AddProductDescription (pending)  ← 未应用

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

.NET 8/9/10 新特性 ​

.NET 8 增强功能 ​

1. TPC (Table-Per-Concrete-Type) 映射 ​

csharp
public abstract class BaseEntity
{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class Product : BaseEntity
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Order : BaseEntity
{
    public string OrderNumber { get; set; }
    public decimal Total { get; set; }
}

// 配置 TPC
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BaseEntity>()
        .UseTpcMappingStrategy(); // .NET 7+ 引入,.NET 8 稳定
    
    modelBuilder.Entity<Product>();
    modelBuilder.Entity<Order>();
}

生成的迁移:

csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Products 表包含基类属性
    migrationBuilder.CreateTable(
        name: "Products",
        columns: table => new
        {
            Id = table.Column<int>(type: "int", nullable: false),
            CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
            Name = table.Column<string>(type: "nvarchar(100)", nullable: false),
            Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
        });

    // Orders 表也包含基类属性
    migrationBuilder.CreateTable(
        name: "Orders",
        columns: table => new
        {
            Id = table.Column<int>(type: "int", nullable: false),
            CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
            OrderNumber = table.Column<string>(type: "nvarchar(50)", nullable: false),
            Total = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
        });
}

2. 存储过程映射 ​

csharp
// .NET 8 支持将 CRUD 操作映射到存储过程
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .InsertUsingStoredProcedure("Products_Insert", sp =>
        {
            sp.HasParameter(p => p.Name);
            sp.HasParameter(p => p.Price);
            sp.HasResultColumn(p => p.Id); // 返回生成的 ID
        })
        .UpdateUsingStoredProcedure("Products_Update", sp =>
        {
            sp.HasOriginalValueParameter(p => p.Id);
            sp.HasOriginalValueParameter(p => p.Name);
            sp.HasParameter(p => p.Price);
        })
        .DeleteUsingStoredProcedure("Products_Delete", sp =>
        {
            sp.HasOriginalValueParameter(p => p.Id);
        });
}

生成的迁移:

csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // 创建存储过程
    migrationBuilder.Sql(@"
        CREATE PROCEDURE Products_Insert
            @Name nvarchar(100),
            @Price decimal(18,2)
        AS
        BEGIN
            INSERT INTO Products (Name, Price) VALUES (@Name, @Price);
            SELECT SCOPE_IDENTITY() AS Id;
        END
    ");

    migrationBuilder.Sql(@"
        CREATE PROCEDURE Products_Update
            @Id int,
            @Name nvarchar(100),
            @Price decimal(18,2)
        AS
        BEGIN
            UPDATE Products SET Name = @Name, Price = @Price WHERE Id = @Id;
        END
    ");

    migrationBuilder.Sql(@"
        CREATE PROCEDURE Products_Delete
            @Id int
        AS
        BEGIN
            DELETE FROM Products WHERE Id = @Id;
        END
    ");
}

.NET 9 新特性(预览) ​

1. 改进的迁移性能 ​

csharp
// .NET 9 优化了大型模型的迁移生成速度
// 对于包含 100+ 实体的项目,迁移生成速度提升 50-70%

// 启用增量编译(.NET 9+)
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    options.EnableCompilationLogging(); // .NET 9 新增
});

2. 更智能的迁移差异检测 ​

csharp
// .NET 9 改进了模型比较算法
// 减少误报和不必要的迁移

// 之前: 仅因配置顺序不同就生成迁移
// .NET 9: 正确识别为相同模型

.NET 10 路线图(2025) ​

预计特性:

  • 更好的多租户迁移支持
  • 迁移冲突自动检测和解决
  • 迁移可视化图形界面
  • 更细粒度的迁移控制 API

常用命令速查 ​

CLI 命令 ​

bash
# 创建迁移
dotnet ef migrations add <Name>

# 列出所有迁移
dotnet ef migrations list

# 移除最后一个迁移(未应用)
dotnet ef migrations remove

# 应用到最新版本
dotnet ef database update

# 应用到指定版本
dotnet ef database update <MigrationName>

# 生成 SQL 脚本
dotnet ef migrations script

# 生成从 A 到 B 的脚本
dotnet ef migrations script InitialCreate AddProductDescription

# 删除数据库
dotnet ef database drop

# 查看 DbContext 信息
dotnet ef dbcontext info

Package Manager Console 命令 ​

powershell
# Visual Studio Package Manager Console

# 创建迁移
Add-Migration AddProductDescription

# 应用迁移
Update-Database

# 回滚
Update-Database InitialCreate

# 列出迁移
Get-Migration

# 移除迁移
Remove-Migration

# 生成脚本
Script-Migration

最佳实践 ​

1. 迁移命名规范 ​

bash
# ✅ 推荐格式: 动词 + 对象 + 详情
AddProductDescription
CreateOrdersTable
AddIndexToProductName
RenameCustomerEmail
DropObsoleteTables

# ❌ 避免
Update1
Change
Fix
Test

2. 原子性原则 ​

bash
# ✅ 每个迁移只做一件事
dotnet ef migrations add AddProductDescription
dotnet ef migrations add AddProductPriceIndex

# ❌ 不要混合多个无关变更
dotnet ef migrations add BigUpdate  # 包含多个不相关的变更

3. 代码审查 ​

csharp
// 审查清单:
// 1. 检查生成的 Up/Down 方法
// 2. 确认不会丢失数据
// 3. 测试回滚(Down 方法)
// 4. 在开发环境充分测试
// 5. 准备回滚计划

// 示例: 安全的列添加
protected override void Up(MigrationBuilder migrationBuilder)
{
    // ✅ 允许 NULL,不影响现有数据
    migrationBuilder.AddColumn<string>(
        name: "Description",
        table: "Products",
        type: "nvarchar(max)",
        nullable: true);
}

// ❌ 危险的列添加(NOT NULL 无默认值)
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "Description",
        table: "Products",
        type: "nvarchar(max)",
        nullable: false); // 失败! 现有行无法满足约束
}

4. 数据迁移策略 ​

csharp
// 场景: 将 FullName 拆分为 FirstName 和 LastName

protected override void Up(MigrationBuilder migrationBuilder)
{
    // 步骤 1: 添加新列(允许 NULL)
    migrationBuilder.AddColumn<string>(
        name: "FirstName",
        table: "Customers",
        type: "nvarchar(50)",
        nullable: true);

    migrationBuilder.AddColumn<string>(
        name: "LastName",
        table: "Customers",
        type: "nvarchar(50)",
        nullable: true);

    // 步骤 2: 迁移数据
    migrationBuilder.Sql(@"
        UPDATE Customers 
        SET FirstName = LEFT(FullName, CHARINDEX(' ', FullName) - 1),
            LastName = RIGHT(FullName, LEN(FullName) - CHARINDEX(' ', FullName))
        WHERE CHARINDEX(' ', FullName) > 0
    ");

    // 步骤 3: 设置为必填
    migrationBuilder.AlterColumn<string>(
        name: "FirstName",
        table: "Customers",
        type: "nvarchar(50)",
        nullable: false);

    migrationBuilder.AlterColumn<string>(
        name: "LastName",
        table: "Customers",
        type: "nvarchar(50)",
        nullable: false);

    // 步骤 4: 删除旧列
    migrationBuilder.DropColumn(
        name: "FullName",
        table: "Customers");
}

5. 团队协作 ​

bash
# 问题: 多人同时创建迁移导致冲突

# 解决方案 1: 定期同步
git pull
dotnet ef database update  # 应用他人的迁移
dotnet ef migrations add MergeChanges  # 基于最新模型创建迁移

# 解决方案 2: 使用分支策略
# - main 分支: 稳定的迁移
# - develop 分支: 开发中的迁移
# - feature 分支: 功能迁移

# 解决方案 3: 迁移锁定(CI/CD)
# 在 CI 管道中检查迁移冲突

常见问题 ​

Q1: 迁移后数据库没有变化? ​

bash
# 检查迁移是否已应用
dotnet ef migrations list

# 如果显示 pending,需要应用
dotnet ef database update

# 检查连接字符串是否正确
# appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=..."  # 确认指向正确的数据库
  }
}

Q2: 如何删除错误的迁移? ​

bash
# 如果迁移还未应用
dotnet ef migrations remove

# 如果迁移已应用,需要先回滚
dotnet ef database update PreviousMigration
dotnet ef migrations remove

# ⚠️ 注意: 生产环境中谨慎使用 remove

Q3: 如何处理大型迁移? ​

csharp
// 策略 1: 拆分为多个小迁移
dotnet ef migrations add AddUserFields
dotnet ef migrations add AddUserIndexes
dotnet ef migrations add AddUserRelationships

// 策略 2: 分批处理大数据
protected override void Up(MigrationBuilder migrationBuilder)
{
    // 每批处理 1000 条记录
    migrationBuilder.Sql(@"
        WHILE EXISTS (SELECT 1 FROM TempTable WHERE Processed = 0)
        BEGIN
            UPDATE TOP (1000) TempTable
            SET Processed = 1
            WHERE Processed = 0;
            
            WAITFOR DELAY '00:00:01'; -- 避免锁表
        END
    ");
}

// 策略 3: 在线下时间执行
// 使用 SQL Agent Job 或 Hangfire 在低峰期执行

Q4: 迁移文件丢失怎么办? ​

bash
# 情况 1: 迁移已应用但文件丢失
# 解决: 从备份恢复或重新创建

# 情况 2: 迁移未应用且文件丢失
# 解决: 重新创建迁移
dotnet ef migrations add RecreateMigration

# 预防: 将迁移文件提交到 Git
# .gitignore 不要忽略 Migrations/ 文件夹

总结 ​

核心要点 ​

  1. 迁移是什么: EF Core 管理数据库架构变更的版本控制系统
  2. 工作原理: 通过比对模型差异生成迁移文件,应用时执行 SQL
  3. 文件结构: 每个迁移包含主文件、Designer 文件和 Snapshot
  4. 生命周期: 创建 → 审查 → 应用 → 验证
  5. 最佳实践: 原子性、命名规范、代码审查、数据安全

性能建议 ​

场景建议预期效果
小型项目(< 20 实体)直接迁移秒级完成
中型项目(20-100 实体)拆分迁移便于管理
大型项目(> 100 实体)模块化迁移性能提升 50%+
生产环境部署SQL 脚本可控、可审计
大数据量变更分批处理避免锁表

学习路径 ​

初学者:
  1. 掌握基本迁移命令
  2. 理解 Up/Down 方法
  3. 学会回滚迁移

中级开发者:
  1. 处理复杂关系映射
  2. 数据迁移策略
  3. 团队协作流程

高级开发者:
  1. 自定义迁移操作
  2. 生产环境部署策略
  3. 迁移冲突解决
  4. 性能优化

下一步 ​

基于 MIT 许可发布