数据库迁移脚本写成"一次性炸弹":我因为没给 `down()` 留退路,回滚时亲手删了生产环境

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 70 浏览 0 回复

上周帮一个老项目做版本升级,迁移脚本跑完才发现字段类型写错了。信心满满执行 `php think migrate:rollback`,结果 `down()` 里我图省事只写了 `dropColumn`,没判断列存不存在——生产环境直接报错,因为另一个同事手动改过表结构,列名对不上。回滚失败,数据还在,但迁移记录乱了,后面的人根本不敢再动。

从那以后我给自己定了三条铁律,分享出来给同样手贱的站长:

一、建表时 `create()` 里藏个 "后悔药"

很多人写 `up()` 很认真,`down()` 就敷衍。我现在强制要求:凡是 `create()` 的表,`down()` 必须完整 `drop()`,但加一层判断。ThinkPHP 的迁移基于 Phinx,可以直接用:

`$this->table('user_extra')->exists() && $this->table('user_extra')->drop();`

别嫌啰嗦。去年有个项目迁移文件堆积到 80 多个,中间某次 `rollback` 跳过失败节点,后面全串了。`exists()` 就是防这种"历史烂账"的。

二、改字段用 `change()`,但先 `save()` 再改

MySQL 里 `VARCHAR(255)` 改 `VARCHAR(100)` 如果已有超长数据会炸。我现在的习惯是:修改前先 `save()` 当前结构快照,或者至少把 `change()` 拆成两步——先新增临时字段做数据清洗,再删旧字段。别信 "MySQL 8 很智能",生产环境字符集、排序规则、行格式随便一个差异都能让 `change()` 锁表十分钟。

宝塔面板里看 "当前 MySQL 线程" 突然飙到 50+,大概率就是这种 ALTER 堵的。

三、卸载插件时,迁移记录比表更重要

我们自己写了个 CMS 插件系统,早期设计是卸载时直接 `DROP TABLE IF EXISTS`。看起来干净,实际埋雷:用户 A 装了插件建表,用了一段时间卸载,表没了但 `phinxlog` 里的迁移记录还在。再重装,迁移脚本判断 "已执行" 直接跳过,`up()` 里的新字段加不进去,用户以为装好了,实际是个残血表。

现在的做法:卸载走独立 `uninstall()` 方法,只清业务数据,表结构和迁移记录保留。真要彻底干净,单独给 "清除迁移历史" 的二次确认,且必须输站点名防误触。

---

最后说个血泪细节:迁移脚本千万别写 `DB::raw` 裸 SQL,尤其是带 `IF NOT EXISTS` 的。Phinx 的 `hasTable()` 和 MySQL 原生的判断在大小写敏感配置下结果可能相反,Linux 生产环境和 Windows 开发环境表现不一致。我被这个坑过两次,现在全部走 ORM 方法链,慢就慢点,至少行为可预期。

你们迁移脚本里踩过什么 "回滚即爆炸" 的坑?

评论0
回复 · 0
还没有回复
微信客服 微信客服