压缩 Django 迁移时的循环依赖

Posted

技术标签:

【中文标题】压缩 Django 迁移时的循环依赖【英文标题】:Circular dependency when squashing Django migrations 【发布时间】:2016-10-09 05:55:58 【问题描述】:

我们已经创建了一个大型 Django 应用程序,并且我们想要压缩迁移。但是,压缩迁移在我们应用程序中的应用程序之间具有循环依赖关系。我们如何在不破坏 Django 迁移压缩的情况下打破这些循环依赖关系?

我创建了一个small sample project 来重现该问题。该项目有两个应用程序:fruitmeat。一个Apple 有很多Bacon 孩子,Bacon 有很多 Cranberry 孩子。可以看到水果类app依赖于肉类app,肉类类app依赖于水果类app。

第一次提交创建了所有三个模型,每个模型都有一个名称字段,并且 从CranberryBacon 和从BaconApple 的外键。调用 makemigrations 会创建三个迁移:

fruit/0001_initial 创建 AppleCranberry 模型 meat/0001_initial 创建带有 Apple 外键的 Bacon 模型 fruit/0002_cranberry_bacon 将外键从 Cranberry 添加到 Bacon

下一次提交添加了一个 Apple.size 字段,这样就可以压缩一些东西了。 调用 makemigrations 添加另一个迁移:

fruit/0003_apple_size 添加size 字段

运行 squashmigrations 现在会创建一个具有循环依赖关系的压缩迁移。 squashmigrations documentation 给出了这样的建议:

要手动解析CircularDependencyError,请将循环依赖循环中的一个 ForeignKey 分解为单独的迁移,并使用它移动对另一个应用程序的依赖。如果您不确定,请查看 makemigrations 在被要求从您的模型创建全新迁移时如何处理该问题。在 Django 的未来版本中,将更新 squashmigrations 以尝试自行解决这些错误。

但是,如果我这样做,则额外的迁移未正确配置为替代。这意味着我当前已经完成原始迁移的数据库尝试再次添加外键字段并失败。

$ ./manage.py migrate
...
django.db.utils.ProgrammingError: column "bacon_id" of relation "fruit_cranberry" already exists

如何告诉迁移系统两个新的迁移替换了所有旧的迁移?

【问题讨论】:

【参考方案1】:

您可以使用django-replace-migration,我编写它是为了更轻松地删除旧迁移。

【讨论】:

【参考方案2】:

对于 1.9 之后的 Django 版本,似乎更难避免 CircularDependencyError。当 Django 加载迁移图并应用替换时,它会将替换迁移的所有依赖项包含为新迁移的依赖项。这意味着即使您将另一个应用程序的依赖项从主压缩迁移中分离出来,您仍然会从您替换的旧迁移中获得依赖项。

这似乎是一个可怕的混乱,但如果你绝对必须找到一种方法来压缩你的迁移,这就是我要在我的小 sample project 上工作的内容:

    删除所有迁移。

    $ rm fruit/migrations/0*
    $ rm meat/migrations/0*
    

    创建一组新的迁移。这是我看到 Django 通过分离 0001_initial0002_cranberry_bacon 来正确打破依赖循环的唯一方法。

    $ ./manage.py makemigrations 
    Migrations for 'fruit':
      fruit/migrations/0001_initial.py
        - Create model Apple
        - Create model Cranberry
      fruit/migrations/0002_cranberry_bacon.py
        - Add field bacon to cranberry
    Migrations for 'meat':
      meat/migrations/0001_initial.py
        - Create model Bacon
    

    将新迁移重命名为替换,并恢复旧迁移。

    $ mv fruit/migrations/0001_initial.py fruit/migrations/0101_squashed.py
    $ mv fruit/migrations/0002_cranberry_bacon.py fruit/migrations/0102_link_apps.py
    $ git checkout -- .
    

    将新迁移更改为实际替换旧迁移。查看旧迁移以查看哪些迁移依赖于其他应用程序。在0102_link_apps.py 中列出这些迁移,并在0101_squashed.py 中列出所有其他迁移。

    # Added to 0101_squashed.py
    replaces = [(b'fruit', '0001_initial'), (b'fruit', '0003_apple_size')]
    
    # Added to 0102_link_apps.py
    replaces = [(b'fruit', '0002_cranberry_bacon')]
    

    现在是大型项目的痛苦部分。所有依赖于其他应用程序的旧迁移都必须从依赖链中取出。在我的示例中,0003_apple_size 现在依赖于0001_initial 而不是0002_cranberry_bacon。当然,如果您在应用程序的迁移中有多个叶节点,Django 会感到不安,因此您需要在最后将两个依赖链重新链接在一起。这里是fruit/migrations/0100_prepare_squash.py

    from __future__ import unicode_literals
    
    from django.db import migrations
    
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ('fruit', '0003_apple_size'),
            ('fruit', '0002_cranberry_bacon'),
        ]
    
        operations = [
        ]
    

    0100_prepare_squash 添加到0102_link_apps 替换的迁移列表中。

    # Added to 0102_link_apps.py
    replaces = [(b'fruit', '0002_cranberry_bacon'), (b'fruit', '0100_prepare_squash')]
    

