压缩Django迁移时的循环依赖

时间:2016-06-08 19:34:44

标签: python django

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

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

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

  • fruit/0001_initial创建AppleCranberry模型
  • meat/0001_initial创建Bacon模型,其外键为Apple
  • fruit/0002_cranberry_bacon将外键从Cranberry添加到Bacon

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

  • fruit/0003_apple_size添加了size字段

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

  

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

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

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

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

3 个答案:

答案 0 :(得分:9)

这似乎是很多工作,但它是迄今为止我发现的最佳解决方案。我已经在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...)。{/ p>

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

切换回从CranberryBacon的外键,然后运行 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只更新其状态而不触及数据库模式。要查看示例,请查看我的unlinksquashedrelink迁移。

答案 1 :(得分:4)

对于1.9之后的Django版本,似乎更难避免CircularDependencyError。当Django加载迁移图并应用替换时,它将替换的迁移的所有依赖项包括为新迁移的依赖项。这意味着,即使您将主要压缩迁移中的其他应用程序的依赖关系拆分,您仍然可以从您替换的旧迁移中获得依赖关系。

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

  1. 删除所有迁移。

    $ rm fruit/migrations/0*
    $ rm meat/migrations/0*
    
  2. 创建一组新的迁移。这是我看到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
    
  3. 将新迁移重命名为替换,并恢复旧迁移。

    $ 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 -- .
    
  4. 将新迁移更改为旧迁移的实际替换。查看旧迁移,了解哪些依赖于其他应用。在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')]
    
  5. 现在是一个大型项目的痛苦部分。所有依赖于其他应用程序的旧迁移都必须从依赖链中取出。在我的示例中,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 = [
        ]
    
  6. 0100_prepare_squash添加到0102_link_apps替换的迁移列表中。

    # Added to 0102_link_apps.py
    replaces = [(b'fruit', '0002_cranberry_bacon'), (b'fruit', '0100_prepare_squash')]
    
  7. 这似乎非常危险,特别是对旧迁移的依赖关系进行了更改。我想你可以让依赖链更精细,以确保一切都以正确的顺序运行,但设置起来会更加痛苦。

答案 2 :(得分:0)

您可以使用django-replace-migration,我已经写过它,可以更轻松地删除旧的迁移。