ChatGPT解决这个技术问题 Extra ChatGPT

如何在两个 Django 应用程序之间移动模型(Django 1.7)

所以大约一年前我开始了一个项目,和所有新开发人员一样,我并没有真正关注结构,但是现在我与 Django 一起更进一步,它开始出现我的项目布局主要是我的模型在结构上很糟糕.

我的模型主要保存在一个应用程序中,实际上这些模型中的大多数应该在它们自己的单独应用程序中,我确实尝试解决这个问题并将它们向南移动,但是由于外键等,我发现它很棘手而且非常困难。

然而,由于 Django 1.7 并内置了对迁移的支持,现在有更好的方法吗?

您可能需要考虑更改接受的答案。
对于将来遇到此问题的人:此处为 Django 3.x,realpython.com/move-django-model/… 中详述的方法对我有用。我在旧应用程序中的模型和新应用程序中的模型之间有多个外键。
如果您要移动的模型是自定义 User 模型(或settings.py中引用并与之相关的任何其他模型),则移动会变得更加复杂。有关详细信息,请参阅 stackoverflow.com/questions/69473228/…

o
ozan

这可以使用 migrations.SeparateDatabaseAndState 相当容易地完成。基本上,我们使用数据库操作来重命名表,同时使用两个状态操作从一个应用程序的历史记录中删除模型并在另一个应用程序的历史记录中创建它。

从旧应用程序中删除

python manage.py makemigrations old_app --empty

在迁移中:

class Migration(migrations.Migration):

    dependencies = []

    database_operations = [
        migrations.AlterModelTable('TheModel', 'newapp_themodel')
    ]

    state_operations = [
        migrations.DeleteModel('TheModel')
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=database_operations,
            state_operations=state_operations)
    ]

添加到新应用

首先,将模型复制到新应用的model.py,然后:

python manage.py makemigrations new_app

这将生成一个以天真的 CreateModel 操作作为唯一操作的迁移。将其包装在 SeparateDatabaseAndState 操作中,这样我们就不会尝试重新创建表。还包括先前的迁移作为依赖项:

class Migration(migrations.Migration):

    dependencies = [
        ('old_app', 'above_migration')
    ]

    state_operations = [
        migrations.CreateModel(
            name='TheModel',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
            ],
            options={
                'db_table': 'newapp_themodel',
            },
            bases=(models.Model,),
        )
    ]

    operations = [
        migrations.SeparateDatabaseAndState(state_operations=state_operations)
    ]

真是很好的解释。这应该是答案,重命名表可以避免丢失任何数据。
这是最好的方法,它比我的要好得多。在我的答案顶部添加了注释。
我这样做了,但是当我在此之后在 newapp 上运行“makemigrations”时,它会生成一个 AlterModelTable 迁移,将其重命名为 None。
根据这些说明找到了解决我的问题的方法。如果您有必填字段的外键引用,则问题会更加复杂。我不得不添加几个步骤来移动参考。
由于多个请求,我使用 GitHub 示例创建了有关 FK 模型迁移的详细答案。 stackoverflow.com/questions/30601107/…
t
tomglynch

我正在删除旧答案,因为可能会导致数据丢失。作为 ozan mentioned,我们可以在每个应用中创建 2 个迁移。这篇文章下面的评论是指我的旧答案。

从第一个应用程序中删除模型的第一次迁移。

$ python manage.py makemigrations old_app --empty

编辑迁移文件以包含这些操作。

class Migration(migrations.Migration):

    database_operations = [migrations.AlterModelTable('TheModel', 'newapp_themodel')]

    state_operations = [migrations.DeleteModel('TheModel')]

    operations = [
      migrations.SeparateDatabaseAndState(
        database_operations=database_operations,
        state_operations=state_operations)
    ]

第二次迁移取决于第一次迁移并在第二个应用程序中创建新表。将模型代码移动到第二个应用程序后

$ python manage.py makemigrations new_app 

并将迁移文件编辑为这样的内容。

class Migration(migrations.Migration):

    dependencies = [
        ('old_app', 'above_migration')
    ]

    state_operations = [
        migrations.CreateModel(
            name='TheModel',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
            ],
            options={
                'db_table': 'newapp_themodel',
            },
            bases=(models.Model,),
        )
    ]

    operations = [
        migrations.SeparateDatabaseAndState(state_operations=state_operations)
    ]

