迁移文件必须从 clean main 分支生成,禁止在未 rebase 的 feature 分支上生成;up() 和 down() 必须可逆且幂等,破坏性操作需拆步实现;生产迁移须人工审批执行,禁止 ci 自动运行。

迁移文件必须从 clean main 分支生成
多人并行开发时,如果开发者直接在 feature 分支上运行 php bin/console make:migration,Doctrine 会基于当前分支的实体状态与数据库结构做 diff —— 而这个数据库很可能只包含你本地或 CI 环境中旧版 schema。结果就是生成的迁移文件误判“新增字段”,实际却是他人已提交但尚未合入 main 的变更。
正确做法是:每次生成迁移前,先确保本地 main 分支已同步最新代码,并用生产环境镜像(或至少是 shared dev DB)执行 php bin/console doctrine:schema:update --dump-sql 验证无差异。再切回你的功能分支,仅运行 php bin/console make:migration --no-interaction,避免交互式提示干扰 CI 流程。
- 禁止在未 rebase 的 feature 分支上生成迁移
- CI 流水线中应加入检查:
git diff main -- migrations/非空则报错,强制人工确认 - 迁移文件名中的时间戳(如
Version20251002062800)不是“创建时间”,而是“预期执行顺序”——它依赖 Git 提交时间戳排序,所以必须保证提交时间合理
up() 和 down() 必须可逆且幂等
Doctrine 迁移不保证 down() 能 100% 恢复数据,尤其涉及 DROP COLUMN、ALTER TABLE ... CHANGE 或类型收缩(如 VARCHAR(255) → VARCHAR(50))。一旦执行 down(),丢失的数据无法找回。
真正安全的做法是:把破坏性操作拆成多步迁移,并在 up() 中显式保留旧字段、迁移数据、再删旧字段;down() 只回退到中间状态,而非原始状态。例如添加非空字段时:
- 第一步迁移:加新字段(允许 NULL)、填充默认值
- 第二步迁移:改新字段为 NOT NULL
-
down()只需删字段,不碰旧字段逻辑
别依赖 Doctrine 自动生成的 down() —— 它常是反向 SQL 拼接,对复杂业务逻辑毫无意义。
生产环境迁移必须走审批 + 手动确认流程
哪怕配置了 --no-interaction,也绝不能让 CI 自动在 production 环境执行 php bin/console doctrine:migrations:migrate。线上迁移失败可能锁表、阻塞请求,甚至因事务失败导致部分 schema 更新而中断。
团队应约定硬性规则:
- 所有 prod 迁移命令必须由 DBA 或指定运维人员在跳板机手动执行
- 执行前需比对
php bin/console doctrine:migrations:status --show-versions输出,确认目标版本无跳过、无冲突 - 关键迁移(如重命名表、大字段修改)必须附带回滚预案 SQL 脚本,存于
docs/migrations/并经两人签字
Doctrine 的 --dry-run 参数只能看 SQL,不能代替人工 review —— 它不校验外键依赖、触发器、视图是否失效。
迁移文件不能含业务逻辑或外部依赖
有人会在 up() 里调用服务类、发 HTTP 请求、读写文件 —— 这会导致迁移在不同环境行为不一致,比如本地能连通第三方 API,但生产环境因网络策略失败;或 migration 目录被部署为只读,fopen() 直接抛异常中断整个迁移链。
迁移的本质是 DDL + 安全的数据搬运,不是业务脚本。若真需处理数据:
- 用原生 SQL 或
$schema->getConnection()->executeStatement() - 避免使用 EntityManager —— 它依赖完整容器,而 migration 执行时容器未必加载完成
- 绝对不要在迁移中调用
$this->getContainer()或任何 service locator
最隐蔽的坑是:某些 IDE 插件会自动补全 $this->getDoctrine()->getManager() —— 看似方便,实则埋雷。