这似乎非常危险,尤其是对旧迁移的依赖项进行更改。我想你可以让依赖链更精细,以确保一切都以正确的顺序运行,但设置起来会更加痛苦。

【讨论】:

我不会用这个解决方案,但是里面的解释很有价值,所以+1。【参考方案3】:

这似乎需要做很多工作,但这是迄今为止我找到的最佳解决方案。我已经在master branch 中发布了压缩的迁移。在运行squashmigrations之前,我们替换外键 从 CranberryBacon 带有一个整数字段。覆盖字段名称,使其 具有外键的_id 后缀。这将在不丢失数据的情况下打破依赖关系。

# TODO: switch back to the foreign key.
# bacon = models.ForeignKey('meat.Bacon', null=True)
bacon = models.IntegerField(db_column='bacon_id', null=True)

运行 makemigrations 并重命名迁移以显示它正在启动 壁球过程:

fruit/0100_unlink_apps 将外键转换为整数字段

现在运行 squashmigrations fruit 0100 并重命名迁移以使其更容易 遵循顺序:

fruit/0101_squashed 结合了从 1 到 100 的所有迁移。

注释掉从fruit/0101_squashedmeat/0001_initial 的依赖关系。它 并不是真正需要的,它创建了一个循环依赖。随着更复杂 迁移历史,其他应用程序的外键可能不会得到优化。 在文件中搜索依赖项中列出的所有应用程序名称以查看是否存在 是否还有任何外键。如果是这样,请手动将它们替换为整数字段。 通常,这意味着替换 CreateModel(...ForeignKey...)AlterModel(...IntegerField...)CreateModel(...IntegerField...)

下一次提交包含所有这些更改以用于演示目的。它 但是,如果没有以下提交,推送它是没有意义的,因为 这些应用仍未关联。

将外键从Cranberry切换回Bacon,然后运行 makemigrations 最后一次。重命名迁移以显示它是 完成壁球过程:

fruit/0102_relink_apps 将整数字段转换回外键

删除从fruit/0102_relink_appsfruit/0101_squashed的依赖, 并添加从fruit/0102_relink_appsfruit/0100_unlink_apps 的依赖项。 原来的依赖是行不通的。采取以前的依赖项 在fruit/0101_squashed 中注释掉并将它们添加到fruit/0102_relink_apps。 这将确保以正确的顺序创建链接。

运行测试套件以显示压缩迁移工作正常。如果你 可以,针对 SQLite 以外的东西进行测试,因为它没有捕捉到一些 外键问题。备份开发或生产数据库并运行 migrate 看到应用程序的取消链接和重新链接不会中断 任何东西。

打个盹。

奖励部分:在所有安装都被压缩之后

convert_squash branch 显示了未来可能发生的一切 安装已迁移超过壁球点。删除所有迁移 从 1 到 100,因为它们已被 101 替换。删除 replaces 列表 来自fruit/0101_squashed。运行showmigrations 以检查是否有任何损坏 依赖项,并将它们替换为fruit/0101_squashed

多对多关系的恐惧

如果你很不幸地在两个应用程序之间建立了多对多的关系,它会变得非常丑陋。我不得不使用the SeparateDatabaseAndState operation 断开这两个应用程序,而无需编写数据迁移。诀窍是使用相同的表和字段名称用临时子模型替换多对多关系,然后告诉 Django 只更新其状态而不触及数据库模式。要查看示例,请查看我的 unlink、squashed 和 relink 迁移。

【讨论】:

以上是关于压缩 Django 迁移时的循环依赖的主要内容,如果未能解决你的问题,请参考以下文章

django.db.migrations.exceptions.CircularDependencyError

实现 proc 宏时的循环包依赖

在 Flyway 的 Java 迁移文件中使用 JdbcTemplate 会导致依赖循环

自定义库名称与系统库相同时的CMake循环依赖错误

引用 IP 地址生成配置文件时的 Terraform 循环依赖问题

Django Rest Framework 序列化程序中的循环依赖