我们已经创建了一个大型Django应用程序,我们希望压缩迁移。但是,压缩的迁移在我们的应用程序中的应用程序之间存在循环依赖关系。如何在不破坏Django迁移压缩的情况下打破这些循环依赖?
我创建了small sample project来重现问题。该项目有两个应用:fruit
和meat
。 Apple
有很多Bacon
孩子,Bacon
有很多Cranberry
个孩子。您可以看到水果应用程序取决于肉类应用程序,肉类应用程序取决于水果应用程序。
第一次提交创建所有三个模型,每个模型都有一个名称字段
从Cranberry
到Bacon
以及从Bacon
到Apple
的外键。调用makemigrations
会创建三个迁移:
fruit/0001_initial
创建Apple
和Cranberry
模型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
如何告诉迁移系统两个新迁移会替换所有旧迁移?
答案 0 :(得分:9)
这似乎是很多工作,但它是迄今为止我发现的最佳解决方案。我已经在master branch发布了压缩的迁移。在运行squashmigrations
之前,我们替换外键
从Cranberry
到Bacon
,带有整数字段。覆盖字段名称以便它
具有外键的_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_squashed
到meat/0001_initial
的依赖关系。它
并不是真正需要的,它创造了一种循环依赖。随着更复杂
迁移历史记录,其他应用程序的外键可能无法优化。
在文件中搜索依赖项中列出的所有应用程序名称,以查看是否存在
是否留下任何外键。如果是这样,请使用整数字段手动替换它们。
通常,这意味着替换CreateModel(...ForeignKey...)
和。{
AlterModel(...IntegerField...)
CreateModel(...IntegerField...)
。{/ p>
下一次提交包含所有这些更改以用于演示目的。它 但是,在没有以下提交的情况下推送它是没有意义的,因为 应用仍然没有关联。
切换回从Cranberry
到Bacon
的外键,然后运行
makemigrations
最后一次。重命名迁移以显示它是
完成壁球过程:
fruit/0102_relink_apps
将整数字段转换回外键删除从fruit/0102_relink_apps
到fruit/0101_squashed
的依赖关系,
并添加从fruit/0102_relink_apps
到fruit/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迁移。
答案 1 :(得分:4)
对于1.9之后的Django版本,似乎更难避免CircularDependencyError
。当Django加载迁移图并应用替换时,它将替换的迁移的所有依赖项包括为新迁移的依赖项。这意味着,即使您将主要压缩迁移中的其他应用程序的依赖关系拆分,您仍然可以从您替换的旧迁移中获得依赖关系。
这似乎是一个可怕的混乱,但如果你绝对必须找到一种方法来压缩你的迁移,这就是我在我的小sample project上工作的东西:
删除所有迁移。
$ rm fruit/migrations/0*
$ rm meat/migrations/0*
创建一组新的迁移。这是我看到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
将新迁移重命名为替换,并恢复旧迁移。
$ 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')]
这似乎非常危险,特别是对旧迁移的依赖关系进行了更改。我想你可以让依赖链更精细,以确保一切都以正确的顺序运行,但设置起来会更加痛苦。
答案 2 :(得分:0)
您可以使用django-replace-migration,我已经写过它,可以更轻松地删除旧的迁移。