在 Django 数据迁移中手动提交

Posted

技术标签:

【中文标题】在 Django 数据迁移中手动提交【英文标题】:Commit manually in Django data migration 【发布时间】:2015-09-23 17:10:39 【问题描述】:

我想编写一个数据迁移,在其中我以较小的批量修改大表中的所有行,以避免锁定问题。但是,我不知道如何在 Django 迁移中手动提交。每次我尝试运行 commit 我都会得到:

TransactionManagementError:当“原子”块处于活动状态时,这是禁止的。

AFAICT,database schema editor always wraps Postgres migrations 中的 atomic block。

有没有一种理智的方法可以从迁移中中断事务?

我的迁移如下所示:

def modify_data(apps, schema_editor):
    counter = 0
    BigData = apps.get_model("app", "BigData")
    for row in BigData.objects.iterator():
        # Modify row [...]
        row.save()
        # Commit every 1000 rows
        counter += 1
        if counter % 1000 == 0:
            transaction.commit()
    transaction.commit()

class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(modify_data),
    ]

我正在使用 Django 1.7 和 Postgres 9.3。这曾经适用于南版和旧版本的 Django。

【问题讨论】:

【参考方案1】:

我发现的最佳解决方法是在运行数据迁移之前手动退出原子范围:

def modify_data(apps, schema_editor):
    schema_editor.atomic.__exit__(None, None, None)
    # [...]

与手动重置connection.in_atomic_block 相比,这允许在迁移中使用atomic 上下文管理器。似乎没有更明智的方法了。

可以在装饰器中包含(诚然混乱的)事务中断逻辑,以与RunPython 操作一起使用:

def non_atomic_migration(func):
  """
  Close a transaction from within code that is marked atomic. This is
  required to break out of a transaction scope that is automatically wrapped
  around each migration by the schema editor. This should only be used when
  committing manually inside a data migration. Note that it doesn't re-enter
  the atomic block afterwards.
  """
  @wraps(func)
  def wrapper(apps, schema_editor):
      if schema_editor.connection.in_atomic_block:
          schema_editor.atomic.__exit__(None, None, None)
      return func(apps, schema_editor)
  return wrapper

更新

Django 1.10 将支持non-atomic migrations。

【讨论】:

Django 1.9.5: AttributeError: 'DatabaseSchemaEditor' object has no attribute 'atomic'【参考方案2】:

来自the documentation about RunPython

默认情况下,RunPython 将在不支持 DDL 事务的数据库(例如 mysql 和 Oracle)上的事务内运行其内容。这应该是安全的,但如果您尝试使用这些后端提供的 schema_editor 可能会导致崩溃;在这种情况下,将 atomic=False 传递给 RunPython 操作。

所以,而不是你所拥有的:

class Migration(migrations.Migration):
  operations = [
      migrations.RunPython(modify_data, atomic=False),
  ]

【讨论】:

谢谢。我已经尝试过了,但它实际上并没有删除迁移周围的原子上下文(至少对于 Postgres)。 很好奇,因为这是 django.db.migration.py 中的代码:if not schema_editor.connection.features.can_rollback_ddl and operation.atomic: - if not schema_editor.connection.features.can_rollback_ddl and operation.atomic: with atomic(schema_editor.connection.alias): ...。你确定没有其他事情发生吗?也许在那里设置一个断点(它是 django 1.8 中的第 109 行)? 是的,这避免了使操作原子化,但数据库模式编辑器仍然使整个迁移原子化:github.com/django/django/blob/stable/1.7.x/django/db/backends/… 哦。如果你没有想到这一点,唯一的方法可能就是connection.cursor().execute("COMMIT;"),尽管你真的想要一种“理智”的方式。 :/ 这绝对不理想,但看起来可能没有办法。 感谢您的提示,如果我还设置了connection.in_atomic_block = False,这确实有效。查看记录的 SQL 语句,这实际上工作正常(在适当的位置开始/提交)。但是,我觉得您不应该打破原子块(使用健全的代码)。这使得使用 Django 编写更复杂的数据迁移变得非常困难。【参考方案3】:

对于遇到此问题的其他人。您可以在同一个迁移中同时拥有这两个数据 (RunPython)。只要确保所有的变更表都排在第一位。您不能在任何 ALTER TABLE 之前执行 RunPython。

【讨论】:

以上是关于在 Django 数据迁移中手动提交的主要内容,如果未能解决你的问题,请参考以下文章

django south 在多个数据库上

Heroku 不会在 Django 中迁移模型

将 postgresql 索引转换为 Django 迁移

Django 南迁移 - 添加 FULLTEXT 索引

在迁移文件中使用特定模型时,Django 测试失败

尝试迁移数据库时,Django 应用程序在 Heroku 上崩溃