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 其实不是个靠谱的东西)

避免

永远不要相信 makemigrations!

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

你可能感兴趣的:(django,sqlite,数据库)