我确实有现有的数据,其中很多是我根本无法丢失的,有可能这样做吗?
@KevinChristopherHenry 修改了代码。这将保留现有数据。
@SamBuckingham 是的,您可以尝试使用修改后的代码进行迁移而不会丢失数据。
我认为这确实是最好的方法,感谢所有帮助的人,这真是太棒了。
IMO 这是一个错误的解决方案,迁移的基本假设是,如果您运行 ./manage.py migrate,一切都会以良好状态结束。手动伪造迁移是 IMO 错误的方法。
C
Community

我遇到了同样的问题。 Ozan's answer 帮了我很多,但不幸的是还不够。事实上,我有几个 ForeignKey 链接到我想要移动的模型。经过一番头痛后,我找到了解决方案,因此决定将其发布以解决人们的时间问题。

您还需要 2 个步骤:

在做任何事情之前,将所有链接到 TheModel 的 ForeignKey 更改为 Integerfield。然后运行 python manage.py makemigrations 完成 Ozan 的步骤后,重新转换你的外键:放回 ForeignKey(TheModel) 而不是 IntegerField()。然后再次进行迁移(python manage.py makemigrations)。然后您可以迁移,它应该可以工作(python manage.py migrate)

希望能帮助到你。当然,在尝试生产之前先在本地进行测试,以避免出现意外:)


ManyToManyField 关系呢?
@tomcounsell 很棒的评论,我假设仅出于迁移的目的添加特定的直通模型。保持数据完整需要大量工作......
由于多对多关系通常只是具有两个外键的表,因此从 SQL 的角度来看,您可以应用此答案的技巧。但是为了仅通过 Django 实现这一点,我能想到的一种方法是按照 @ozan 的回答,除了第一步是复制 MTM 关系中涉及的表(每个应用程序中的一个版本的骗子) ,将所有外键迁移到新应用程序,然后才删除旧应用程序中的欺骗。免责声明:我没有测试过:)
M
Michael van de Waeter

我是怎么做到的(在 Django==1.8 上测试,使用 postgres,所以可能也是 1.7)

情况

app1.YourModel

但您希望它转到:app2.YourModel

将 YourModel(代码)从 app1 复制到 app2。将此添加到 app2.YourModel: Class Meta: db_table = 'app1_yourmodel' $ python manage.py makemigrations app2 在 app2 中使用 migrations.CreateModel() 语句进行新迁移(例如 0009_auto_something.py),将此语句移动到初始app2 的迁移(例如 0001_initial.py)(就像它一直在那里一样)。现在删除创建的迁移 = 0009_auto_something.py 就像你行动一样,就像 app2.YourModel 一直存在一样,现在从你的迁移中删除 app1.YourModel 的存在。含义:注释掉 CreateModel 语句,以及之后您使用的每个调整或数据迁移。当然,每个对 app1.YourModel 的引用都必须通过您的项目更改为 app2.YourModel。另外,不要忘记迁移中 app1.YourModel 的所有可能外键都必须更改为 app2.YourModel 现在,如果您执行 $ python manage.py migrate,则没有任何变化,即使您执行 $ python manage.py makemigrations ,未检测到任何新内容。现在画龙点睛:从 app2.YourModel 中删除 Class Meta 并执行 $ python manage.py makemigrations app2 && python manage.py migrate app2 (如果您查看此迁移,您会看到类似这样的内容:) migrations.AlterModelTable( name ='你的模型',表=无,),

table=None,意味着它将采用默认的表名,在本例中为 app2_yourmodel。

完成,数据已保存。

PS 在迁移过程中它会看到 content_type app1.yourmodel 已被删除并且可以删除。您可以对此说“是”,但前提是您不使用它。如果您严重依赖它来使该内容类型的 FK 完好无损,请不要回答是或否,而是手动进入数据库,然后删除内容类型 app2.yourmodel,然后重命名内容类型 app1。 yourmodel 到 app2.yourmodel,然后回答 no 继续。


虽然这个解决方案绝对比@ozan 的解决方案更“hackier”,而且它肯定需要更多的编辑,但它对我来说效果很好(编辑迁移是可以的——根据文档,它们应该是可编辑的)。
可能还使用 app_label = 'app1' 元选项。
天才!这对我来说非常适合 ForeignKey 关系。我想这也适用于多对多领域。
我按照您的步骤操作,但属于 app1 的某个模型中的字段包含一个外键,该外键与要移动的模型(myModel)具有递归关系。就像 field1 = models.ForeignKey('app1.myModel'). 当我迁移时,我收到一个 ValueError 说明 field1 was declared with a lazy reference to 'app1.myModel' but app 'app1' doesn't provide model 'MyModel'
c
claytond

