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

Move models between Django (1.8) apps with required ForeignKey references

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

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

非常感谢任何帮助!

正在应用之间迁移模型。

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

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

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

问题

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

factory
  |_ cars
    |_ Car
    |_ Tires

Car 模型与 Tires 有外键关系。 (例如,您通过汽车型号指定轮胎)。

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

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

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

解决方案

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

浏览step 1.

的代码

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

查看step 2.

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

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

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

django.db.utils.IntegrityError: (1217, 'Cannot delete or update a parent row: a foreign key constraint fails')

查看目前在 step 3.

中的代码和迁移

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

您可以跟随并在 step 4. 中查看带有评论的最终迁移 我确实测试了它以验证它是否有效。

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

您可以创建一个空迁移:

./manage.py makemigrations --empty cars

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

在第一次自定义迁移中,我将只执行 "database_operations" 迁移。 Django 为您提供了拆分 "state" 和 "database" 操作的选项。您可以通过查看 code here.

了解这是如何完成的

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

现在您已准备好修改初始 tires 迁移。

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

操作没问题,但我们只想修改 "state" 而不是数据库。为什么?因为我们保留来自 cars 应用程序的数据库 tables。此外,您需要确保之前进行的自定义迁移是此迁移的依赖项。查看轮胎 migration file.

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

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

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

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

步骤 4.d. 清理 old_app.

中的陈旧模型

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

刚刚将两个模型从 old_app 移动到 new_app,但 FK 引用来自 app_xapp_y 的某些模型,而不是 [=] 的模型11=].

在这种情况下,请按照 Nostalg.io 提供的步骤进行操作,如下所示:

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

备注:

  • 来自其他应用程序的所有已编辑的自动生成的迁移文件都依赖于来自 old_app 的自定义迁移文件,其中 AlterModelTable 用于重命名 table(s)。 (在步骤 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

这对我有用,但我相信我会听到为什么这是一个糟糕的主意。将此函数和调用它的操作添加到您的 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 迁移是 运行,and/or 是 old_app 迁移的要求。在您完成 old_app 迁移之前拒绝删除过时的内容类型。

在 Django 1.9 之后,您可能需要更仔细地单步执行:
迁移 1:新建 table
迁移 2:填充 table
Migration3:更改其他 tables
上的字段 Migration4:删除旧的table

如果您需要移动模型并且您无法再访问该应用程序(或者您不想访问),您可以创建一个新的操作并考虑仅在以下情况下创建一个新模型迁移的模型不存在。

在此示例中,我将 '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))
            ],
        ),
    ]

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

如果您的 Django 模型使用 HistoricalRecords 字段,请不要忘记在遵循@Nostalg.io 答案时添加额外的 models/tables。

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

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

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

    migrations.DeleteModel(name='HistoricalModleName'),

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)
    ]   

我已经构建了一个管理命令来做到这一点 - 将模型从一个 Django 应用程序移动到另一个 - 基于 nostalgic.io 在

的建议

您可以在 GitHub alexei/django-move-model

上找到它

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

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

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

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

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

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

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

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

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

急!如果我需要,迁移历史仍在 Git 中。由于这本质上是空操作,回滚不是问题。

您可以相对直接地执行此操作,但您需要按照 Django Users' Group.

中的一个问题总结的这些步骤进行操作
  1. 在将您的模型移动到我们称之为 new 的新应用程序之前,请将 db_table 选项添加到当前模型的 Meta class.我们将调用您要移动的模型 M。但是如果你愿意,你可以一次做多个模型。

    class M(models.Model):
        a = models.ForeignKey(B, on_delete=models.CASCADE)
        b = models.IntegerField()
    
        class Meta:
            db_table = "new_M"
    
  2. 运行python manage.py makemigrations。这将生成一个新的迁移文件,将数据库中的 table 从 current_M 重命名为 new_M。稍后我们将此迁移文件称为 x

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

  4. 的 table 中
  5. 进行新的迁移。 运行 python manage.py makemigrations。在我们的示例中,这将生成 two 个新的迁移文件。第一个将在 new 应用程序中。确认在依赖项 属性 中,Django 已从之前的迁移文件中列出 x。第二个将在 current 应用程序中。现在将操作列表包装在对 SeparateDatabaseAndState 的调用中的两个迁移文件中,如下所示:

    operations = [
        SeparateDatabaseAndState([], [
            migrations.CreateModel(...), ...
        ]),
    ]
    
  6. 运行python manage.py migrate。你完成了。这样做的时间相对较快,因为与某些答案不同,您不会将记录从一个 table 复制到另一个。您只是重命名 tables,这本身就是一项快速操作。