Django migration 新增外键的坑

TL;DR

永远不要相信 makemigrations!

migrate 之前一定好好看看 migrate 了啥东西,必要时手动修改生成的 migrate 文件。

最好把db的更新与服务代码更新解耦

场景

先描述下场景:

现在有两个表,一个是 question,一个是 choice,其中 question 和 choice 是一对多的关系,其中 choice 表中会记录 question_id(此时不是外键约束)。

class Question(models.Model):
    content = models.CharField(max_length=256, blank=True, default='')

class Choice(models.Model):
	content = models.CharField(max_length=256, default='')
    question_id = models.IntegerField(null=True)

现在我想把 question_id 改为外键,所以很自然的改写 model:

class Question(models.Model):
    content = models.CharField(max_length=256, blank=True, default='')

class Choice(models.Model):
	content = models.CharField(max_length=256, default='')
    question = models.ForeignKey(Question, on_delete=models.DO_NOTHING) 

然后快乐地执行 makemigration -> migrate,然后 choice 里面的 question_id 就全没了(实际上我还手贱加了个 default,导致都关联到 default 的 question 上了),然后第二天因为左脚先进办公室被开了。。。

原因

在自己的 demo 上复现了下。

问题出在 makemigrate 生成的文件上,解析出来的 operations 是先把 question_id 列 remove 掉(RemoveField),然后再加上(AddField)。。。

这样设计的思路我不是很懂,可能是有些 engine(比如SQLite)不支持对现有的表加外键?

解决

修改 migration 文件,把先 remove 再 add 的逻辑调整一下。

需要用到 migration.RunPython,migration 文件应该长成下边这样:

from django.db import migrations, models
import django.db.models.deletion

def duplicate_question_id(apps, schema_editor):
    Choice = apps.get_model("polls", "Choice")
    for c in Choice.objects.all():
        Question = apps.get_model("polls", "Question")
        q = Question.objects.get(id=c.question_id)
        c.question_cpy = q
        c.save()


class Migration(migrations.Migration):
	# 一些必要的依赖,这里大概率不用改
	dependencies = []
	
	operations = [
		migrations.AddField(
			model_name="choice",
			name="question_cpy",
			# question_cpy 先把 question_id 列的数据 copy 过来
			field=models.ForeignKey(
                null=True,
                on_delete=django.db.models.deletion.DO_NOTHING,
                to="polls.question",
            ),
		),
		migrations.RunPython(
            code=duplicate_question_id,
            reverse_code=migrations.RunPython.noop,
            # reverse_code 是 migrate 回退时会执行的操作
            # noop 方法是为了支持回退的空方法
        ),
		migrations.RemoveField(
            model_name="choice",
            name="question_id",
        ),
        migrations.RenameField(
            model_name="choice",
            old_name="question_cpy",
            new_name="question",
        ),
	]

注意:你需要根据实际情况编写合适的代码,上面仅提供思路。

如果你是在sqlite上测试,可以正常 migrate,但是外键不会生效(因为 sqlite 默认关闭外键)。

如果在 mysql 上测试,应该是可以成功的(待验证)。

除了 RunPython 之外还可以用RunSQL,然后仔细确定SQL的合法性,个人感觉会更安全一些,毕竟直接操作 DB 的部分,普遍会认为潜在风险较高,会对相应动作有更高的敏感度。(不过最关键的还是要感知 makemigrations 其实不是个靠谱的东西)

又跟了下这个问题,发现在 AlterField 里面指定外键的 db_column,然后再 rename 成 model 里面指定的 name 即可实现上述需求,类似:

		migrations.AlterField(
            model_name="choice",
            name="question_id",
            field=models.ForeignKey(
                name='question',
                to='polls.question',
                on_delete=django.db.models.deletion.DO_NOTHING,
                db_column='question_id',
                null=False
            ),
        ),
        migrations.RenameField(
            model_name="choice",
            old_name="question_id",
            new_name="question",
        )

这里还有些需要注意的地方,migration 整体上是通过 DDL 事务执行的,但是有些 SQL 引擎不支持DDL 事务(比如 MySQL),所以可能会出现 migrate 不成功然后无法回滚的情况(一个 migration 可能会对应两条 SQL 语句),需要关注。

避免

永远不要相信 makemigrations!

最好把db的更新与服务代码更新解耦

Django 还提供了 sqlmigrate 这个命令,可以看对应的 migrations 动作具体会解析成哪些 sql 语句执行,可以在 merge 之前把对应的 sql 语句进行一次 review。

推荐

migrate 其实分 code state 和 db state 两部分,可以把这两部分开设置:change field name with no db impact

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值