Skip to content

迁移概述与工作流 ​

EF Core 的数据库版本控制系统,完整指南

📖 目录 ​


什么是迁移 ​

概念理解 ​

迁移(Migrations) 是 EF Core 的数据库版本控制系统,用于:

  1. 跟踪模型变化 - 实体类的修改
  2. 生成更新脚本 - SQL 变更语句
  3. 应用数据库变更 - 执行 SQL 脚本
  4. 版本回滚 - 恢复到之前的版本
代码模型变化 → 创建迁移 → 生成 SQL → 应用迁移 → 数据库更新

为什么需要迁移 ​

❌ 没有迁移的问题 ​

csharp
// 1. 添加新属性
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; } // 新增
}

// 问题:
// - 数据库表没有 Description 列
// - 运行时抛出异常
// - 需要手动修改数据库
// - 团队协作困难
// - 生产环境部署复杂

✅ 使用迁移的优势 ​

bash
# 1. 创建迁移
dotnet ef migrations add AddProductDescription

# 2. 应用迁移
dotnet ef database update

# 优势:
# ✅ 自动生成 SQL
# ✅ 版本控制
# ✅ 可重复执行
# ✅ 支持回滚
# ✅ 团队协作友好
# ✅ 生产环境安全

迁移工作流 ​

完整工作流程 ​

mermaid
graph LR
    A[修改实体类] --> B[创建迁移]
    B --> C[审查迁移代码]
    C --> D[测试迁移]
    D --> E[提交到版本控制]
    E --> F[应用到开发数据库]
    F --> G[合并到主分支]
    G --> H[应用到生产数据库]

详细步骤 ​

步骤 1: 修改模型 ​

csharp
// Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    // 新增字段
    public string Description { get; set; }
    public decimal Weight { get; set; }
    
    // 修改字段
    // public string Name { get; set; } → 增加长度限制
}

步骤 2: 创建迁移 ​

bash
dotnet ef migrations add AddProductDescriptionAndWeight

命名规范:

  • ✅ Add<Product>Description - 添加字段
  • ✅ Remove<Product>Price - 删除字段
  • ✅ Modify<Product>NameLength - 修改字段
  • ✅ Create<Orders>Table - 创建表
  • ❌ Migration1, Update - 不清晰

步骤 3: 审查生成的迁移 ​

csharp
// Migrations/20240101120000_AddProductDescriptionAndWeight.cs
public partial class AddProductDescriptionAndWeight : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 添加 Description 列
        migrationBuilder.AddColumn<string>(
            name: "Description",
            table: "Products",
            type: "nvarchar(max)",
            nullable: true);
        
        // 添加 Weight 列
        migrationBuilder.AddColumn<decimal>(
            name: "Weight",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
    }

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

检查要点:

  • ✅ 列类型是否正确
  • ✅ 是否允许 NULL
  • ✅ 默认值是否合理
  • ✅ Down 方法是否正确

步骤 4: 应用到数据库 ​

bash
# 应用到开发环境
dotnet ef database update

# 应用到指定迁移
dotnet ef database update AddProductDescriptionAndWeight

# 回滚到初始状态
dotnet ef database update 0

步骤 5: 提交到版本控制 ​

bash
git add Migrations/
git commit -m "Add Product Description and Weight fields"
git push

注意: 迁移文件必须提交到 Git!


常用命令 ​

CLI 命令(推荐) ​

bash
# 安装工具
dotnet tool install --global dotnet-ef
dotnet tool update --global dotnet-ef

# 创建迁移
dotnet ef migrations add <名称>

# 移除最后一次迁移
dotnet ef migrations remove

# 列出所有迁移
dotnet ef migrations list

# 应用迁移
dotnet ef database update
dotnet ef database update <迁移名称>

# 生成 SQL 脚本
dotnet ef migrations script
dotnet ef migrations script <from> <to>

# 查看 DbContext 信息
dotnet ef dbcontext info

