在具有所需 ForeignKey 引用的 Django (1.8) 应用程序之间移动模型

Posted

技术标签:

【中文标题】在具有所需 ForeignKey 引用的 Django (1.8) 应用程序之间移动模型【英文标题】:Move models between Django (1.8) apps with required ForeignKey references 【发布时间】:2015-08-16 13:00:54 【问题描述】:

这是对这个问题的扩展:How to move a model between two Django apps (Django 1.7)

我需要将一堆模型从 old_app 移动到 new_app。最好的答案似乎是Ozan's,但如果需要外键引用,事情就有点棘手了。 @halfnibble 在 cmets 中为 Ozan 的答案提供了一个解决方案,但我仍然无法确定步骤的精确顺序(例如,何时将模型复制到 new_app,何时从 old_app 中删除模型,哪些迁移将位于 old_app.migrationsnew_app.migrations 等)

非常感谢任何帮助!

【问题讨论】:

【参考方案1】:

在应用之间迁移模型。

简短的回答是,不要这样做!

但这个答案在现实世界的生活项目和生产数据库中很少奏效。因此,我创建了一个sample GitHub repo 来演示这个相当复杂的过程。

我正在使用 mysql(不,这些不是我的真实凭据)。

问题

我使用的示例是一个带有 cars 应用程序的工厂项目,该应用程序最初具有 Car 模型和 Tires 模型。

factory
  |_ cars
    |_ Car
    |_ Tires

Car 模型与 Tires 具有 ForeignKey 关系。 (如,您通过汽车模型指定轮胎)。

但是,我们很快意识到Tires 将成为一个拥有自己视图等的大型模型,因此我们希望它在自己的应用程序中。因此,所需的结构是:

factory
  |_ cars
    |_ Car
  |_ tires
    |_ Tires

我们需要保持CarTires 之间的ForeignKey 关系,因为太多依赖于保存数据。

解决方案

第 1 步。设置设计不佳的初始应用。

浏览step 1.的代码

第 2 步。 创建一个管理界面并添加一堆包含 ForeignKey 关系的数据。

查看step 2.

第 3 步。 决定将 Tires 模型移至其自己的应用程序。小心翼翼地将代码剪切并粘贴到新的轮胎应用程序中。确保更新 Car 模型以指向新的 tires.Tires 模型。

然后运行./manage.py makemigrations 并在某处备份数据库(以防万一这严重失败)。

最后,运行./manage.py migrate,看看doom的错误信息,

django.db.utils.IntegrityError: (1217, '无法删除或更新父行:外键约束失败')

在step 3.中查看到目前为止的代码和迁移

第 4 步。 棘手的部分。自动生成的迁移看不到您只是将模型复制到不同的应用程序。所以,我们必须做一些事情来解决这个问题。

您可以在step 4. 中关注并查看使用 cmets 的最终迁移,我确实对此进行了测试以验证它是否有效。

首先,我们将处理cars。您必须进行新的空迁移。此迁移实际上需要在最近创建的迁移(执行失败的迁移)之前运行。因此,我重新编号了我创建的迁移并更改了依赖项以首先运行我的自定义迁移,然后是 cars 应用程序的最后一个自动生成的迁移。

您可以使用以下命令创建一个空迁移:

./manage.py makemigrations --empty cars

步骤 4.a. 进行自定义 old_app 迁移。

在第一次自定义迁移中,我将只执行“database_operations”迁移。 Django 为您提供了拆分“状态”和“数据库”操作的选项。您可以通过查看code here 来了解这是如何完成的。

我在第一步中的目标是将数据库表从 oldapp_model 重命名为 newapp_model 而不会弄乱 Django 的状态。你必须弄清楚 Django 会根据应用程序名称和模型名称来命名你的数据库表。

现在您可以修改初始 tires 迁移。

步骤 4.b. 修改 new_app 初始迁移

操作很好,但我们只想修改“状态”而不是数据库。为什么?因为我们保留了来自cars 应用程序的数据库表。此外,您需要确保先前进行的自定义迁移是此迁移的依赖项。看轮胎migration file。

所以,现在我们在数据库中将cars.Tires 重命名为tires.Tires,并更改了Django 状态以识别tires.Tires 表。

步骤 4.c. 修改 old_app 上次自动生成的迁移。

返回到汽车,我们需要修改最后一个自动生成的迁移。它应该需要我们的第一次定制汽车迁移,以及最初的轮胎迁移(我们刚刚修改)。

