一、为什么数据库迁移让人头疼

咱们做Web开发的,最怕遇到数据库结构要改的情况。特别是用Django开发的项目,模型改来改去简直是家常便饭。昨天还在用CharField存用户名,今天产品经理说要支持国际化,得改成JSONField;上周刚设计好的订单表,这周就要加个优惠券关联字段。每次改模型都像在拆炸弹,生怕把生产环境的数据给搞没了。

Django自带的迁移系统(makemigrations/migrate)确实帮了大忙,但遇到复杂的模型变更时,光靠默认流程还是容易翻车。比如要给已有数据的表添加非空字段、修改字段类型、拆分表结构这些操作,都需要特别小心。

二、基础迁移操作的正确姿势

先温习下Django迁移的基本操作流程。假设我们有个简单的博客应用,技术栈是Django 3.2 + PostgreSQL。

# models.py 初始版本
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    # 注意这个字段后面会被修改
    status = models.CharField(max_length=10, default='draft')

第一次创建迁移文件:

python manage.py makemigrations
python manage.py migrate

这时候如果要在status字段上动手术,比如想把CharField改成IntegerField用数字表示状态,直接改模型会报错:

# 错误示范:直接修改字段类型
status = models.IntegerField(default=1)  # 会导致迁移失败

正确的做法是分三步走:

  1. 创建新字段status_code
  2. 编写数据迁移脚本转移数据
  3. 删除旧字段

三、处理复杂变更的实战技巧

3.1 添加非空字段的陷阱

最常见的坑就是给已有数据的表添加非空字段。比如要给文章添加分类:

# 危险操作:直接添加非空字段
category = models.CharField(max_length=50)  # 没有default值!

Django会直接拒绝生成迁移文件。正确做法有两种:

方案A:设置default值

category = models.CharField(max_length=50, default='uncategorized')

方案B:分两步走(更推荐)

# 第一步:先允许为空
category = models.CharField(max_length=50, null=True)

# 第二步:数据迁移后改为非空
# 新建空迁移文件:python manage.py makemigrations --empty yourapp
# 然后在生成的迁移文件中编写数据填充逻辑

3.2 修改字段类型的正确姿势

继续上面的status字段改造案例,完整流程如下:

  1. 创建新字段
status_code = models.IntegerField(default=1)
  1. 生成数据迁移脚本
python manage.py makemigrations --empty blog

编辑生成的迁移文件:

# 生成的迁移文件示例
from django.db import migrations

def convert_status(apps, schema_editor):
    Article = apps.get_model('blog', 'Article')
    for article in Article.objects.all():
        if article.status == 'published':
            article.status_code = 2
        elif article.status == 'archived':
            article.status_code = 3
        else:
            article.status_code = 1
        article.save()

class Migration(migrations.Migration):
    dependencies = [...]
    
    operations = [
        migrations.AddField(...),  # 添加status_code字段
        migrations.RunPython(convert_status),
        migrations.RemoveField('article', 'status'),
        migrations.RenameField('article', 'status_code', 'status'),
    ]

3.3 多表关联变更的处理

当需要拆分表结构时,比如把文章标签从CharField改成ManyToManyField:

# 改造前
tags = models.CharField(max_length=200)  # 用逗号分隔的标签

# 改造后
class Tag(models.Model):
    name = models.CharField(max_length=50)

class Article(models.Model):
    tags = models.ManyToManyField(Tag)

迁移步骤:

  1. 创建Tag模型和新的tags关联字段
  2. 编写数据迁移脚本解析原有tags字符串
  3. 创建Tag对象并建立关联关系
  4. 删除旧字段

四、生产环境迁移的最佳实践

4.1 安全迁移的黄金法则

  1. 永远先在测试环境演练 - 准备与生产环境数据量相当的测试数据
  2. 备份!备份!备份! - 执行迁移前先备份数据库
  3. 小步快跑 - 复杂的变更拆分成多个小迁移
  4. 监控回滚方案 - 确保每个迁移都有可逆方案

4.2 性能优化技巧

处理大数据表时要注意:

  • 使用批量操作代替循环save()
# 低效做法
for article in Article.objects.all():
    article.save()

# 高效做法
Article.objects.bulk_update(articles, ['status_code'])
  • 考虑在迁移中添加索引
migrations.AddIndex(
    model_name='article',
    index=models.Index(fields=['status'], name='article_status_idx'),
)

4.3 常见错误及解决方案

错误1:迁移依赖冲突
👉 解决方案:手动调整迁移文件的dependencies顺序

错误2:迁移过程中断
👉 解决方案:检查django_migrations表,手动清理失败记录

错误3:字段重命名被识别为删除+新增
👉 解决方案:使用SeparateDatabaseAndState保持数据

五、高级场景处理方案

5.1 使用第三方工具辅助

  • django-migration-linter:检测可能有问题的迁移
  • django-test-migrations:为迁移编写测试用例

安装后使用示例:

python manage.py lintmigrations --include-apps=blog

5.2 自定义迁移操作

当内置操作不满足需求时,可以自定义Operation:

from django.db.migrations.operations.base import Operation

class CustomOperation(Operation):
    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        # 实现正向迁移逻辑
        pass
    
    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        # 实现回滚逻辑
        pass

5.3 零停机迁移策略

对于不能停机的生产系统:

  1. 先部署兼容新旧代码的中间版本
  2. 执行数据迁移
  3. 部署只依赖新结构的代码版本

六、总结与经验分享

经过这么多年的Django项目实践,我总结了几个核心心得:

  1. 简单就是美 - 能用AddField解决的问题就不要用AlterField
  2. 数据迁移要幂等 - 确保脚本可以安全重复执行
  3. 文档很重要 - 在迁移文件中用注释详细说明变更原因
  4. 团队协作要同步 - 确保所有开发者同时更新迁移文件

最后记住,Django迁移系统虽然强大,但它不是魔法。复杂的数据库变更需要开发者真正理解每一步操作背后的SQL语句。当你对某个迁移操作不确定时,总可以先python manage.py sqlmigrate blog 0002查看生成的SQL,确认无误后再执行。