压缩 Django 迁移时的循环依赖
Posted
技术标签:
【中文标题】压缩 Django 迁移时的循环依赖【英文标题】:Circular dependency when squashing Django migrations 【发布时间】:2016-10-09 05:55:58 【问题描述】:我们已经创建了一个大型 Django 应用程序,并且我们想要压缩迁移。但是,压缩迁移在我们应用程序中的应用程序之间具有循环依赖关系。我们如何在不破坏 Django 迁移压缩的情况下打破这些循环依赖关系?
我创建了一个small sample project 来重现该问题。该项目有两个应用程序:fruit
和 meat
。一个Apple
有很多Bacon
孩子,Bacon
有很多 Cranberry
孩子。可以看到水果类app依赖于肉类app,肉类类app依赖于水果类app。
第一次提交创建了所有三个模型,每个模型都有一个名称字段,并且
从Cranberry
到Bacon
和从Bacon
到Apple
的外键。调用 makemigrations
会创建三个迁移:
fruit/0001_initial
创建 Apple
和 Cranberry
模型
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_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')]
这似乎非常危险,尤其是对旧迁移的依赖项进行更改。我想你可以让依赖链更精细,以确保一切都以正确的顺序运行,但设置起来会更加痛苦。
【讨论】:
我不会用这个解决方案,但是里面的解释很有价值,所以+1。【参考方案3】:这似乎需要做很多工作,但这是迄今为止我找到的最佳解决方案。我已经在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...)
。
下一次提交包含所有这些更改以用于演示目的。它 但是,如果没有以下提交,推送它是没有意义的,因为 这些应用仍未关联。
将外键从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 迁移。
【讨论】:
以上是关于压缩 Django 迁移时的循环依赖的主要内容,如果未能解决你的问题,请参考以下文章
django.db.migrations.exceptions.CircularDependencyError
在 Flyway 的 Java 迁移文件中使用 JdbcTemplate 会导致依赖循环