Django 迁移使用 RunPython 提交更改

Posted

技术标签:

【中文标题】Django 迁移使用 RunPython 提交更改【英文标题】:Django migrations using RunPython to commit changes 【发布时间】:2015-04-10 09:11:06 【问题描述】:

我想更改我的一个模型中的一个外键,该模型当前可以具有 NULL 值,使其不可为空。

我从我的字段中删除了null=True 并运行了makemigrations

因为我正在更改一个表,该表中已经包含包含 NULL 值的行,所以我被要求立即提供一次性值或编辑迁移文件并添加 RunPython 操作。

我的 RunPython 操作列在 AlterField 操作之前,并对此字段进行了必要的更新,因此它不包含 NULL 值(仅限已包含 NULL 值的行)。

但是,迁移仍然失败并出现以下错误: django.db.utils.OperationalError: cannot ALTER TABLE "my_app_site" because it has pending trigger events

这是我的代码:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations

def add_default_template(apps, schema_editor):
    Template = apps.get_model("my_app", "Template")
    Site = apps.get_model("my_app", "Site")

    accept_reject_template = Template.objects.get(name="Accept/Reject")
    Site.objects.filter(template=None).update(template=accept_reject_template)    

class Migration(migrations.Migration):

    dependencies = [
        ('my_app', '0021_auto_20150210_1008'),
    ]

    operations = [
        migrations.RunPython(add_default_template),
        migrations.AlterField(
            model_name='site',
            name='template',
            field=models.ForeignKey(to='my_app.Template'),
            preserve_default=False,
        ),
    ]

如果我理解正确,当字段更改为不可为空但该字段包含空值时,可能会发生此错误。 在这种情况下,我能想到发生这种情况的唯一原因是RunPython 操作事务在运行AlterField 之前没有“提交”数据库中的更改。

如果这确实是原因 - 我如何确保更改反映在数据库中? 如果不是 - 错误的原因是什么?

谢谢!

【问题讨论】:

想到的第一个想法,拆分它。先进行数据迁移,然后让你的字段不是 Null。 是的,我想到了这种方法,但我想知道是否有办法避免这种情况并在同一个迁移中完成所有操作 【参考方案1】:

这是因为 Django 创建约束为DEFERRABLE INITIALLY DEFERRED:

ALTER TABLE my_app_site
ADD CONSTRAINT "[constraint_name]"
FOREIGN KEY (template_id)
REFERENCES my_app_template(id)
DEFERRABLE INITIALLY DEFERRED;

这告诉 PostgreSQL 不需要在每个命令之后立即检查外键,但可以推迟到事务结束。

因此,当事务修改内容和结构时,会在结构更改的同时检查约束,或者计划在更改结构后进行检查。这两种状态都是错误的,数据库将中止事务而不是做出任何假设。

您可以通过调用 SET CONSTRAINTS ALL IMMEDIATE 指示 PostgreSQL在当前事务中立即检查约束,因此结构更改不会成为问题(请参阅 SET CONSTRAINTS 文档)。您的迁移应如下所示:

operations = [
    migrations.RunSQL('SET CONSTRAINTS ALL IMMEDIATE',
                      reverse_sql=migrations.RunSQL.noop),

    # ... the actual migration operations here ...

    migrations.RunSQL(migrations.RunSQL.noop,
                      reverse_sql='SET CONSTRAINTS ALL IMMEDIATE'),
]

第一个操作用于应用(向前)迁移,最后一个用于取消应用(向后)迁移。

编辑:约束延迟对于避免插入排序很有用,特别是对于自引用表和具有循环依赖关系的表。所以弯曲 Django 时要小心。

后期编辑: 在 Django 1.7 和更新版本上,有一个特殊的 SeparateDatabaseAndState 操作允许在同一迁移中更改数据和更改结构。在使用上面的“立即设置约束”方法之前尝试使用此操作。示例:

operations = [
    migrations.SeparateDatabaseAndState(database_operations=[
            # put your sql, python, whatever data migrations here
        ],
        state_operations=[
            # field/model changes goes here
        ]),
]

【讨论】:

这就是我要找的答案! 这需要恢复到原来的样子吗? @shadow 不,文档说“SET CONSTRAINTS 在当前事务中设置约束检查的行为。” 你能提供一个具体、正确的使用SeparateDatabaseAndState的例子吗?我试图通过在database_operations 中用RunPython 和在state_operations 中的AlterField 填充目标字段来解决同样的问题。但是结果是Django实际上并没有在数据库中创建NOT NULL约束。 对我有用的是 Django 对非原子迁移的支持。用“atomic = True”标记迁移类,它将停止在单个事务中执行整个迁移。 docs.djangoproject.com/en/dev/howto/writing-migrations/…【参考方案2】:

是的,我想说是事务边界阻止了在运行 ALTER 之前提交迁移中的数据更改。

我会按照@danielcorreia 的说法将其实现为两个迁移,因为看起来即使SchemaEditor 也受事务约束,通过您必须使用的上下文管理器。

【讨论】:

我不得不承认,虽然这种方法是有效的并且在技术上是可行的,但令人沮丧的是,在同一次迁移中没有办法实现这一点。谢谢! 拆分成2个(一个带有--empty和手动处理)是解决方案!

以上是关于Django 迁移使用 RunPython 提交更改的主要内容,如果未能解决你的问题,请参考以下文章

Django迁移RunPython无法调用模型方法

历史模型在哪里?

清空桌子并装满固定装置

内置应用程序的 Django 初始数据

在 Django 数据迁移中手动提交

将字段更改为多对多时的 Django 数据迁移