PAYATHON 2026

如何让本地 SQL Server schema 与 main branch 保持同步?

技术老齐

结论

SQL Server 的 .mdf、.ldf 等 database data file 不应提交到 Git,带有业务状态的数据库也不应固化进 Docker image。建议采用以下做法:

  1. Entity Framework migrations 与相关代码一起提交、评审和合并。
  2. 以 migrations 作为 schema 的唯一来源。
  3. 本地开发数据库按可丢弃资源管理,并通过统一脚本重建。
  4. feature branch 合并前先同步最新 main。如有需要,重新生成该 branch 的 migration。
  5. 使用 seed、fixture 或脱敏备份恢复测试数据,避免依赖开发人员手工修改数据库。

Docker 可以统一 SQL Server 的运行环境,migration 管理仍然要单独完成。容器启动后,应使用当前 branch 中的 migrations 创建或升级 schema。

当前流程为什么容易失控

当前流程的问题在于,migration 被移出 Pull Request,等代码合并后再单独生成。这会带来以下问题:

  • schema 变更和相关代码无法作为同一个变更集接受评审。
  • 合并后生成的 migration 可能不同于评审期间开发人员实际使用的版本。
  • feature branch 中的 migration 已写入本地数据库,切换 branch 后却无法在代码中找到对应文件。
  • 开发人员不得不手工修改 tables、columns 和 __EFMigrationsHistory,本地数据库的状态会越来越难确认。

Entity Framework Core 根据 migration ID 和 __EFMigrationsHistory 判断已经执行了哪些 migrations。删除 migration 文件并不会撤销数据库中的 schema,也不会自动清除历史记录。手工修改 __EFMigrationsHistory 风险更高,可能导致历史记录与实际 schema 不一致。

推荐的 migration 合并流程

migration 应保留在 feature branch 中,并随 Pull Request 一起评审。如果开发期间 main 合入了其他 migrations,可以在最终合并前重新生成当前 feature 的 migration。

可以按下面的流程操作。

1. 在 feature branch 中创建 migration

dotnet ef migrations add AddCustomerStatus \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

dotnet ef database update \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

将 migration 文件、model snapshot 和相关代码放进同一个 Pull Request。

2. 合并前同步最新 main

如果 main 没有新增 migration,通常可以直接合并。

如果 main 已有新的 migration,最好基于更新后的 model snapshot 重新生成当前 feature migration。对于可以丢弃的本地数据库,可先删除数据库,再移除 feature migration:

dotnet ef database drop --force \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

dotnet ef migrations remove \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

然后同步 main:

git fetch origin
git rebase origin/main

解决代码或 model snapshot 冲突后,重新创建并验证 migration:

dotnet ef migrations add AddCustomerStatus \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

dotnet ef database update \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

重新生成后,检查 migration 的 Up、Down 方法和生成的 SQL,确认没有意外删除 columns、遗漏 indexes 或重复创建 objects。

已经应用到共享测试环境的 migration 不应随意删除或改写,应另建修正 migration。只有尚未进入共享环境和正式环境的 feature migration,才适合在 rebase 后重新生成。

为本地数据库提供统一重建脚本

团队应提供一条命令,将本地数据库恢复到与当前代码对应的 schema。最简单的 reset 脚本如下:

$project = "src/App.Infrastructure"
$startupProject = "src/App.Web"

dotnet ef database drop --force `
  --project $project `
  --startup-project $startupProject

if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
}

dotnet ef database update `
  --project $project `
  --startup-project $startupProject

if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
}

在 Bash 环境中可以写成:

#!/usr/bin/env bash
set -euo pipefail

dotnet ef database drop --force \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

dotnet ef database update \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

遇到以下情况时,可以运行该脚本:

  • 切回最新 main 后;
  • 切换到 migration 历史不同的 branch 后;
  • rebase 或 merge 出现 model snapshot 冲突后;
  • 本地 __EFMigrationsHistory 与代码不匹配时。

