5.8 EF Core 迁移发布策略
迁移不是“本地运行一下 database update”。生产数据库承载真实业务数据,任何表结构变化都需要可审查、可备份、可回滚,并尽量避免长时间锁表和应用停机。迁移发布策略的目标是让代码版本和数据库结构稳定协同演进。
学习目标
- 能区分开发环境迁移和生产环境迁移的执行方式。
- 能生成、审查和归档 EF Core 迁移 SQL 脚本。
- 能设计向前兼容的表结构变更,降低停机风险。
- 能为失败迁移准备备份、回滚和补救方案。
应用场景
- 给
Todos表增加DueDate字段并发布到生产。 - 把一个可空字段改成必填字段。
- 给大表增加索引或拆分字段。
- 多个应用实例滚动部署时保持新旧代码都能运行。
核心概念
| 概念 | 说明 |
|---|---|
| Migration 文件 | EF Core 记录模型变化的 C# 文件,必须提交到版本库 |
| 迁移 SQL 脚本 | 从迁移生成的数据库变更脚本,生产发布前应审查 |
| 向前兼容 | 数据库先支持新旧代码共同运行,再逐步切换代码 |
| Expand / Contract | 先扩展结构保证兼容,确认稳定后再收缩旧结构 |
| 回滚 | 发布失败后的恢复路径,可能是回滚代码、执行反向脚本或数据修复 |
| 备份 | 生产迁移前保留可恢复的数据快照 |
案例:新增任务截止日期
新增可空字段通常是低风险迁移:旧代码不会使用它,新代码可以逐步写入。
dotnet ef migrations add AddTodoDueDate \
--project src/TodoApp.Infrastructure \
--startup-project src/TodoApp.Api
生成生产可审查 SQL:
dotnet ef migrations script \
--project src/TodoApp.Infrastructure \
--startup-project src/TodoApp.Api \
--output artifacts/migrations/2026-08-12-add-todo-due-date.sql
如果发布环境可能已经执行过部分迁移,可以生成幂等脚本:
dotnet ef migrations script --idempotent \
--project src/TodoApp.Infrastructure \
--startup-project src/TodoApp.Api \
--output artifacts/migrations/2026-08-12-idempotent.sql
脚本进入发布流程后,应由开发和数据库负责人审查,再在测试或预生产环境演练。
推荐发布流程
| 阶段 | 动作 | 产出 |
|---|---|---|
| 开发 | 修改实体和配置,生成 Migration | Migration 文件 |
| 本地验证 | 新库迁移、旧库升级、核心测试 | 可重复执行的验证记录 |
| 脚本生成 | 生成 SQL 或幂等 SQL | 可审查脚本 |
| 预生产演练 | 使用接近生产数据量验证耗时和锁 | 风险清单和执行窗口 |
| 生产发布 | 备份、执行脚本、部署应用、验证健康 | 发布记录 |
| 发布后 | 监控错误率、慢查询、连接和业务指标 | 回归确认 |
不要让生产应用启动时自动执行迁移,除非项目规模很小且团队明确接受风险。更稳妥的做法是把数据库变更作为发布步骤单独执行和审计。
示例:Expand / Contract
把 Title 拆成 Title 和 NormalizedTitle 时,不要一步删除旧字段或要求所有代码立即切换。更安全的节奏是:
- Expand:新增
NormalizedTitle可空字段,旧代码继续可用。 - Deploy:新代码开始写入两个字段,并读取新字段兜底旧字段。
- Backfill:后台脚本补齐历史数据。
- Enforce:确认无空值后增加约束或索引。
- Contract:所有实例都升级后,删除旧兼容逻辑或旧字段。
示例回填脚本:
UPDATE Todos
SET NormalizedTitle = UPPER(LTRIM(RTRIM(Title)))
WHERE NormalizedTitle IS NULL;
大表回填不要一次性更新全表,应按主键范围分批执行,并观察锁、日志增长和业务延迟。
示例:从可空改为必填
直接把可空列改成 NOT NULL 可能失败,因为历史数据里已经有空值。更稳妥的流程是先补数据,再加约束。
UPDATE Todos
SET DueDate = CreatedAt
WHERE DueDate IS NULL;
ALTER TABLE Todos
ALTER COLUMN DueDate DATETIMEOFFSET NOT NULL;
如果补值规则有业务含义,应由产品或业务负责人确认。不要为了让迁移通过就随意填默认值。
示例:索引发布注意点
索引能提升查询速度,但创建索引会消耗 CPU、IO 和日志空间。大表索引发布前应在预生产环境测量耗时。
CREATE INDEX IX_Todos_TenantId_IsCompleted_CreatedAt
ON Todos (TenantId, IsCompleted, CreatedAt DESC);
索引不是越多越好。每个索引都会增加写入成本和存储成本,应服务真实查询路径,并在发布后通过慢查询和执行计划验证效果。
回滚策略
| 失败类型 | 常见处理 |
|---|---|
| 应用代码失败,数据库兼容旧代码 | 回滚应用版本,保留数据库变更 |
| 数据库脚本执行失败且未提交 | 修复脚本或回滚事务 |
| 数据库脚本部分成功 | 根据发布记录执行补救脚本,必要时恢复备份 |
| 数据写入错误 | 停止写入、保留现场、执行数据修复脚本 |
生产回滚不一定等于执行 Down 迁移。删列、改类型、数据回填等操作可能不可逆。发布前要明确:哪些变更可以回滚,哪些只能向前修复。
重点难点
- Migration 文件要提交到版本库,不能只保留本地数据库状态。
- 生产迁移前要生成 SQL 脚本并审查,不要盲目执行自动迁移。
- 有数据的表做非空约束、类型变更和大索引时风险更高。
- 多实例滚动发布要求数据库结构同时兼容新旧代码。
- 回滚方案必须在发布前写清楚,发布失败时才不会临时猜测。
常见误区
| 误区 | 推荐做法 |
|---|---|
本地 database update 成功就直接上生产 | 生成 SQL、审查、演练、备份后发布 |
| 应用启动自动迁移生产库 | 把数据库迁移作为独立发布步骤 |
| 一次迁移同时改结构、搬数据、删旧列 | 拆成 Expand / Backfill / Contract 多步 |
| 只准备代码回滚 | 同时准备数据库补救和数据恢复方案 |
| 忽略迁移耗时 | 在接近生产数据量的环境测量锁和耗时 |
练习
- 为
Todos增加可空DueDate,生成 Migration 和 SQL 脚本。 - 设计一个把可空字段改成必填字段的两阶段发布流程。
- 为任务列表查询新增组合索引,并说明如何验证索引效果。
- 写一份发布检查清单:备份、脚本、执行人、验证项和回滚方案。
延伸阅读
- 本手册:EF Core CRUD 与迁移
- 本手册:查询性能与常见陷阱
- 本手册:部署与运维总览