django 3.2 数据迁移必须分离结构与数据操作,添加非空字段、重命名、拆分/合并字段、类型转换、跨模型迁移等场景需手动编写 runpython/runsql;务必用 --empty 创建迁移文件、分页处理大数据、注意数据库差异及 atomic=true 保证原子性。

在 Django 3.2(LTS)中安全迁移 SQL 数据,核心是「结构迁移」和「数据迁移」必须分离处理,且不能依赖 makemigrations 自动生成数据逻辑——它只管 schema,不管数据一致性。
什么时候必须写自定义数据迁移而不是靠 makemigrations
Django 的 makemigrations 只检测模型定义变更,不会为你生成数据转换逻辑。以下场景必须手动写数据迁移:
- 添加非空字段(
CharField、IntegerField等)且已有数据表不为空时,必须提供默认值或用null=True+ 后续填充 - 重命名字段或模型后需保留历史数据(
RenameField不迁移已有值,仅改名) - 拆分/合并字段(如把
full_name拆成first_name和last_name) - 转换字段类型(如
TextField→JSONField)且需解析/重构内容 - 跨模型数据聚合或迁移(如把 User 的 profile 数据迁入新
Profile模型)
用 makemigrations --empty 创建可编辑的数据迁移文件
不要直接修改自动生成的迁移文件(容易被下次 makemigrations 覆盖),而是用空迁移作为数据操作载体:
运行:python manage.py makemigrations --empty myapp
生成的文件(如 migrations/0002_data_migration.py)里会包含空的 operations = []。你需要手动填入 RunPython 或 RunSQL 操作:
from django.db import migrations
def populate_new_field(apps, schema_editor):
MyModel = apps.get_model('myapp', 'MyModel')
for obj in MyModel.objects.filter(new_field__isnull=True):
obj.new_field = f"computed_{obj.id}"
obj.save()
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.RunPython(populate_new_field, reverse_code=migrations.RunPython.noop),
]
注意:apps.get_model() 是关键——它加载迁移时刻的模型快照,避免因当前 models.py 已改动导致运行失败。
MySQL / SQLite 上执行数据迁移的致命陷阱
不同数据库对数据迁移的容忍度差异极大,直接影响你是否敢在生产环境跑 migrate:
- MySQL:无事务保障的 DDL(如
ADD COLUMN)一旦中断,表可能处于半损坏状态;RunPython中的批量更新若没分页,极易锁表超时 - SQLite:整个迁移过程会复制整张表,百万级数据可能耗时数分钟且期间不可读写;
RunSQL若含多语句(分号分隔),Django 默认只执行第一条 - PostgreSQL:相对最稳,但若在
RunPython中执行未加select_for_update()的更新,仍可能引发并发脏写
应对方式:
- 对大表数据迁移,务必在
RunPython中手动分页(例如每次处理 1000 行) - 在 MySQL 上避免
RunSQL执行 DDL;优先用RunPython+ 原生cursor.execute() - SQLite 生产环境慎用数据迁移;建议先导出为 JSON,停机后用脚本重建
migrate --fake 和 --fake-initial 不是“跳过”,而是“声明已就绪”
这两个参数常被误用为“绕过迁移”,实际作用是让 Django 记录状态,而非跳过执行:
-
migrate myapp 0001 --fake:告诉 Django “这个迁移已人工执行,请标记为完成”,适用于已有旧库需接入 Django 迁移体系 -
migrate --fake-initial:仅对初始迁移(0001_initial)有效,前提是当前数据库表结构与该迁移完全一致,否则后续迁移会出错
误用后果:Django 迁移记录与真实 DB 状态脱节,下次 makemigrations 可能生成错误的 diff,甚至破坏依赖链。
最易被忽略的一点:Django 3.2 的 RunPython 函数默认不进事务(除非显式加 atomic=True),而数据迁移往往需要原子性。漏加 atomic=True 会导致部分数据写入成功、部分失败,且无法回滚——这比迁移失败本身更危险。











