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

时间:2015-06-02 16:05:58

标签: python database django-models schema-migration

这是此问题的扩展: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之间,等等。

非常感谢任何帮助!

10 个答案:

答案 0 :(得分:70)

在应用之间迁移模型。

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

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

我正在使用MySQL。 (不,那些不是我真正的凭据)。

问题

我使用的示例是一个带有汽车应用的工厂项目,该应用最初具有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并查看末日的错误消息

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

到目前为止查看step 3.

中的代码和迁移

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

您可以跟随并查看step 4.中包含评论的最终迁移。我对此进行了测试以验证其是否有效。

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

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

./manage.py makemigrations --empty cars

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

在第一次自定义迁移中,我只会执行" database_operations"移民。 Django为您提供了拆分状态" state"和"数据库"操作。您可以通过查看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状态。

答案 1 :(得分:3)

刚刚将两个模型从old_app移到new_app,但FK引用来自app_xapp_y的某些模型,而不是来自{{1}的模型}。

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

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

注意:

  • 来自其他应用的所有已编辑的自动生成的迁移文件都依赖于来自DeleteModel的自定义迁移文件,其中old_app用于重命名表。 (在步骤4.a中创建。)
  • 就我而言,我必须从AlterModelTable删除自动生成的迁移文件,因为我没有old_app次操作,只有AlterField和{{1}操作。或者保持空DeleteModel
  • 要避免从头开始创建测试数据库时出现迁移异常,请确保在步骤4.a创建的RemoveField自定义迁移。拥有其他应用程序之前的所有迁移依赖项。

    operations = []

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

答案 2 :(得分:1)

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

在这个例子中,我传递的是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))
            ],
        ),
    ]

答案 3 :(得分:1)

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

如果使用HistoricalRecords字段的Django模型在忘记@Nostalg.io回答的同时不忘记添加附加模型/表。

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

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

并在最后一步(4.d)中向state_operations添加其他删除:

    migrations.DeleteModel(name='HistoricalModleName'),

答案 4 :(得分:0)

这对我有用但我相信我会听到为什么这是一个糟糕的主意。添加此函数以及将其调用到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:删除旧表

答案 5 :(得分:0)

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

#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 :(得分:0)

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

您可以在alexei/django-move-model

的GitHub上找到它

答案 7 :(得分:0)

几个月后(成功实施Lucianovici的方法之后),回到这一点,在我看来,如果您小心地将db_table指向旧的,它会变得很多表(如果您只关心代码组织,而又不介意数据库中的过时名称)。

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

所以我所做的就是从Django进行自动迁移,并将其包装到migrations.SeparateDatabaseAndState中。

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

我不确定这有什么问题,但我似乎还没有发现,但是它似乎已经在我的devel系统上工作了(当然,我已经做好了备份工作)。所有数据看起来都是完整的。我会仔细检查一下是否有问题...

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

答案 8 :(得分:0)

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

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

setMessageRetentionDuration

Presto!如果需要,迁移历史记录仍在Git中。而且由于这本质上是无人值守的,因此无需担心回滚。

答案 9 :(得分:0)

您可以相对直接地执行此操作,但是您需要执行以下步骤,这些步骤是Django Users' Group中的一个问题的总结。

  1. 在将模型移至我们将称为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"
    
  2. 运行python manage.py makemigrations。这将生成一个新的迁移文件,该文件会将数据库中的表从current_M重命名为new_M。稍后,我们将将此迁移文件称为x

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

  4. 进行新的迁移。运行python manage.py makemigrations。在我们的示例中,这将生成 两个 新的迁移文件。第一个将在new应用中。验证Django在依赖项属性中是否列出了先前迁移文件中的x。第二个将在current应用中。现在,通过调用SeparateDatabaseAndState将操作列表包装在两个迁移文件中,如下所示:

    operations = [
        SeparateDatabaseAndState([], [
            migrations.CreateModel(...), ...
        ]),
    ]
    
  5. 运行python manage.py migrate。大功告成执行此操作的时间相对较快,因为与某些答案不同,您不是将记录从一个表复制到另一个表。您只是重命名表,这本身就是一项快速操作。