这里我们应该保留AlterField 操作,因为Car 模型指向不同的模型(即使它具有相同的数据)。但是,我们需要删除与DeleteModel 相关的迁移行,因为cars.Tires 模型不再存在。它已完全转换为tires.Tires。查看this migration。

步骤 4.d. 清理 old_app 中的陈旧模型。

最后但同样重要的是,您需要在汽车应用中进行最终的自定义迁移。在这里,我们将做一个“状态”操作,只删除cars.Tires 模型。它是仅状态的,因为 cars.Tires 的数据库表已被重命名。这个last migration 清理了剩余的 Django 状态。

【讨论】:

感谢@halfnibble,这太完美了。 非常感谢!像魅力一样工作;)只有一个小建议:当你说“I changed the number”时要更加明确。也许“交换迁移文件名”或类似的东西...... @Teekin 我想不出任何技术原因。虽然为了减少可能的歧义,但我认为最好制定一个管理命令。也许manage.py migrate --refactor cars.Tires > tires.Tires 或类似的东西。如果我有时间,我会自己做,但我得工作谋生。 :) 通用外键仍然会损坏!您还需要添加迁移以重命名 django_contenttypes 中的 app_label 以反映更改。 太好了,谢谢!我注意到可以减少步骤数。更新RenameModelTable(旧应用)和CreateModel(新应用)的迁移后,您可以使用manage.py migrate 生成最终迁移。它可以在一次迁移中更新外键和状态。【参考方案2】:

刚刚将两个模型从 old_app 移至 new_app,但 FK 引用在来自 app_xapp_y 的某些模型中,而不是来自 old_app 的模型。

在这种情况下,请按照 Nostalg.io 提供的步骤如下:

将模型从 old_app 移动到 new_app,然后在代码库中更新 import 语句。 makemigrations。 执行步骤 4.a。但对所有移动的模型使用AlterModelTable。两个给我。 执行步骤 4.b。照原样。 执行步骤 4.c。此外,对于每个具有新生成的迁移文件的应用,手动编辑它们,以便您迁移 state_operations。 执行步骤 4.d,但对所有移动的模型使用 DeleteModel

注意事项:

所有从其他应用程序编辑的自动生成的迁移文件都依赖于来自old_app 的自定义迁移文件,其中AlterModelTable 用于重命名表。 (在步骤 4.a 中创建) 就我而言,我不得不从old_app 中删除自动生成的迁移文件,因为我没有任何AlterField 操作,只有DeleteModelRemoveField 操作。或者留空operations = []

为避免在从头开始创建测试数据库时出现迁移异常,请确保在步骤 4.a 中创建来自 old_app 的自定义迁移。具有来自其他应用程序的所有先前迁移依赖项。

old_app
  0020_auto_others
  0021_custom_rename_models.py
    dependencies:
      ('old_app', '0020_auto_others'),
      ('app_x', '0002_auto_20170608_1452'),
      ('app_y', '0005_auto_20170608_1452'),
      ('new_app', '0001_initial'),
  0022_auto_maybe_empty_operations.py
    dependencies:
      ('old_app', '0021_custom_rename_models'),
  0023_custom_clean_models.py
    dependencies:
      ('old_app', '0022_auto_maybe_empty_operations'),
app_x
  0001_initial.py
  0002_auto_20170608_1452.py
  0003_update_fk_state_operations.py
    dependencies
      ('app_x', '0002_auto_20170608_1452'),
      ('old_app', '0021_custom_rename_models'),
app_y
  0004_auto_others_that_could_use_old_refs.py
  0005_auto_20170608_1452.py
  0006_update_fk_state_operations.py
    dependencies
      ('app_y', '0005_auto_20170608_1452'),
      ('old_app', '0021_custom_rename_models'),

顺便说一句:有一张关于此的公开票:https://code.djangoproject.com/ticket/24686

【讨论】:

在尝试@Nostalg.io 的方法之前,我应该阅读您的评论,因为我遇到了您解决的所有问题;)仍然感谢您在这里记录!然而,这个复杂的过程很难通过阅读来获得,我想我实际上必须尝试它才能理解它。我需要我的数据库备份几次,所以首先有一个有效的备份真的是最重要的部分! 嗯,我希望它有所帮助 :) 跨应用程序移动模型确实很麻烦。在一个事物不断变化(进化设计)的世界里,我认为我们真的需要找到一种更好的方法来做到这一点。也许一个好主意是从一开始就从松散的 FK 开始,为将来的微服务架构做准备。我同意您不会完全受益于完整的 ORM 功能,但它仍然是一个很好的权衡。【参考方案3】:

如果您需要移动模型并且您不再有权访问该应用程序(或者您不希望访问),您可以创建一个新的操作并考虑仅在迁移后创建一个新模型模型不存在。

在本例中,我将“MyModel”从 old_app 传递给 myapp。

class MigrateOrCreateTable(migrations.CreateModel):
    def __init__(self, source_table, dst_table, *args, **kwargs):
        super(MigrateOrCreateTable, self).__init__(*args, **kwargs)
        self.source_table = source_table
        self.dst_table = dst_table

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        table_exists = self.source_table in schema_editor.connection.introspection.table_names()
        if table_exists:
            with schema_editor.connection.cursor() as cursor:
                cursor.execute("RENAME TABLE  TO ;".format(self.source_table, self.dst_table))
        else:
            return super(MigrateOrCreateTable, self).database_forwards(app_label, schema_editor, from_state, to_state)


class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0002_some_migration'),
    ]

    operations = [
        MigrateOrCreateTable(
            source_table='old_app_mymodel',
            dst_table='myapp_mymodel',
            name='MyModel',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=18))
            ],
        ),
    ]

【讨论】:

【参考方案4】:

工作完成后,我尝试进行新的迁移。但我面临以下错误: ValueError: Unhandled pending operations for models: oldapp.modelname (referred to by fields: oldapp.HistoricalProductModelName.model_ref_obj)

如果您的 Django 模型使用 HistoricalRecords 字段,请不要忘记在关注 @Nostalg.io 答案时添加附加模型/表。

在第一步(4.a)将以下项目添加到database_operations

    migrations.AlterModelTable('historicalmodelname', 'newapp_historicalmodelname'),

并在最后一步(4.d)将额外的删除添加到state_operations

    migrations.DeleteModel(name='HistoricalModleName'),

【讨论】:

【参考方案5】:

Nostalg.io 的方式在前向工作(自动生成引用它的所有其他应用程序 FK)。但我也需要倒退。为此,向后的 AlterTable 必须在向后任何 FK 之前发生(原来它会在那之后发生)。因此,为此,我将 AlterTable 拆分为 2 个单独的 AlterTableF 和 AlterTableR,每个仅在一个方向上工作,然后在第一次自定义迁移中使用正向而不是原始方向,并在最后一次汽车迁移中反转一个(两者都发生在汽车应用程序中)。像这样的:

#cars/migrations/0002...py :

class AlterModelTableF( migrations.AlterModelTable):
    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        print( 'nothing back on', app_label, self.name, self.table)

class Migration(migrations.Migration):                                                         
    dependencies = [
        ('cars', '0001_initial'),
    ]

    database_operations= [
        AlterModelTableF( 'tires', 'tires_tires' ),
        ]
    operations = [
        migrations.SeparateDatabaseAndState( database_operations= database_operations)         
    ]           


#cars/migrations/0004...py :

class AlterModelTableR( migrations.AlterModelTable):
    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        print( 'nothing forw on', app_label, self.name, self.table)
    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        super().database_forwards( app_label, schema_editor, from_state, to_state)

class Migration(migrations.Migration):
    dependencies = [
        ('cars', '0003_auto_20150603_0630'),
    ]

    # This needs to be a state-only operation because the database model was renamed, and no longer exists according to Django.
    state_operations = [
        migrations.DeleteModel(
            name='Tires',
        ),
    ]

    database_operations= [
        AlterModelTableR( 'tires', 'tires_tires' ),
        ]
    operations = [
        # After this state operation, the Django DB state should match the actual database structure.
       migrations.SeparateDatabaseAndState( state_operations=state_operations,
         database_operations=database_operations)
    ]   

【讨论】:

【参考方案6】:

我已经构建了一个管理命令来做到这一点 - 将模型从一个 Django 应用程序移动到另一个应用程序 - 基于 nostalgic.io 在https://***.com/a/30613732/1639699 的建议

你可以在 GitHub 上找到它alexei/django-move-model

【讨论】:

你好,这能修复 GenericForeign Key 吗? @injaon 我确实为相关模型创建了迁移。我没有使用通用 FK 对其进行测试,但您可以在 GitHub 上随意打开一个关于该问题的问题【参考方案7】:

您可以相对简单地执行此操作,但您需要遵循这些步骤,这些步骤是从Django Users' Group 中的一个问题中总结出来的。

    在将您的模型移动到新应用程序(我们将其称为new)之前,将db_table 选项添加到当前模型的Meta 类。我们将调用您要移动的模型M。但是如果你愿意,你可以一次做多个模型。

    class M(models.Model):
        a = models.ForeignKey(B, on_delete=models.CASCADE)
        b = models.IntegerField()
    
        class Meta:
            db_table = "new_M"
    

    运行python manage.py makemigrations。这会生成一个新的迁移文件,它将数据库中的表从current_M 重命名为new_M。我们稍后会将此迁移文件称为x

    现在将模型移至您的 new 应用程序。删除对db_table 的引用,因为Django 会自动将其放入名为new_M 的表中。

    进行新的迁移。运行python manage.py makemigrations。这将在我们的示例中生成 两个 新的迁移文件。第一个将在new 应用程序中。验证在 dependencies 属性中,Django 已经从之前的迁移文件中列出了 x。第二个将在current 应用程序中。现在在调用SeparateDatabaseAndState 时将两个迁移文件中的操作列表包装起来,如下所示:

    operations = [
        SeparateDatabaseAndState([], [
            migrations.CreateModel(...), ...
        ]),
    ]
    

    运行python manage.py migrate。你完成了。执行此操作的时间相对较快,因为与某些答案不同,您不会将记录从一个表复制到另一个表。您只是在重命名表,这本身就是一个快速操作。

【讨论】:

【参考方案8】:

这对我有用,但我相信我会听到为什么这是一个糟糕的主意。将此函数和调用它的操作添加到您的 old_app 迁移:

def migrate_model(apps, schema_editor):
    old_model = apps.get_model('old_app', 'MovingModel')
    new_model = apps.get_model('new_app', 'MovingModel')
    for mod in old_model.objects.all():
        mod.__class__ = new_model
        mod.save()


class Migration(migrations.Migration):

    dependencies = [
        ('new_app', '0006_auto_20171027_0213'),
    ]

    operations = [
        migrations.RunPython(migrate_model),
        migrations.DeleteModel(
            name='MovingModel',
        ),
    ]     

第 1 步:备份您的数据库! 确保首先运行 new_app 迁移,和/或 old_app 迁移的要求。在完成 old_app 迁移之前,请拒绝删除过时的内容类型。

在 Django 1.9 之后,您可能需要更仔细地逐步完成: 迁移1:创建新表 Migration2:填充表 Migration3:更改其他表上的字段 Migration4:删除旧表

【讨论】:

【参考方案9】:

几个月后回到这个问题(在成功实施 Lucianovici 的方法之后),在我看来,如果您注意将 db_table 指向旧表(如果您只关心代码组织而不介意数据库中过时的名称)。

您不需要 AlterModelTable 迁移,因此不需要自定义第一步。 您仍然需要在不接触数据库的情况下更改模型和关系。

所以我所做的只是从 Django 中获取自动迁移并将它们包装到 migrations.SeparateDatabaseAndState 中。

请注意(再次),这只有在您注意将 db_table 指向每个模型的 old 表时才有效。

我不确定这是否有问题,我还没有看到,但它似乎在我的开发系统上工作(当然,我注意备份)。所有数据看起来都完好无损。我会仔细看看有没有问题...

也许以后也可以在单独的步骤中重命名数据库表,从而使整个过程不那么复杂。

【讨论】:

【参考方案10】:

这有点晚了,但如果你想要最简单的路径并且不要太在意保留你的迁移历史。简单的解决方案就是擦除迁移并刷新。

我有一个相当复杂的应用程序,在尝试了上述解决方案几个小时都没有成功后,我意识到我可以做到。

rm cars/migrations/*
./manage.py makemigrations
./manage.py migrate --fake-initial

快!如果需要,迁移历史记录仍在 Git 中。而且由于这本质上是无操作的,所以回滚不是问题。

【讨论】:

以上是关于在具有所需 ForeignKey 引用的 Django (1.8) 应用程序之间移动模型的主要内容,如果未能解决你的问题,请参考以下文章

引用具有多个共同父项的子实体

外键(FK_ 必须与引用的主键具有相同的列数

保存前需要检查 ForeignKey 引用特定对象

django:自相关 ForeignKey 字段的相关名称不起作用 |在模板中获取相反方向的自引用

通过 ForeignKey 外部引用过滤的汇总子查询注释

有没有办法只引用 Django ForeignKey 中的某些字段?