# 逆向工程(Database First)
dotnet ef dbcontext scaffold "连接字符串" Microsoft.EntityFrameworkCore.SqlServer

PMC 命令(Visual Studio) ​

powershell
# 创建迁移
Add-Migration <名称>

# 移除迁移
Remove-Migration

# 列出迁移
Get-Migration

# 应用迁移
Update-Database
Update-Database <迁移名称>

# 生成 SQL 脚本
Script-Migration
Script-Migration -From <迁移> -To <迁移>

# 删除数据库
Drop-Database

迁移文件结构 ​

文件组织 ​

Migrations/
├── 20240101120000_InitialCreate.cs          # 迁移文件
├── 20240101120000_InitialCreate.Designer.cs # 元数据
├── 20240101130000_AddDescription.cs         # 迁移文件
├── 20240101130000_AddDescription.Designer.cs# 元数据
└── AppDbContextModelSnapshot.cs             # 当前模型快照

迁移文件详解 ​

csharp
// 20240101120000_InitialCreate.cs
[DbContext(typeof(AppDbContext))]
[Migration("20240101120000_InitialCreate")]
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 创建表
        migrationBuilder.CreateTable(
            name: "Categories",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Categories", x => x.Id);
            });
        
        // 创建索引
        migrationBuilder.CreateIndex(
            name: "IX_Products_CategoryId",
            table: "Products",
            column: "CategoryId");
        
        // 添加外键
        migrationBuilder.AddForeignKey(
            name: "FK_Products_Categories_CategoryId",
            table: "Products",
            column: "CategoryId",
            principalTable: "Categories",
            principalColumn: "Id",
            onDelete: ReferentialAction.Cascade);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // 删除外键
        migrationBuilder.DropForeignKey(
            name: "FK_Products_Categories_CategoryId",
            table: "Products");
        
        // 删除索引
        migrationBuilder.DropIndex(
            name: "IX_Products_CategoryId",
            table: "Products");
        
        // 删除表
        migrationBuilder.DropTable(name: "Categories");
    }
}

Designer 文件 ​

csharp
// 20240101120000_InitialCreate.Designer.cs
[DbContext(typeof(AppDbContext))]
partial class InitialCreate
{
    protected override void BuildTargetModel(ModelBuilder modelBuilder)
    {
        // 构建目标模型
        // 用于检测模型变化
    }
}

快照文件 ​

csharp
// AppDbContextModelSnapshot.cs
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
    protected override void BuildModel(ModelBuilder modelBuilder)
    {
        // 当前完整的模型定义
        // EF Core 用它对比检测变化
    }
}

注意:

  • ⚠️ 不要手动编辑快照文件
  • ⚠️ 每次迁移都会自动更新

最佳实践 ​

1. 迁移命名规范 ​

bash
# ✅ 好的命名
dotnet ef migrations add AddUserEmail
dotnet ef migrations add RemoveProductDiscount
dotnet ef migrations add ModifyOrderStatusLength
dotnet ef migrations add CreateAuditLogsTable

# ❌ 坏的命名
dotnet ef migrations add Migration1
dotnet ef migrations add Update
dotnet ef migrations add Changes

2. 一次迁移一个变更 ​

bash
# ✅ 推荐: 分开创建
dotnet ef migrations add AddUserEmail
dotnet ef migrations add AddUserProfilePicture

# ❌ 避免: 混合多个变更
dotnet ef migrations add MultipleChanges

优点:

  • ✅ 易于理解和审查
  • ✅ 便于回滚特定变更
  • ✅ 清晰的提交历史

3. 审查迁移代码 ​

csharp
// 创建迁移后,立即审查
public partial class AddUserEmail : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 检查:
        // □ 列类型是否正确?
        // □ 是否允许 NULL?
        // □ 是否有默认值?
        // □ 是否需要索引?
        
        migrationBuilder.AddColumn<string>(
            name: "Email",
            table: "Users",
            type: "nvarchar(100)",
            maxLength: 100,
            nullable: false,  // ⚠️ 确认是否允许 NULL
            defaultValue: ""); // ⚠️ 确认默认值
    }
}

