压缩Django迁移时的循环依赖

Don*_*kby 4 python django

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

我已经创建了一个小样本项目来重现问题.该项目有两个应用程序:fruitmeat.一个Apple有许多Bacon 孩子,并且Bacon有许多Cranberry儿童.你可以看到水果应用程序依赖于肉类应用程序,肉类应用程序取决于水果应用程序.

第一次提交创建所有三个型号,每个键和外键的名称字段CranberryBaconBaconApple.调用makemigrations创建三个迁移:

  • fruit/0001_initial创建AppleCranberry模型
  • meat/0001_initialBacon使用其外键创建模型Apple
  • fruit/0002_cranberry_bacon增加了从外键CranberryBacon

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

  • fruit/0003_apple_size增加了这个size领域

squashmigrations现在运行会创建一个带有循环依赖关系的压缩迁移.该squashmigrations文件给出了这样的建议:

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

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

$ ./manage.py migrate
...
django.db.utils.ProgrammingError: column "bacon_id" of relation "fruit_cranberry" already exists
Run Code Online (Sandbox Code Playgroud)

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

Don*_*kby 10

这似乎很多工作,但它是迄今为止我发现的最佳解决方案.我已经在主分支中发布了压缩的迁移.在运行之前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)
Run Code Online (Sandbox Code Playgroud)

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

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

现在运行squashmigrations fruit 0100并重命名迁移,以便更容易遵循序列:

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

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

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

Cranberryto 切换回外键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分支表示在未来会发生什么,一旦所有设施已迁移过去的南瓜点.删除从1到100的所有迁移,因为它们已被101替换.replaces从中删除列表fruit/0101_squashed.运行showmigrations以检查是否存在任何损坏的依赖项,并将其替换为fruit/0101_squashed.

多对多关系的恐怖

如果你不幸在两个应用程序之间建立多对多的关系,那真的很难看.我不得不使用SeparateDatabaseAndState操作断开两个应用程序,而无需编写数据迁移.诀窍是使用相同的表和字段名称替换与临时子模型的多对多关系,然后告诉Django只更新其状态而不触及数据库模式.要查看示例,请查看我的unlink,压缩重新链接迁移.


Don*_*kby 5

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

这似乎令人费解,但是如果您绝对必须找到一种方法来压缩迁移,这就是我在小样本项目中要做的工作:

  1. 删除所有迁移。

    $ rm fruit/migrations/0*
    $ rm meat/migrations/0*
    
    Run Code Online (Sandbox Code Playgroud)
  2. 创建一组新的迁移。这是我看到Django通过将0001_initial和分开来正确打破依赖关系周期的唯一方法0002_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
    
    Run Code Online (Sandbox Code Playgroud)
  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 -- .
    
    Run Code Online (Sandbox Code Playgroud)
  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')]
    
    Run Code Online (Sandbox Code Playgroud)
  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 = [
        ]
    
    Run Code Online (Sandbox Code Playgroud)
  6. 添加0100_prepare_squash到要0102_link_apps替换的迁移列表中。

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

这似乎非常危险,特别是对旧迁移的依赖项进行更改时。我想您可以使依赖关系链更加精细,以确保所有内容都以正确的顺序运行,但是设置起来会更加痛苦。

  • 我不打算使用这个解决方案,但其中的解释非常有价值,所以 +1。 (2认同)