1. 迁移机制与底层原理
在 Django 中,ORM(Object-Relational Mapping)是连接模型(Model)和数据库结构的桥梁。Django 鼓励开发者通过编写 Python 类(模型)来定义业务数据结构,而不是手动创建数据库表。当模型发生变化时,如何将这些变化安全地同步到数据库?这就是 迁移系统(Migration System) 的职责。
Django 提供了两个核心命令来实现这一机制:
-
makemigrations
:自动检测模型变更并生成迁移文件 -
migrate
:将迁移文件中的变更应用到数据库中
什么是迁移(Migration)?
“迁移”是 Django ORM 用来管理数据库 schema(结构)变化的机制。它的核心思想是:
以版本化脚本的形式记录模型的每次变更,并确保这些变更可以一步步地、按顺序、安全地同步到数据库结构中。
每一个迁移文件(如 0001_initial.py
)都表示一次变更的“快照”和“指令集”,包含:
-
变更内容(如新增字段、删除模型)
-
依赖关系(如依赖某次迁移)
-
可执行的操作序列(operations)
迁移常见命令
命令 | 说明 | 应用场景 |
---|---|---|
python manage.py makemigrations | 自动检测模型变更并生成新的迁移文件(记录变更指令) | 模型新增字段、修改字段、删除模型等操作后使用 |
python manage.py migrate | 执行迁移文件中的操作,应用到数据库 | 本地或生产环境数据库更新结构时使用 |
python manage.py showmigrations | 显示所有 app 的迁移文件及其执行状态 | 检查当前迁移执行情况,调试迁移问题 |
python manage.py sqlmigrate <app> <migration> | 查看某次迁移将执行的 SQL 语句 | 审查实际执行的 SQL,避免意外删除数据 |
python manage.py migrate <app> zero | 将指定 app 的所有迁移回滚(还原到未迁移状态) | 重置数据库结构,仅用于测试或初始化 |
python manage.py migrate <app> <migration_name> | 回滚或迁移到某个特定迁移版本 | 控制迁移步骤,手动回滚或追踪问题 |
python manage.py makemigrations --empty <app> | 生成一个空迁移文件(无自动检测变化) | 手动编写迁移(如 RunPython 、RunSQL )的入口 |
python manage.py makemigrations --merge | 合并多个冲突的迁移文件 | 分支合并后出现迁移冲突时使用 |
python manage.py migrate --fake <app> <migration> | 标记某个迁移为“已执行”,但不真正执行操作 | 数据库已手动修改,跳过实际执行,仅记录状态 |
python manage.py migrate --fake-initial | 如果数据库中已有初始结构,跳过初始迁移操作 | 数据库结构存在但未记录迁移时用于初始化 |
--fake和--fake-initial区别:
-
--fake
:标记迁移为已执行,但不实际执行数据库操作。常用于手动修改数据库结构后,同步 Django 记录。 -
--fake-initial
:只对初始迁移(initial)文件起作用。若数据库中已存在对应表结构,会跳过执行迁移,仅标记为已迁移。
--fake
是无条件标记为已迁移,--fake-initial
是检测到表已存在时标记为已迁移,不重复创建表,如果表不存在,照常创建表
迁移命令背后的流程
makemigrations
工作流程
-
检测模型变化:Django 会加载当前所有模型定义,和上一次迁移文件中的“历史模型状态”进行对比。
-
生成变更指令:根据差异生成操作列表(如
AddField
,RemoveField
,AlterField
等)。 -
保存为迁移文件:把这些操作写入新的迁移文件中,路径通常是:
yourapp/migrations/000X_*.py
Django 会在迁移文件中维护一个 state_operations
来表示“模型结构”,用于后续比对。
migrate
工作流程
-
读取数据库中已执行的迁移记录:Django 会读取特殊表
django_migrations
-
找出需要执行的迁移:即本地 migrations 文件夹中存在,但数据库未记录执行的迁移
-
逐个执行:迁移文件中的
operations
会被逐条执行(如 ALTER TABLE),并记录到django_migrations
django_migrations
表的作用
该表是 Django 用于记录“数据库已完成哪些迁移”的关键数据结构,字段包含:
字段名 | 说明 |
---|---|
app | 应用名 |
name | 迁移文件名(不含后缀) |
applied | 应用时间 |
这张表 并不代表数据库当前结构,它仅仅记录哪些迁移已被执行。数据库结构的真实状态取决于这张表 + 迁移脚本 + 数据库执行结果的三者一致性。
迁移底层的 Operation
系统
每个迁移文件的 operations
是一个指令列表,继承自 django.db.migrations.operations.base.Operation
,常见子类包括:
-
CreateModel
:创建模型 -
AddField
/RemoveField
:字段变更 -
AlterField
:字段属性修改 -
RunPython
:执行自定义 Python 代码(如数据迁移) -
RunSQL
:执行原生 SQL(如重命名表等)
这些操作通过 MigrationExecutor
进行调度和执行,内部依赖 SchemaEditor
进行跨数据库后端兼容处理。
2. 常见错误分类与解决方案
Django 的迁移系统虽然强大,但在实际开发中,尤其是多人协作、频繁模型变更的场景中,迁移相关的报错比较常见。以下是开发中出现的问题及解决建议记录:
❌ 1. table already exists
表已存在
django.db.utils.OperationalError: (1050, "Table 'xxx' already exists")
常见场景
你尝试运行migrate,报错说某张表已经存在。这通常出现在以下几种开发/部署场景中
-
你手动在数据库中创建过表(如写了原生 SQL)
-
你还原了生产环境数据库结构,但本地仍执行初始迁移
-
某个 app 的迁移记录丢失或未提交,但数据库中的表已经存在
问题本质:
Django 迁移系统默认假设自己会“从无到有”创建所有表。如果你跳过了创建记录、手动建了表或还原了旧数据,它在执行 CreateModel
操作时会检测到表已存在,从而报错。
换句话说:数据库中已经有表了,但
django_migrations
表中没有记录迁移已执行。
✅ 解决方案:使用 --fake-initial
Django 为此场景提供了专门参数:
python manage.py migrate --fake-initial
含义是:如果数据库中已经存在初始表结构,就跳过初始迁移文件的执行,但仍然将迁移记录写入 django_migrations
,以免后续迁移报错。
仅适用于“数据库中已有表结构”且“你确定结构与模型一致”的情况。
❌ 2. is applied before its dependency
迁移历史不一致
错误信息:
django.db.migrations.exceptions.InconsistentMigrationHistory:
Migration <app>.<migration_name> is applied before its dependency <other_app>.<migration_name> on database 'default'.
常见场景:
将测试环境或生产环境中的完整数据库备份(含数据和表结构)导入本地开发环境后,执行
migrate
命令时抛出该错误。
本地和远程环境的迁移文件可能一致,但数据库中 django_migrations
表中的记录已经“走在前面”或出现不一致。
错误本质:
Django 通过 django_migrations
表记录哪些迁移已经在数据库中执行过。
但在某些情况下,如从测试环境复制数据库后:
-
数据库结构已经更新到较新的状态(如执行了
0005_auto_xxxx
) -
本地代码中的迁移文件还停留在较早状态(如只有到
0003
)
当你在本地执行 migrate
时,Django 会发现 数据库中“先执行”了某个迁移,但它依赖的迁移在本地尚未执行,于是抛出 InconsistentMigrationHistory
异常。
通用解决方案(推荐用于开发/测试环境)
清空迁移记录,让 Django 以
--fake
的方式重新记录当前状态,而不实际执行数据库操作
步骤如下:
-
清空
django_migrations
表:⚠️ 此操作不会影响业务数据,仅清除迁移记录。
-- MySQL / PostgreSQL:
TRUNCATE TABLE django_migrations;-- SQLite:
DELETE FROM django_migrations;
-
确保本地代码迁移文件完整、和数据库结构对应:
✅ 推荐:使用与数据库导出时相同的 Git 分支代码,并保留
migrations/
目录。 -
重新 fake 所有迁移(跳过实际执行,仅登记记录):
python manage.py migrate --fake
🔐 注意事项:
-
不要在生产环境这样操作,否则迁移记录丢失,后续无法增量执行迁移。
-
清空
django_migrations
后,数据库和模型之间的状态需完全一致,否则你跳过的是未正确应用的变更,可能引发更严重的问题。
❌ 3. non-nullable field without a default
添加非空字段时执行makemigrations报错
错误信息:
You are trying to add a non-nullable field 'xxx' to <Model> without a default
常见场景:
在已有数据的模型中新增一个字段,未设置null(默认为null=False
)且未指定默认值:
# 原有模型
class Product(models.Model):name = models.CharField(max_length=100)# 修改后(会报错)
class Product(models.Model):name = models.CharField(max_length=100)status = models.IntegerField() # 新增字段未指定 default
原因分析:
数据库中的旧数据没有该字段,Django 不知道用什么值填充现有记录。
解决方案:
方法一:设置默认值
status = models.IntegerField(default=0)
方法二:允许为空
status = models.IntegerField(null=True, blank=True)
方法三:在迁移时指定默认值(Django 提示时输入)
方法四:使用 RunPython
手动填充旧数据,再移除默认值(推荐用于正式项目)
❌ 4. Duplicate column name
字段重复导致migrate失败
错误信息:
django.db.utils.OperationalError: (1060, "Duplicate column name 'xxx'")
常见场景:
迁移文件被误删或修改,模型与数据库结构不一致,重新执行迁移导致字段重复。
原因分析:
数据库中已有字段,但迁移记录(django_migrations
)中并未记录此字段的创建,Django 尝试重新创建列。
解决方案:
-
核对数据库中实际结构和模型定义,确认字段是否已存在
-
避免手动修改数据库结构;如果修改,必须确保迁移状态同步
-
使用
migrate --fake
标记迁移已执行,跳过实际操作:
python manage.py migrate yourapp 0001 --fake
❌ 5. Unknown column 'xxx' in 'field list'
删除字段后未清理数据依赖导致迁移失败
错误信息:
django.db.utils.OperationalError: (1054, "Unknown column 'xxx' in 'field list'")
常见场景:
删除模型字段后,项目代码或数据迁移脚本中仍有对该字段的引用。
原因分析:
字段已从模型中移除,但数据库中仍存在引用字段的旧代码或旧 SQL。
解决方案:
-
全局搜索项目代码中是否还有
xxx
字段的使用 -
若使用
RunPython
写了数据迁移脚本,需改为先清数据后删字段 -
对于依赖字段的查询逻辑,也要在迁移之前调整逻辑
❌ 6. conflicting migrations
多人协作导致makemigrations冲突
错误信息:
Conflicting migrations detected; multiple leaf nodes
常见场景:
两名开发者分别在不同分支对同一个 app 做了迁移,合并后造成迁移分叉。
原因分析:
迁移文件是线性依赖的,两个迁移文件若没有互相引用,就形成冲突。
解决方案:
使用 --merge
合并迁移:
python manage.py makemigrations --merge
Django 会生成一个新的迁移文件,合并两个冲突迁移(开发者需判断操作顺序)。