4. 测试迁移 ​

bash
# 1. 在开发环境测试
dotnet ef database update

# 2. 验证功能正常
# 运行应用程序,测试新功能

# 3. 测试回滚
dotnet ef database update PreviousMigration
dotnet ef database update

# 4. 生成生产脚本
dotnet ef migrations script -o migration.sql

5. 生产环境部署 ​

方案 1: 应用迁移 ​

bash
# 在生产服务器执行
dotnet ef database update

优点: 简单
缺点: 需要安装 EF Core 工具


方案 2: SQL 脚本(推荐) ​

bash
# 生成 SQL 脚本
dotnet ef migrations script -o production.sql

# 在服务器上执行 SQL
sqlcmd -S server -d database -i production.sql

优点:

  • ✅ 无需安装工具
  • ✅ DBA 可以审查
  • ✅ 可以集成到部署流程

方案 3: 程序启动时自动应用 ​

csharp
// Program.cs
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
    // 自动应用所有待处理的迁移
    context.Database.Migrate();
}

优点: 自动化
缺点:

  • ⚠️ 需要适当权限
  • ⚠️ 生产环境谨慎使用

6. 处理种子数据 ​

csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // 插入种子数据
    migrationBuilder.InsertData(
        table: "Categories",
        columns: new[] { "Id", "Name" },
        values: new object[,]
        {
            { 1, "Electronics" },
            { 2, "Books" },
            { 3, "Clothing" }
        });
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    // 删除种子数据
    migrationBuilder.DeleteData(
        table: "Categories",
        keyColumn: "Id",
        keyValues: new object[] { 1, 2, 3 });
}

7. 处理破坏性变更 ​

csharp
// 场景: 重命名列 Name → Title

// ❌ 错误: 直接删除和创建会丢失数据
migrationBuilder.DropColumn("Name", "Products");
migrationBuilder.AddColumn<string>("Title", "Products", ...);

// ✅ 正确: 先复制数据
migrationBuilder.AddColumn<string>("Title", "Products", ...);

// 复制数据
migrationBuilder.Sql(@"
    UPDATE Products 
    SET Title = Name
");

migrationBuilder.DropColumn("Name", "Products");

常见问题 ​

1. 迁移冲突 ​

问题: 多人同时创建迁移

bash
# 解决方案 1: 重新同步
git pull
dotnet ef migrations remove
dotnet ef migrations add <新名称>

# 解决方案 2: 合并迁移
# 手动合并两个迁移文件

2. 迁移失败 ​

问题: 应用迁移时出错

bash
# 解决步骤:
# 1. 查看错误信息
dotnet ef database update

# 2. 回滚到上一个版本
dotnet ef database update PreviousMigration

# 3. 修复问题
# 修改迁移代码或模型

# 4. 重新应用
dotnet ef database update

3. 删除迁移 ​

bash
# 删除最后一次迁移
dotnet ef migrations remove

# 注意:
# ⚠️ 只能删除未应用的迁移
# ⚠️ 已应用的迁移需要先回滚

4. 重置迁移 ​

bash
# 完全重置(谨慎使用!)
dotnet ef database drop
dotnet ef migrations remove
dotnet ef migrations add InitialCreate
dotnet ef database update

💡 小结 ​

核心要点:

  • ✅ 迁移是数据库版本控制系统
  • ✅ 遵循工作流: 修改 → 创建 → 审查 → 测试 → 应用
  • ✅ 使用清晰的命名规范
  • ✅ 一次迁移一个变更
  • ✅ 生产环境使用 SQL 脚本
  • ✅ 始终审查生成的迁移代码

下一步:

  1. 学习 创建迁移
  2. 掌握 应用迁移
  3. 理解 生产环境最佳实践

基于 MIT 许可发布