不建议默认在 Git post-checkout hook 中执行 destructive reset。开发人员可能在本地数据库里保留调试数据,自动删库容易造成意外损失。更合适的做法是提供显式命令,例如 ./scripts/reset-db.sh 或 Reset-DevDatabase.ps1。

需要保留本地数据时如何处理

如果开发工作依赖大量测试数据,可以分别处理 schema 重建和数据恢复:

  1. 删除并重建数据库。
  2. 执行当前 branch 的全部 migrations。
  3. 运行可重复执行的 seed 程序。
  4. 按需导入脱敏后的测试数据。

Seed 应尽量支持重复执行,例如根据稳定的业务键判断数据是否已经存在。个人调试数据不要写入全局 seed。

数据量较大的项目也可以维护脱敏后的 SQL Server backup。将 backup 存放在受控的 artifact storage 中,由脚本负责下载和恢复,然后应用尚未包含在 backup 中的 migrations。backup 属于二进制文件,也可能包含敏感数据,通常不适合直接提交到 Git。

Docker 能解决什么

Docker 可以统一 SQL Server 的版本、配置和启动方式。不过,数据库 volume 默认会保留旧 schema,仅将 SQL Server 放入 container,仍然无法解决 branch 切换时的 schema 差异。

容器可以按可丢弃环境管理:

docker compose down --volumes
docker compose up -d

dotnet ef database update \
  --project src/App.Infrastructure \
  --startup-project src/App.Web

SQL Server image 只负责提供数据库引擎。当前代码中的 migrations 由应用启动脚本、开发脚本或单独的 migration job 执行。

不建议每次 schema 变化都构建新的 SQL Server image。这样会混合管理 migration 历史、数据库数据和基础运行环境,image 体积、缓存和数据安全也会更难控制。

是否应该把 database data file 放进 repository

通常不应该,原因如下:

  • .mdf 和 .ldf 是二进制文件,Git 无法有效比较或合并。
  • 数据文件可能依赖特定的 SQL Server 版本、实例配置和文件路径。
  • 多个 branch 修改同一个数据文件时,无法进行有效的冲突解决。
  • repository 会迅速膨胀。即使之后删除文件,Git 历史中仍会保留相应对象。
  • 数据文件可能夹带账号、令牌、个人信息或其他敏感数据。
  • Git 能够切换文件,并不代表 SQL Server 可以安全地 detach、替换并重新 attach 正在使用的数据库。

Git 应保存 migration 源文件、必要的 SQL scripts、数据库项目文件和 seed 定义,不应保存正在运行的数据库文件。

可以进一步采用每个 branch 一个数据库

如果经常在 feature branches 之间切换,而且重建数据库的成本较高,可以为每个 branch 创建独立数据库,例如:

App_main
App_feature_customer_status
App_bugfix_order_total

启动脚本可以根据当前 branch 生成清理过的数据库名,并将其写入开发连接字符串。首次使用该 branch 时执行:

dotnet ef database update

这种方式可以保留各个 branch 的本地状态,但需要定期清理废弃数据库,同时处理数据库名称长度和非法字符问题。它无法替代 migration rebase。合并前仍要确认 feature migration 基于最新 main 生成。

团队规则建议

团队可以统一采用以下规则:

  • migration 与引发 schema 变化的代码一起提交。
  • migration 必须进入 Pull Request 并接受评审。
  • 合并前同步最新 main。如果基线发生变化,在 feature branch 内重新生成 migration。
  • 已进入共享环境的 migration 不再修改,后续问题通过新的 migration 修正。
  • 本地 schema 异常时,优先运行统一的 reset 脚本,不手工修改 tables。
  • CI 从空数据库开始执行全部 migrations,确认整条 migration 链可以完整运行。
  • migrations、SSDT/DACPAC 等方案只选一个作为 schema 的主要发布来源,避免两个工具同时修改同一套 schema。
  • Docker 提供一致的数据库引擎,migrations 定义 schema。

版本库中的 migration 链决定数据库的正确状态。本地数据库只是可以随时重建的运行结果,无需跟随 Git branch 一起保存。

备注:内容仅供参考。