1. 项目概述:两个命令,一条数据演进的生命线
刚接触 Django 的时候,我对着终端敲下
python manage.py makemigrations
和
python manage.py migrate
,心里其实挺没底的——这俩命令到底在干啥?为什么有时候只跑一个就报错,有时候又得连着跑?有次我在本地改完模型字段长度,兴冲冲
makemigrations
生成了文件,结果忘了
migrate
,第二天上线前测试发现数据库里字段还是老样子,页面一提交就报
DataError
,硬是卡在发布前两小时手忙脚乱回滚。这种“知道要跑,但不知道为什么跑、什么时候该停”的状态,恰恰是 Django 迁移机制最常被误解的起点。
简单说,
makemigrations
是
写剧本
,
migrate
是
演戏
。剧本(migration 文件)描述了“数据库结构该怎么变”,而演戏(执行 migrate)是把剧本真正落地到数据库里,让表结构、索引、约束这些物理存在跟着剧本走。它们不是可互换的开关,而是严格串行的两个阶段:没有剧本,演戏无从谈起;写了剧本却不演,舞台永远停留在旧布景。这个逻辑背后,是 Django 对“代码即契约”原则的坚守——模型类定义的是你期望的数据形态,迁移文件是这份契约的法律存证,而 migrate 操作则是法院强制执行的过程。它不关心你本地数据库当前长什么样,只认迁移历史记录里的“已执行”状态。所以当你看到 “You have 3 unapplied migrations” 这种提示,Django 其实是在说:“你的代码契约已经签了三份新协议,但数据库还没盖章生效。”
这篇文章面向所有正在用 Django 做真实项目的开发者,无论你是刚写完第一个
models.py
的新人,还是带团队维护十年老项目的架构师。如果你曾因迁移失败导致线上服务中断、因回滚失误丢失测试数据、或在多人协作时被同事的 migration 文件搞懵过,那这篇就是为你写的。它不讲抽象理论,只拆解真实场景下的每一步操作意图、每个参数背后的权衡、每次报错背后的数据状态真相。接下来的内容,全部来自我过去八年在电商中台、SaaS 后台、政务系统等不同规模项目里踩过的坑、记下的日志、和凌晨三点对着
django_migrations
表反复核对的实战经验。
2. 核心设计思路:为什么必须分两步?——从“代码-数据库”一致性保障说起
2.1 本质差异:静态描述 vs 动态执行
makemigrations
的核心任务,是
对比当前模型定义与数据库当前状态,生成一份可版本化、可审查、可回溯的变更描述文件
。它不碰数据库一根手指头,纯粹是个“文本生成器”。它读取
models.py
,再查一遍
django_migrations
表里已执行的迁移记录,最后计算出“从当前数据库快照出发,要达到模型定义的新状态,需要执行哪些 DDL 操作”。这个过程完全离线,甚至可以在没有数据库连接的环境下运行(比如 CI 流水线里预生成迁移文件)。我见过最典型的误用,是有人在部署脚本里直接
makemigrations && migrate
一把梭,结果在生产环境因为权限不足或网络问题卡在
makemigrations
阶段,整个发布流程就挂了——这恰恰暴露了对命令本质的误解:
makemigrations
不该出现在生产部署环节,它属于开发阶段的“契约签署”行为。
migrate
则完全不同,它是
数据库的实时操作引擎
。它会读取
django_migrations
表,确认哪些迁移文件尚未标记为“已执行”,然后按顺序逐条执行其中的
operations
(比如
AlterField
、
CreateModel
),同时在
django_migrations
表里插入一条新记录,表示“此迁移已完成”。这个过程是原子性的:如果某条 SQL 执行失败(比如给一个已有百万数据的字段加非空约束),整个迁移事务会回滚,
django_migrations
表也不会新增记录,确保数据库状态不会停留在“半完成”的危险中间态。这也是为什么 Django 要求数据库必须支持事务(PostgreSQL、MySQL 的 InnoDB 引擎都满足),因为迁移的可靠性,就建立在这层事务保障之上。
提示:
makemigrations生成的.py文件,本质是 Python 代码,里面包含operations列表和dependencies字段。你可以像读普通 Python 文件一样打开它,看到AlterField(model_name='user', name='email', field=models.EmailField(...))这样的语句。它不是 SQL,而是 Django 自己的迁移操作 DSL,由 Django 在migrate时翻译成对应数据库的 SQL。这意味着同一个迁移文件,在 PostgreSQL 和 MySQL 上生成的最终 SQL 可能不同,但语义完全一致。
2.2 设计哲学:可预测性优先于便利性
Django 选择分两步,根本上是为了
牺牲一点“快捷”,换取十倍的“可控”
。试想如果只有
migrate
一个命令:每次运行时,它既要扫描模型变化,又要决定是否生成新迁移,还要执行变更——那在团队协作中就会变成灾难。A 同学改了
User.email
字段长度,B 同学同时改了
Order.status
的 choices,两人各自
migrate
,很可能生成两份冲突的迁移文件,或者更糟:其中一人
migrate
时,Django 发现模型有变化,自动创建了一个匿名迁移,而另一个人的代码里根本没有这个文件,导致本地环境和 CI 环境数据库状态彻底不一致。
分两步的设计,把“决策点”明确地放在了开发者的手中:
-
makemigrations是 人工决策时刻 :你看到Migrations for 'users':的输出,就知道“哦,这次改确实影响了数据库结构,我得检查下生成的文件对不对”; -
migrate是 确定性执行时刻 :你确认迁移文件没问题后,才让它去动数据库,而且执行过程完全可预期——它只会执行django_migrations表里标记为“未完成”的那些文件,不多不少。
这个设计还天然支持了“迁移审查”流程。在我们团队,所有
makemigrations
生成的文件都必须经过 Code Review,重点看三点:一是
operations
是否符合预期(比如改字段类型是否用了
RunPython
做数据迁移);二是
dependencies
是否正确(避免跨 App 的循环依赖);三是
initial=True
标记是否合理(针对新 App 的首次迁移)。而
migrate
命令,则被封装进标准的部署脚本,由运维同学统一执行,开发同学无需干预。
2.3 一个反直觉事实:
makemigrations
有时什么都不做,才是对的
新手常有个误区:觉得“只要改了 models.py,就必须跑一次
makemigrations
”。其实不然。Django 的迁移检测逻辑,是基于
模型定义的语义等价性
,而非文件内容的字面差异。举个例子:
# models.py 原来是
class Product(models.Model):
name = models.CharField(max_length=100)
# 你改成这样(只是加了个 blank=True)
class Product(models.Model):
name = models.CharField(max_length=100, blank=True)
运行
makemigrations
,输出是
No changes detected
。为什么?因为
blank=True
是 Django 表单层的验证属性,它不改变数据库字段的任何物理特性(类型、长度、是否为空),所以不需要 DDL 操作。同理,修改
verbose_name
、
help_text
、
default
(注意:
default
如果是函数或
timezone.now()
这种动态值,Django 会要求你提供
migrations.RunPython
来处理历史数据,但单纯改字符串 default 通常也不触发迁移)都不会生成新迁移。
反过来,有些看似“没改”的操作却会触发迁移。比如你把
CharField(max_length=50)
改成
CharField(max_length=100)
,Django 会生成
AlterField
;但如果你改成
TextField()
,它会先
AddField
新字段,再
RemoveField
旧字段(因为类型变更无法直接
ALTER COLUMN TYPE
安全完成)。这些细节,正是
makemigrations
输出里值得你逐行审视的地方——它不是黑盒,而是你理解数据变更影响的第一道防线。
3. 实操要点深度解析:从生成到执行的每一个关键环节
3.1
makemigrations
:不只是生成,更是建模意图的精准表达
3.1.1 命名的艺术:为什么
--name
不是可选项,而是必选项?
python manage.py makemigrations --name add_user_profile
这个命令,表面看只是给迁移文件起个好记的名字,实则关乎
协作效率与故障定位速度
。默认生成的
0001_auto_20240515_1430.py
,除了告诉你时间戳,什么信息都不提供。而当你的项目运行三年,App 里有 87 个迁移文件时,光靠数字序号根本无法快速定位“用户资料表是哪次加的”、“邮箱唯一性约束是哪次引入的”。
命名规范我们团队强制执行三条:
-
动词开头
:
add_,remove_,alter_,rename_,明确操作类型; -
对象聚焦
:紧接核心模型或字段名,如
add_user_profile,alter_order_status_choices; -
避免模糊词
:禁用
fix,update,change这类无法体现具体变更的词。
有一次线上出现
IntegrityError: duplicate key value violates unique constraint "users_email_key"
,运维同学立刻
git blame
找到最近一次涉及
User.email
的迁移——
0042_alter_user_email_unique.py
。他打开文件,三秒内确认这是上周为合规要求添加的唯一性约束,于是马上联系法务确认是否允许临时降级,而不是花半小时在 Git 历史里翻找“那次改邮箱的提交”。
注意:
--name参数生成的文件名,前面的数字序号(如0042_)是 Django 自动管理的,你无法手动指定。它基于当前 App 下已存在的迁移文件数量递增。所以0042并不意味着“第 42 次”,而只是“当前目录下第 42 个迁移文件”。这也是为什么不要手动重命名迁移文件——Django 会根据文件名里的数字序号来确定执行顺序,乱改会导致MigrationNotFound错误。
3.1.2 处理复杂变更:当
makemigrations
无法自动推断时
Django 的自动检测能力很强,但遇到以下场景,它会直接报错,要求你手动介入:
-
字段重命名
:
makemigrations默认会认为这是“删除旧字段 + 新增新字段”,导致历史数据丢失。正确做法是使用--fake-initial或显式编写RenameField操作。 -
数据迁移
:比如要把
User.is_active的布尔值,按规则映射成新的User.status枚举('active','inactive','pending')。makemigrations只能生成AddField,但填充数据的逻辑必须由你用migrations.RunPython编写。 -
自定义 SQL
:某些数据库特有操作(如 PostgreSQL 的全文检索索引、JSONB 字段的 GIN 索引),Django 无法自动生成,需用
migrations.RunSQL。
以数据迁移为例,这是
makemigrations
生成的骨架文件里你需要补全的部分:
# 0043_add_user_status.py
from django.db import migrations
def set_initial_status(apps, schema_editor):
User = apps.get_model('auth', 'User')
# 注意:这里不能用 from myapp.models import User,必须用 apps.get_model!
User.objects.filter(is_active=True).update(status='active')
User.objects.filter(is_active=False).update(status='inactive')
def reverse_set_initial_status(apps, schema_editor):
User = apps.get_model('auth', 'User')
User.objects.filter(status='active').update(is_active=True)
User.objects.filter(status='inactive').update(is_active=False)
class Migration(migrations.Migration):
dependencies = [
('auth', '0042_alter_user_email_unique'),
]
operations = [
migrations.AddField(
model_name='user',
name='status',
field=models.CharField(max_length=20, default='pending'),
),
migrations.RunPython(set_initial_status, reverse_set_initial_status),
]
关键点在于
apps.get_model
—— 这是 Django 迁移系统的“沙箱模式”,它加载的是迁移执行时刻的模型状态,而非当前
models.py
的最新版。如果你在
RunPython
里直接
import
模型,当未来你重构了
User
模型(比如把
status
字段移到了
UserProfile
里),这个旧迁移就会因找不到
User.status
而崩溃。
apps.get_model
则永远指向该迁移文件所依赖的那个历史版本的模型。
3.2
migrate
:执行不是终点,而是状态同步的开始
3.2.1 目标迁移:精确控制执行范围的三种方式
migrate
命令最常被低估的能力,是它的
目标定位精度
。默认
python manage.py migrate
会执行所有未应用的迁移,但在真实运维中,你往往需要更精细的控制:
-
回滚到指定版本
:
python manage.py migrate auth 0041。这会反向执行0042、0043...直到0041(含),将数据库状态倒退回那个时间点。注意:0041必须是已执行过的迁移,且其reverse_code(反向操作)必须能安全执行。比如CreateModel的反向是DeleteModel,没问题;但RunPython的反向函数如果写得不严谨(比如试图删除已被其他逻辑引用的数据),就可能失败。 -
只执行某个 App 的迁移
:
python manage.py migrate users。当你的项目有 20 个 App,而只想更新用户模块时,这能避免意外触发其他模块的迁移(比如某个监控 App 的迁移里包含耗时的RunPython数据清洗)。 -
模拟执行(Dry Run)
:
python manage.py migrate --plan。它不会真执行任何 SQL,而是打印出“如果现在运行 migrate,将会按什么顺序执行哪些迁移”。这是我们上线前必做的一步:把它和 Git Diff 对比,确认计划执行的迁移,和本次发布的代码变更完全匹配。曾经有次--plan显示要执行0055_add_audit_log,但我们代码里根本没有这个文件——后来发现是同事误把本地未提交的迁移文件 push 到了测试分支,及时拦截了这次潜在事故。
3.2.2 处理冲突:当
migrate
报错 “Conflicting migrations” 时
这是多人协作中最让人头皮发麻的错误之一。典型场景:A 同学在 feature-a 分支写了
0044_add_discount_field.py
,B 同学在 feature-b 分支写了
0044_rename_coupon_code.py
,两人同时合并到 main,Git 把两个
0044_*.py
文件都保留了下来。Django 启动时发现同一个序号有两个迁移,直接报错。
解决路径非常明确,分三步:
-
识别冲突
:
python manage.py showmigrations会清晰列出所有 App 的迁移状态,并用[ ]标记未应用、[X]标记已应用,冲突的迁移会显示为(!)。 -
合并迁移
:运行
python manage.py makemigrations --empty your_app_name,生成一个空的0045_merge.py文件。然后手动编辑它,把dependencies设为两个冲突迁移的元组,operations设为空列表(因为实际变更已在那两个文件里):class Migration(migrations.Migration): dependencies = [ ('your_app', '0044_add_discount_field'), ('your_app', '0044_rename_coupon_code'), ] operations = [] -
执行合并
:
python manage.py migrate your_app 0045。Django 会把这个合并迁移标记为“已执行”,后续所有迁移都以此为依赖基点。
提示:合并迁移本身不产生任何数据库变更,它只是一个“协调者”,告诉 Django “这两个 0044 的迁移,我已经都认可了”。所以它的
operations=[]是完全正确的。切忌在合并迁移里写实际操作——那会破坏迁移的幂等性和可追溯性。
3.3 迁移文件的生命周期管理:从生成、审查到归档
3.3.1 何时该删除迁移文件?——一个危险但必要的操作
官方文档几乎从不提“删除迁移文件”,因为这听起来很危险。但在超大型遗留项目中,这是不得不做的清理工作。我们的判断标准只有一条: 该迁移文件所描述的数据库变更,已经在所有环境(开发、测试、预发、生产)中被永久固化,且其变更逻辑已完全融入当前模型定义,不再有任何历史价值 。
典型场景是“初始迁移”(
0001_initial.py
)。一个运行了八年的项目,
0001_initial.py
里创建的表结构,可能和现在的
models.py
已经天差地别。每次
makemigrations
都要扫描这个大文件,拖慢生成速度;CI 流水线里
migrate
也要多执行一次无意义的“已存在表”的检查。我们做过测试:删除
0001_initial.py
后,
makemigrations
生成速度提升 40%,
migrate
启动时间减少 15%。
操作步骤极其谨慎:
-
确保所有环境数据库都已执行到最新迁移(
showmigrations全是[X]); -
在本地新建一个干净数据库,
migrate到最新版,确认一切正常; -
手动从
django_migrations表里删除0001_initial的记录 (DELETE FROM django_migrations WHERE app='your_app' AND name='0001_initial';); -
删除
0001_initial.py文件; -
运行
makemigrations,Django 会生成一个新的0001_initial.py,但内容是当前模型的完整快照; -
把这个新文件加入 Git,但不要立即
migrate;通知所有团队成员,在下次发布时,先手动在生产库执行DELETE FROM django_migrations WHERE ...,再migrate。
这本质上是一次“迁移重置”,风险极高,必须全员知晓并协同操作。我们只在季度技术债清理日执行,且提前一周邮件通告。
3.3.2 迁移文件的 Git 管理铁律
-
必须提交
:所有
makemigrations生成的.py文件,和models.py一样,是代码的一部分,必须随功能代码一起提交。禁止“只提交 models.py,不提交迁移文件”。 -
禁止修改已提交的迁移
:一旦
0042_*.py被推送到共享分支,任何人不得修改其内容(包括operations和dependencies)。如果发现错误,必须新建0043_fix_*.py来修正,而不是篡改0042。因为其他同事的本地数据库可能已经执行了0042,你改了它,他们的migrate就会校验失败。 -
小迁移优于大迁移
:鼓励“每次 PR 只包含一个业务相关的迁移”。比如“用户注册流程优化”这个需求,应该拆成
0045_add_referral_code.py(加字段)、0046_add_referral_reward.py(加奖励逻辑)两个迁移,而不是一个0045_user_registration_enhancement.py包含所有变更。这样回滚、调试、审查都更精准。
4. 实操过程全记录:一次真实的电商订单状态迁移实战
4.1 场景还原:从需求到迁移文件生成
背景是我们正在重构电商订单的状态机。老系统用一个
order_status
字符串字段(
'created'
,
'paid'
,
'shipped'
,
'delivered'
),新需求要求:
- 状态必须是枚举,杜绝非法值;
-
增加
canceled和refunded状态; -
为每个状态增加“可进入来源状态”的校验逻辑(比如
refunded只能从delivered或shipped进入); -
历史订单的
order_status需要映射到新状态('created'→'pending_payment','paid'→'confirmed')。
第一步,修改
models.py
:
# models.py
from django.db import models
class Order(models.Model):
# ... 其他字段
STATUS_CHOICES = [
('pending_payment', '待支付'),
('confirmed', '已确认'),
('shipped', '已发货'),
('delivered', '已送达'),
('canceled', '已取消'),
('refunded', '已退款'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending_payment'
)
第二步,运行
makemigrations
:
$ python manage.py makemigrations orders --name add_status_enum
Migrations for 'orders':
orders/migrations/0028_add_status_enum.py
- Alter field status on order
Django 检测到
status
字段的
choices
和
default
发生了变化,生成了一个
AlterField
操作。但它
没有
处理历史数据映射——这是我们必须手动补充的。
4.2 手动增强迁移:编写数据迁移逻辑
打开
0028_add_status_enum.py
,在
operations
列表末尾添加
RunPython
:
# 0028_add_status_enum.py
from django.db import migrations
def migrate_old_status(apps, schema_editor):
Order = apps.get_model('orders', 'Order')
# 映射字典:老状态 -> 新状态
status_map = {
'created': 'pending_payment',
'paid': 'confirmed',
'shipped': 'shipped',
'delivered': 'delivered',
# 新增状态,老数据默认设为 pending_payment
'canceled': 'canceled',
'refunded': 'refunded',
}
# 批量更新,避免 N+1 查询
for old, new in status_map.items():
Order.objects.filter(status=old).update(status=new)
# 处理未知老状态(如有)
Order.objects.exclude(status__in=status_map.values()).update(status='pending_payment')
def reverse_migrate_old_status(apps, schema_editor):
# 反向操作:新状态 -> 老状态,仅用于开发环境回滚
Order = apps.get_model('orders', 'Order')
reverse_map = {
'pending_payment': 'created',
'confirmed': 'paid',
'shipped': 'shipped',
'delivered': 'delivered',
}
for new, old in reverse_map.items():
Order.objects.filter(status=new).update(status=old)
class Migration(migrations.Migration):
dependencies = [
('orders', '0027_alter_orderitem_quantity'),
]
operations = [
migrations.AlterField(
model_name='order',
name='status',
field=models.CharField(choices=[('pending_payment', '待支付'), ('confirmed', '已确认'), ('shipped', '已发货'), ('delivered', '已送达'), ('canceled', '已取消'), ('refunded', '已退款')], default='pending_payment', max_length=20),
),
migrations.RunPython(migrate_old_status, reverse_migrate_old_status),
]
关键细节:
-
migrate_old_status函数里,我们用filter().update()而不是for order in Order.objects.all(): order.save(),因为前者是单条 SQL,后者是 N 条,面对百万级订单,性能差百倍; -
reverse_migrate_old_status里只映射了部分状态,因为canceled和refunded在老系统里没有对应值,反向操作时直接忽略,避免IntegrityError; -
apps.get_model确保了即使未来Order模型被拆分,这个迁移依然能正确执行。
4.3 执行与验证:从本地到生产的全流程
4.3.1 本地开发环境
-
python manage.py migrate orders 0028执行迁移; -
运行
python manage.py dbshell,手动查表确认:SELECT DISTINCT status FROM orders_order LIMIT 10; -- 应该只返回 ['pending_payment', 'confirmed', 'shipped', 'delivered'] -
启动 Django shell,测试状态机逻辑:
>>> from orders.models import Order >>> o = Order.objects.first() >>> o.status = 'refunded' # 尝试设非法值 >>> o.full_clean() # 触发 Django 模型验证 ValidationError: {'status': ['"refunded" is not a valid choice.']}
4.3.2 测试环境(自动化)
我们的 CI 流水线包含一个专门的迁移测试 Job:
- 创建一个空的 PostgreSQL 数据库;
-
migrate到0027(上一个迁移); -
插入 1000 条模拟历史订单数据,
status字段设为'created','paid'等老值; -
migrate到0028; -
运行 SQL 查询,验证
SELECT COUNT(*) FROM orders_order WHERE status NOT IN ('pending_payment', 'confirmed', 'shipped', 'delivered');返回 0; - 运行 Django 测试用例,覆盖所有状态流转路径。
4.3.3 生产环境(灰度发布)
生产发布是最高危环节,我们采用三阶段策略:
-
第一阶段(只读检查)
:
python manage.py migrate orders 0028 --plan,确认计划执行的只有0028;python manage.py showmigrations orders,确认0028确实是未执行状态。 -
第二阶段(低峰期执行)
:选择凌晨 2 点(订单量最低),执行
python manage.py migrate orders 0028。全程监控数据库 CPU、慢查询日志。由于RunPython是批量更新,我们预估耗时约 3 分钟(基于测试环境数据量推算)。 -
第三阶段(验证与回滚预案)
:迁移完成后,立即运行一个验证脚本:
同时,回滚命令# verify_status_migration.py from orders.models import Order # 检查是否有非法状态 invalid = Order.objects.exclude(status__in=[s[0] for s in Order.STATUS_CHOICES]) if invalid.exists(): print(f"ERROR: Found {invalid.count()} orders with invalid status!") # 触发告警,并准备回滚 else: print("SUCCESS: All orders have valid status.")python manage.py migrate orders 0027已预先写好并测试通过,随时可执行。
5. 常见问题与排查技巧实录:那些年我们一起填过的坑
5.1 经典报错速查表
| 报错信息 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
No changes detected
| Django 认为模型与数据库状态一致 |
1.
python manage.py showmigrations
确认当前迁移状态;2.
python manage.py dbshell
查
django_migrations
表;3. 检查
models.py
是否真的有影响数据库的变更(如只改了
verbose_name
)
|
如果确认需要迁移,可加
--empty
手动生成空迁移,或检查
managed=False
设置
|
You have X unapplied migrations
| 代码有新迁移文件,但数据库未执行 |
1.
python manage.py showmigrations
查看哪些未执行;2.
git status
确认这些文件是否已提交;3. 检查
settings.py
中
INSTALLED_APPS
是否包含该 App
|
python manage.py migrate
或指定 App 执行
|
Migration <name> is applied before its dependency <dep>
| 迁移依赖关系错乱(如 A 依赖 B,但 B 未执行) |
1.
python manage.py showmigrations --plan
查执行顺序;2.
cat <migration_file>.py
查
dependencies
字段;3. 检查 Git 历史,确认依赖迁移是否被误删
|
手动
migrate
依赖迁移,或修复
dependencies
后生成合并迁移
|
DatabaseError: column "xxx" does not exist
|
迁移执行失败,但
django_migrations
表已记录为成功
|
1.
SELECT * FROM django_migrations WHERE name='<failed_migration>';
;2.
python manage.py dbshell
手动检查表结构;3. 查数据库错误日志
|
危险操作
:
DELETE FROM django_migrations WHERE ...
,然后
migrate
重试;或
migrate --fake
标记为已执行(仅当确认数据库结构已正确)
|
5.2 高频陷阱与独家避坑技巧
5.2.1 陷阱一:“假成功”迁移——数据库报错但迁移表已记录
这是最隐蔽的坑。Django 的迁移执行逻辑是:先执行 SQL,再往
django_migrations
表插入记录。但如果 SQL 执行成功(比如
ALTER TABLE ADD COLUMN
成功),而后续的
RunPython
函数抛出异常,Django 会回滚
RunPython
的事务,但
不会回滚前面的 SQL
!此时
django_migrations
表里已有一条记录,而数据库里字段已加,但数据未填充。
避坑技巧
:永远把
RunPython
操作放在
AlterField
/
AddField
等 DDL 操作之后,并在
RunPython
函数里做双重校验:
def safe_data_migration(apps, schema_editor):
Order = apps.get_model('orders', 'Order')
# 第一步:确认字段已存在
if not hasattr(Order, 'new_field'):
raise RuntimeError("Field 'new_field' not found in Order model!")
# 第二步:确认数据可安全更新
if Order.objects.filter(new_field__isnull=True).exists():
# 执行填充逻辑...
pass
5.2.2 陷阱二:
--fake
的双刃剑
python manage.py migrate --fake
会跳过实际执行,只在
django_migrations
表里插入记录。它适用于两种场景:
-
初始化遗留数据库
:你接手一个老项目,数据库结构早已存在,但没有迁移历史。此时
--fake-initial会把0001_initial.py标记为已执行,而不尝试创建表(因为表已存在)。 -
紧急绕过失败迁移
:某个
RunPython因数据脏污失败,你确认数据库结构已正确,只想让迁移状态同步。
致命风险
:
--fake
后,Django 会认为该迁移已“成功”,后续所有依赖它的迁移都会正常执行。如果
--fake
的迁移里本该有关键数据填充,而你跳过了,后续逻辑就会出错。
我的经验
:
--fake
永远是最后手段。在执行前,必须:
-
git checkout到该迁移文件生成时的代码版本; -
在本地空库上
migrate一次,确认它本应做什么; -
手动在生产库执行等效 SQL(如
UPDATE ...); -
再
--fake。
5.2.3 陷阱三:时区与
auto_now_add
的幽灵错误
DateTimeField(auto_now_add=True)
字段,在迁移时会被 Django 自动设置为
default=timezone.now
。但
timezone.now
是一个函数对象,
makemigrations
无法序列化它,所以会生成
default=django.utils.timezone.now
这样的字符串。问题在于,如果服务器时区和开发机器时区不同,
migrate
时
default
的计算时机(迁移文件生成时 vs 迁移执行时)会导致数据不一致。
解决方案
:永远用字符串形式的
default
,并在
models.py
里明确定义:
from django.utils import timezone
class MyModel(models.Model):
created_at = models.DateTimeField(default=timezone.now) # ✅ 正确:函数对象
# created_at = models.DateTimeField(default=timezone.now()) # ❌ 错误:调用结果,是固定时间戳
Django 会智能处理
timezone.now
这种可调用对象,在每次创建实例时调用它,确保时区正确。
5.3 性能优化:当迁移变成发布瓶颈
在数据量超大的表(如日志表、订单表)上执行迁移,是线上发布的心腹大患。
ALTER TABLE
在 MySQL 上会锁表,在 PostgreSQL 上虽支持并发,但
ADD COLUMN
仍需短时锁。我们的优化策略是分层应对:
-
预防层
:在
models.py设计阶段就规避高危操作。比如,绝不给百万级表的字段加NOT NULL约束(除非有默认值);用CharField(max_length=255)代替TextField存储短文本,因为TextField在某些数据库上索引效率更低。 -
工具层
:对 PostgreSQL,我们封装了
pg_repack命令到部署脚本,对 MySQL,使用pt-online-schema-change(Percona Toolkit)。这些工具能在不锁表的情况下完成 DDL。 -
迁移层
:对于必须的
RunPython数据迁移,拆分成小批次:def batched_data_migration(apps, schema_editor): Order = apps.get_model('orders', 'Order') batch_size = 1000 offset = 0 while True:
1077

被折叠的 条评论
为什么被折叠?