我对手工编码迁移感到紧张(正如 Ozan's 答案所要求的),因此以下结合了 Ozan 和 Michael's 的策略,以尽量减少所需的手工编码量:

在移动任何模型之前,请确保通过运行 makemigrations 使用干净的基线。将模型的代码从 app1 移动到 app2 根据@Michael 的建议,我们使用“新”模型上的 db_table Meta 选项将新模型指向旧数据库表: class Meta: db_table = 'app1_yourmodel' 运行 makemigrations。这将在 app2 中生成 CreateModel,在 app1 中生成 DeleteModel。从技术上讲,这些迁移指的是完全相同的表,并且会删除(包括所有数据)并重新创建表。实际上,我们不想(或不需要)对桌子做任何事情。我们只需要 Django 相信已经做出了改变。根据@Ozan 的回答,SeparateDatabaseAndState 中的 state_operations 标志就是这样做的。因此,我们使用SeparateDatabaseAndState(state_operations=[...]) 将所有迁移条目包装在两个迁移文件中。例如,操作 = [ ... migrations.DeleteModel( name='YourModel', ), ... ] 变为操作 = [ migrations.SeparateDatabaseAndState(state_operations=[ ... migrations.DeleteModel( name='YourModel', ) , ... ]) ] 您还需要确保新的“虚拟” CreateModel 迁移依赖于实际创建或更改原始表的任何迁移。例如,如果您的新迁移是 app2.migrations.0004_auto_(用于创建)和 app1.migrations.0007_auto_(用于删除),那么最简单的操作是:打开 app1.migrations.0007_auto_< date> 并复制其 app1 依赖项(例如 ('app1', '0006...'),)。这是 app1 中的“紧接在前”的迁移,应该包括对所有实际模型构建逻辑的依赖。打开 app2.migrations.0004_auto_ 并将刚刚复制的依赖项添加到其依赖项列表中。

如果您与要移动的模型有 ForeignKey 个关系,则上述方法可能不起作用。发生这种情况是因为:

不会为 ForeignKey 更改自动创建依赖项

我们不想将 ForeignKey 更改包装在 state_operations 中,因此我们需要确保它们与表操作分开。

注意:Django 2.2 添加了一个警告 (models.E028) 会破坏此方法。您也许可以使用 managed=False 解决它,但我尚未对其进行测试。

“最小”操作集因情况而异,但以下过程应适用于大多数/所有 ForeignKey 迁移:

将模型从 app1 复制到 app2,设置 db_table,但不要更改任何 FK 引用。运行 makemigrations 并将所有 app2 迁移包装在 state_operations 中(见上文)如上,将 app2 CreateTable 中的依赖项添加到最新的 app1 迁移中,将所有 FK 引用指向新模型。如果您不使用字符串引用,请将旧模型移动到 models.py 的底部(不要删除它),这样它就不会与导入的类竞争。运行 makemigrations 但不要在 state_operations 中包含任何内容(实际上应该发生 FK 更改)。将所有 ForeignKey 迁移(即 AlterField)中的依赖项添加到 app2 中的 CreateTable 迁移(下一步将需要此列表,因此请跟踪它们)。例如:找到包含 CreateModel 的迁移,例如 app2.migrations.0002_auto_ 并复制该迁移的名称。查找具有该模型的 ForeignKey 的所有迁移(例如,通过搜索 app2.YourModel 以查找迁移,例如: class Migration(migrations.Migration): dependencies = [ ('otherapp', '0001_initial'), ] operations = [ migrations.AlterField ( model_name='relatedmodel', name='fieldname', field=models.ForeignKey(... to='app2.YourModel'), ), ] 添加 CreateModel 迁移作为依赖项:class Migration(migrations.Migration) : dependencies = [ ('otherapp', '0001_initial'), ('app2', '0002_auto_'), ] 从 app1 中移除模型 运行 makemigrations 并将 app1 迁移包装在 state_operations 中。为所有上一步的 ForeignKey 迁移(即 AlterField)(可能包括 app1 和 app2 中的迁移)。当我构建这些迁移时,DeleteTable 已经依赖于 AlterField 迁移,因此我不需要手动强制执行它(即 Alter before Delete) .

在这一点上,Django 很好。新模型指向旧表,Django 的迁移已经说服它所有内容都已适当地重新定位。最大的警告(来自@Michael 的回答)是为新模型创建了一个新的 ContentType。如果您链接(例如通过 ForeignKey)到内容类型,则需要创建迁移以更新 ContentType 表。

我想自己清理(元选项和表名),所以我使用了以下过程(来自@Michael):

删除 db_table 元条目 再次运行 makemigrations 以生成数据库重命名 编辑最后一次迁移并确保它依赖于 DeleteTable 迁移。似乎没有必要,因为删除应该是纯逻辑的,但如果我不这样做,我会遇到错误(例如 app1_yourmodel 不存在)。


这工作得很好,谢谢!我认为编辑最后一次迁移并不重要,因为它无论如何都位于依赖关系树的底部。
好答案!我认为您需要在 migrations.SeparateDatabaseAndState 中添加右括号,对吗?
这对我有用。我也没有像@JamesMeakin 那样编辑最后一次迁移(第 3 步,整个答案的最后一行),它仍然可以正常工作
在第二种情况下,带有 FK 的那个,第二步对我来说失败了,出现了一个有意义的错误:table_name: (models.E028) db_table 'table_name' is used by multiple models: app1.Model, app2.Model.
我已经使用了几次该程序。如果您比较 2.2 (docs.djangoproject.com/en/2.2/ref/checks) 和 2.1 (docs.djangoproject.com/en/2.1/ref/checks) 的文档,您会发现它是在 2.2 中添加的。可能可以使用 managed=False,但我无处可查。
W
Wtower

如果数据不大或太复杂,但维护仍然很重要,另一种 hacky 选择是:

使用 manage.py dumpdata 获取数据夹具

继续正确建模更改和迁移,而不关联更改

将旧模型和应用程序名称中的固定装置全局替换为新的

使用 manage.py loaddata 加载数据


G
Gal Singer

复制自我在 https://stackoverflow.com/a/47392970/8971048 的回答

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

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

请不要对多个问题添加相同的答案。回答最好的一个并将其余的标记为重复。请参阅Is it acceptable to add a duplicate answer to several questions?
S
Sergey Fedoseev

这是经过粗略测试的,所以不要忘记备份你的数据库!!!

例如,有两个应用程序:src_appdst_app,我们要将模型 MoveMesrc_app 移动到 dst_app

为两个应用创建空迁移:

python manage.py makemigrations --empty src_app
python manage.py makemigrations --empty dst_app

假设,新的迁移是 XXX1_src_app_newXXX1_dst_app_new,之前的顶级迁移是 XXX0_src_app_oldXXX0_dst_app_old

添加重命名 MoveMe 模型的表并将其在 ProjectState 中的 app_label 重命名为 XXX1_dst_app_new 的操作。不要忘记添加对 XXX0_src_app_old 迁移的依赖。生成的 XXX1_dst_app_new 迁移是:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations

# this operations is almost the same as RenameModel
# https://github.com/django/django/blob/1.7/django/db/migrations/operations/models.py#L104
class MoveModelFromOtherApp(migrations.operations.base.Operation):

    def __init__(self, name, old_app_label):
        self.name = name
        self.old_app_label = old_app_label

    def state_forwards(self, app_label, state):

        # Get all of the related objects we need to repoint
        apps = state.render(skip_cache=True)
        model = apps.get_model(self.old_app_label, self.name)
        related_objects = model._meta.get_all_related_objects()
        related_m2m_objects = model._meta.get_all_related_many_to_many_objects()
        # Rename the model
        state.models[app_label, self.name.lower()] = state.models.pop(
            (self.old_app_label, self.name.lower())
        )
        state.models[app_label, self.name.lower()].app_label = app_label
        for model_state in state.models.values():
            try:
                i = model_state.bases.index("%s.%s" % (self.old_app_label, self.name.lower()))
                model_state.bases = model_state.bases[:i] + ("%s.%s" % (app_label, self.name.lower()),) + model_state.bases[i+1:]
            except ValueError:
                pass
        # Repoint the FKs and M2Ms pointing to us
        for related_object in (related_objects + related_m2m_objects):
            # Use the new related key for self referential related objects.
            if related_object.model == model:
                related_key = (app_label, self.name.lower())
            else:
                related_key = (
                    related_object.model._meta.app_label,
                    related_object.model._meta.object_name.lower(),
                )
            new_fields = []
            for name, field in state.models[related_key].fields:
                if name == related_object.field.name:
                    field = field.clone()
                    field.rel.to = "%s.%s" % (app_label, self.name)
                new_fields.append((name, field))
            state.models[related_key].fields = new_fields

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        old_apps = from_state.render()
        new_apps = to_state.render()
        old_model = old_apps.get_model(self.old_app_label, self.name)
        new_model = new_apps.get_model(app_label, self.name)
        if self.allowed_to_migrate(schema_editor.connection.alias, new_model):
            # Move the main table
            schema_editor.alter_db_table(
                new_model,
                old_model._meta.db_table,
                new_model._meta.db_table,
            )
            # Alter the fields pointing to us
            related_objects = old_model._meta.get_all_related_objects()
            related_m2m_objects = old_model._meta.get_all_related_many_to_many_objects()
            for related_object in (related_objects + related_m2m_objects):
                if related_object.model == old_model:
                    model = new_model
                    related_key = (app_label, self.name.lower())
                else:
                    model = related_object.model
                    related_key = (
                        related_object.model._meta.app_label,
                        related_object.model._meta.object_name.lower(),
                    )
                to_field = new_apps.get_model(
                    *related_key
                )._meta.get_field_by_name(related_object.field.name)[0]
                schema_editor.alter_field(
                    model,
                    related_object.field,
                    to_field,
                )

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        self.old_app_label, app_label = app_label, self.old_app_label
        self.database_forwards(app_label, schema_editor, from_state, to_state)
        app_label, self.old_app_label = self.old_app_label, app_label

    def describe(self):
        return "Move %s from %s" % (self.name, self.old_app_label)


class Migration(migrations.Migration):

    dependencies = [
       ('dst_app', 'XXX0_dst_app_old'),
       ('src_app', 'XXX0_src_app_old'),
    ]

    operations = [
        MoveModelFromOtherApp('MoveMe', 'src_app'),
    ]

XXX1_dst_app_new 的依赖项添加到 XXX1_src_app_newXXX1_src_app_new 是无操作迁移,需要确保将来的 src_app 迁移将在 XXX1_dst_app_new 之后执行。

MoveMesrc_app/models.py 移动到 dst_app/models.py。然后运行:

python manage.py migrate

就这样!


请注意,此代码可能仅对 django 1.7 有用。在 django 2.0 中尝试这个是行不通的。这也意味着使用这种机制来移动模型会增加升级 django 版本的维护开销。
W
Webthusiast

您可以尝试以下(未经测试):

将模型从 src_app 移动到 dest_app 迁移 dest_app;确保架构迁移依赖于最新的 src_app 迁移 (https://docs.djangoproject.com/en/dev/topics/migrations/#migration-files) 添加一个数据迁移到 dest_app,从 src_app 复制所有数据 migrate src_app ;确保架构迁移依赖于 dest_app 的最新(数据)迁移——即:步骤 3 的迁移

请注意,您将复制整个表格,而不是移动它,但是这样两个应用程序都不必接触属于另一个应用程序的表格,我认为这更重要。


a
akaariai

假设您将模型 TheModel 从 app_a 移动到 app_b。

另一种解决方案是手动更改现有迁移。这个想法是,每次您在 app_a 的迁移中看到更改 TheModel 的操作时,您都会将该操作复制到 app_b 初始迁移的末尾。每次您在 app_a 的迁移中看到引用“app_a.TheModel”时,将其更改为“app_b.TheModel”。

我只是为一个现有项目做了这个,我想将某个模型提取到一个可重用的应用程序中。手续很顺利。我想如果有从 app_b 到 app_a 的引用,事情会变得更加困难。此外,我为我的模型手动定义了一个 Meta.db_table,这可能会有所帮助。

值得注意的是,您最终会改变迁移历史。这无关紧要,即使您有一个应用了原始迁移的数据库。如果原始迁移和重写迁移最终都具有相同的数据库模式,那么这种重写应该是可以的。


t
tomcounsell

将旧模型的名称更改为 'model_name_old' makemigrations 创建名为 'model_name_new' 的新模型,在相关模型上具有相同的关系(例如,用户模型现在具有 user.blog_old 和 user.blog_new) makemigrations 编写迁移所有数据的自定义迁移到新模型表通过在运行迁移之前和之后比较备份与新数据库副本来测试这些迁移的地狱当一切都令人满意时,删除旧模型 makemigrations 将新模型更改为正确的名称'model_name_new'->'model_name ' 在暂存服务器上测试全部迁移,让您的生产站点停机几分钟,以便在没有用户干预的情况下运行所有迁移

为需要移动的每个模型单独执行此操作。我不建议通过更改为整数并返回外键来做其他答案所说的那样有可能新的外键会有所不同,并且迁移后行可能具有不同的 ID,我不想冒任何风险切换回外键时不匹配的 id。