三个命名的迁移版本按 v1、v2、v3 顺序前进。已经应用的历史必须保持不变;本地模型记录版本与名称,不记录可执行内容的摘要。

迁移是可版本化的 schema 变更

数据库的 schema 不是一次性的设计;它随使用它的软件一起演进。与其临时编辑 schema,不如用迁移应用一组命名、有序的变更,让任何环境都能重放。 把 schema 当代码 把 schema 当源代码一样对待:每一条迁移都是一个带顺序、带编号、被命名的小步骤,把库从一个状态推进到下一个状态,整个序列都收进仓库。全新的库从空库开始重放这些步骤直到当前版本,这正是测试建库、临时环境克隆、安装新机器的统一口径;没有人再对着线上实例随手敲一条 ALTER TABLE。schema 也因此从「某个实例才知道的事实」变成有历史、可 review、有 diff 的正式产物。 顺序就是契约 在起始结构相同、引擎兼容、所需扩展齐备且步骤确定的前提下,有序重放才能复现预期结构。只有顺序相同,不能消除环境差异或依赖已有数据的变化。这种可预测性就是契约:它让测试能创建全新数据库、让暂存能克隆,让运维升级时不用靠猜。随之而来的铁律是——凡是已经被任何环境执行过的迁移,都不得重写或重排,因为执行器依靠已记录的身份决定哪些步骤已经执行;这种身份不一定包含内容校验和。篡改历史可能造成环境分叉,需要检查实际状态,再通过明确的前向修复恢复一致。 危险在不可逆 删除列或表的迁移一旦提交,就会丢失所存数据;只重建结构不能找回原值,必须谨慎对待。给它们配一张清单:先备份,把 drop 拆成单独的后续迁移,这样能先回滚前面的 schema 改动,最后再面对不可逆的破坏。真正的纪律是「只能向前、但留有退路」——绝不要写一条把维护者锁在门外的迁移。上线前一定要在 staging 上跑一遍这类破坏性迁移,让失败模式在真正投入生产前就变成一件平淡无奇的事。 坏掉的样子 最贵的一种坏法,是把不可逆迁移直接送上生产。在 PostgreSQL 里,删除列不需要重写表,但仍需取得 ACCESS EXCLUSIVE 锁。许多类型转换或新增带易变默认值的列需要重写;符合条件的二进制兼容转换可以免去重写,却不会因此免去所需的锁及等待时间。大型重写可能长期等待或运行。PostgreSQL 的事务性 DDL 失败后可以回滚;是否留下部分结果,取决于先前提交、非事务步骤与执行器事务边界,不能把超时一概理解为半张表已经改好。而 down 迁移并不能撤销数据丢失:删掉邮箱列再加回来,得到的是空列,而不是被删掉的地址。破坏性步骤应当拆成独立的后续迁移,上线前先备份,优先使用扩张再收缩的顺序,先加新结构,再回填数据,最后删除旧结构,让每一次发布都平淡无奇。换句话说,只要迁移涉及删数据,就默认它回不来。 另外两类经典故障是并发双跑和篡改历史。如果两个发布副本在没有任何锁的情况下同时执行迁移,它们可能重复应用同一版本,也可能把版本顺序搅乱。生产级的 runner 因此只允许单个执行者,或者借助 PostgreSQL 咨询锁之类的机制加锁,而 golang-migrate 在失败后会把库标记为 dirty,而不是假装一切正常。重写已经被应用过的迁移,或者合并了两个各自新增版本 7 的分支,都会让历史分叉:环境可能分叉。还要检查执行器到底保存什么:golang-migrate 的 PostgreSQL 账本保存版本与 dirty 状态,并不为每份文件保存内容校验和。先核对实际状态,再设计前向修复,不能假定工具自动发现所有历史编辑。strong_migrations 这类工具会在执行迁移时检查危险操作;应先在开发环境运行检查,再安排部署,不能把它当成自动覆盖所有风险的静态审查。 ...

一月 24, 2027 · 4 分钟 · XB Field Notes