Django 数据库迁移的安全姿势:改表、改名与两段式上线

作者:程序员白大力 · 2026-09-29 18:40
Django 数据库迁移的安全姿势:改表、改名与两段式上线

改一个模型字段,makemigrations 生成文件,migrate 一跑,开发机上秒过。于是很多人把迁移当成「把模型同步到数据库的一条命令」,直到某天在几十万行的表上改了字段类型,迁移卡住整站报错;或者跟着教程随手 --fake,从此迁移历史和真实表结构对不上,之后每次 migrate 都在盲跑。

迁移框架确实把脏活干了,但「它替你干活」不等于「随便跑都安全」。这篇文章把 Django 迁移里最容易被低估的几件事理清:哪些改动便宜、哪些昂贵、哪些不可逆,以及上线时代码和表结构该谁先动。

一、先认清约束:迁移基本是单向门

第一件事,别把「能回滚」当兜底。 migrate 确实支持退回上一个迁移,但前提是迁移文件里写了反向操作。删列这种操作一旦执行,数据当场就没了——所谓回滚最多把列的骨架加回来,数据不会跟着回来。表结构可以重建,业务数据重建不了。

第二件事,事务保护因数据库而异。 Django 官方文档写得很明确:SQLite 和 PostgreSQL 支持事务性 DDL,默认整个迁移跑在一个事务里,中途失败自动回滚,库停留在迁移前的状态;MySQL 和 Oracle 不支持,迁移跑到一半失败,会留下需要手工清理的半成品结构。同样是「迁移失败」,两种库的善后成本完全不同。

由此得出动手前的三条底线:生产库先备份、用 sqlmigrate 预览这条迁移到底执行什么 SQL、最好在一份真实数据量的副本上先跑一遍。sqlmigrate 只打印不执行,成本为零。

二、每个操作的真实成本不一样

同样是「改模型」,落到数据库上的动作差别巨大:

操作 数据库实际动作 成本与风险
加一列(可空) ADD COLUMN 瞬时完成
加列带默认且 NOT NULL 取决于数据库版本,老版本可能全表重写 大表上可能锁表
字段改名 RENAME COLUMN,只改元数据 几乎免费
改列类型、收紧长度、加约束 重写表或全表扫描校验 与行数成正比
删列 数据立即丢失 不可逆

PostgreSQL 从 11 起,加列带一个固定默认值不再触发全表重写,默认值记在系统目录里按需返回,官方文档明确说明这让大表上的 ALTER TABLE 变得很快;但易变默认值(比如取当前时间戳的表达式)仍然要逐行回填。

SQLite 是另一个极端。Django 官方文档原文:SQLite 几乎没有内置的表结构修改支持,Django 通过「用新结构建新表 → 拷贝数据 → 删除旧表 → 改名」来模拟。也就是说,除了加列、改名这类轻量操作(SQLite 3.25 起原生支持 RENAME COLUMN),大多数字段变更在 SQLite 上都会触发整表重建。小库无感,表一大就得掂量时机了。

所以拿到一个自动生成的迁移,别急着跑——先想想它对应上表里的哪一行。

三、上线顺序:新旧代码要同时兼容一张表

滚动发布或重启的窗口期里,旧代码和新代码会同时连着同一个库。这决定了迁移上线的基本纪律:每一版表结构,都要同时兼容正在运行的每一版代码。破坏性变更不能和依赖它的代码同一次上线。

标准做法是把改动拆成「先加后删」的两段式:

第一段(expand):只做加法
  加新列(先不加 NOT NULL)→ 发迁移,不改业务逻辑
第二段:发新代码
  新代码读新列、写新列,旧代码继续用旧列,互不干扰
第三段(contract):确认旧代码全部下线后
  再发「删旧列、补 NOT NULL」的迁移

为什么这么拆:任何一段出了问题,回滚到上一段都不丢数据、不停服;而「一个迁移里又改名又删列、和代码一起上」的做法,等于把新旧代码的兼容窗口压成了零。

顺带纠正一个常见误区:如果只是字段改名,数据库层面直接用 RenameField 就好——它生成的是 RENAME COLUMN,数据原封不动。真正需要两段式的是「删列」和「改类型」这类高风险变更,而不是改名本身。

四、三个高级命令的真实定位

--fake 是给高手用的危险开关。 官方文档的原话是:使用 --fake 有把迁移状态表置于需要手工恢复才能正确运行的风险。它不做任何检查,只往 django_migrations 表里记一笔「已应用」,数据库一个字节都不动。合理场景只有一种:你手工改过表结构,需要把历史补登记。除此之外用它,都是在给未来的自己埋雷。

存量库接入迁移体系,用 --fake-initial。 它是带检查的版本:先核对初始迁移要建的表是否都已存在,存在才标记跳过,否则正常执行。把一个已有表结构的老库纳入 Django 管理时,这是正确入口。

squashmigrations 要分两次发布。 几百个迁移确实该压扁,但官方推荐流程是:先 squash 并保留旧迁移文件、发布一次;等所有环境都升级到包含 squash 结果的版本之后,再删除旧文件、做第二次发布。压缩完立刻删旧文件,等于宣布「所有环境的库结构已经完全一致」——这话在上线当天说,通常为时过早。

结尾

迁移框架的核心价值不是「自动改表」,而是一份可重放的、与真实库结构一致的变更历史。所有省事的做法——手改已应用的迁移文件、不做检查的 --fake、squash 完立刻删旧文件——省下的都是几分钟,赔掉的是这份历史本身。下次改字段之前,先问自己一句:这个操作落到数据库上到底是哪一行成本,新旧代码在窗口期各看到什么。

如果这篇对你有用,欢迎转发给那个准备在周五下午跑迁移的同事。

参考文章

  • 《Django 官方文档:Migrations(后端支持、事务、squashing migrations)》
  • 《Django 官方文档:django-admin 与 manage.py(--fake 与 --fake-initial)》
  • 《PostgreSQL 官方文档:Modifying Tables》
  • 《Zero-Downtime Database Migrations: The Expand/Contract Pattern》
  • 《MicroPyramid:How to Create Initial Django Migrations for an Existing Database》
本文由 AI 辅助生成并经人工审核发布,内容仅供参考。