Appearance
迁移系统详解
目录
什么是数据库迁移
概念理解
数据库迁移(Migration)是 EF Core 管理数据库架构(schema)变更的机制。它允许开发者:
- 代码优先(Code First): 从 C# 实体类生成数据库表
- 版本控制: 将数据库架构变更作为代码提交到 Git
- 可重复应用: 在不同环境(开发/测试/生产)中一致地应用变更
- 回滚能力: 可以撤销已应用的迁移
为什么需要迁移?
传统方式的问题
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 Fix2. 审查阶段
创建迁移后,必须审查生成的代码:
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.sql4. 验证阶段
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 infoPackage 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
Test2. 原子性原则
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
# ⚠️ 注意: 生产环境中谨慎使用 removeQ3: 如何处理大型迁移?
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/ 文件夹总结
核心要点
- 迁移是什么: EF Core 管理数据库架构变更的版本控制系统
- 工作原理: 通过比对模型差异生成迁移文件,应用时执行 SQL
- 文件结构: 每个迁移包含主文件、Designer 文件和 Snapshot
- 生命周期: 创建 → 审查 → 应用 → 验证
- 最佳实践: 原子性、命名规范、代码审查、数据安全
性能建议
| 场景 | 建议 | 预期效果 |
|---|---|---|
| 小型项目(< 20 实体) | 直接迁移 | 秒级完成 |
| 中型项目(20-100 实体) | 拆分迁移 | 便于管理 |
| 大型项目(> 100 实体) | 模块化迁移 | 性能提升 50%+ |
| 生产环境部署 | SQL 脚本 | 可控、可审计 |
| 大数据量变更 | 分批处理 | 避免锁表 |
学习路径
初学者:
1. 掌握基本迁移命令
2. 理解 Up/Down 方法
3. 学会回滚迁移
中级开发者:
1. 处理复杂关系映射
2. 数据迁移策略
3. 团队协作流程
高级开发者:
1. 自定义迁移操作
2. 生产环境部署策略
3. 迁移冲突解决
4. 性能优化