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

How to move a model between two Django apps (Django 1.7)

所以大约一年前我开始了一个项目,像所有的新开发人员一样,我并没有太多地关注结构,但是现在我和Django更进一步,我的项目布局主要是我的模型在结构上很糟糕。

我的模型主要放在一个应用程序中,实际上大多数模型都应该放在自己的应用程序中,我确实尝试解决了这个问题,并将它们向南移动,但是我发现这很棘手,而且由于使用了外置钥匙等原因,确实很难实现。

然而,由于django 1.7和内置的迁移支持,现在有更好的方法来实现这一点吗?


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

从旧应用中删除

1
python manage.py makemigrations old_app --empty

在迁移过程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,然后:

1
python manage.py makemigrations new_app

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
    ]


我也遇到了同样的问题。奥赞的回答帮助了我很多,但不幸的是还不够。事实上,我有几个外星键链接到我想要移动的模型。头痛过后,我找到了解决办法,所以决定把它贴出来解决人的时间。

您还需要两个步骤:

  • 在做任何事情之前,把所有连接到TheModelForeignKey更改为Integerfield。然后运行python manage.py makemigrations
  • 在完成了Ozan的步骤之后,重新转换您的外键:将ForeignKey(TheModel)放回,而不是IntegerField()。然后再进行迁移(python manage.py makemigrations)。然后您可以迁移,它应该可以工作(python manage.py migrate)
  • 希望有帮助。当然,在试生产之前先在当地测试一下,以避免不好的惊吓。


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

    情况

    app1.您的模式

    但你想让它转到:附录2.你的模型

  • 将您的模式(代码)从App1复制到App2。
  • 将此添加到app2.yourmodel:

    1
    2
    Class Meta:
        db_table = 'app1_yourmodel'
  • $python manage.py makemigrations应用程序2

  • 在app2中使用migrations.createModel()语句进行新的迁移(例如,0009_auto_something.py),将此语句移动到app2的初始迁移(例如,0001_initial.py)(就像它一直存在一样)。现在移除创建的migration=0009_auto_something.py

  • 正如您所做的,就像app2.yourmodel一直在那里一样,现在从迁移中删除app1.yourmodel的存在。意思是:注释掉createModel语句,以及之后使用的每个调整或数据迁移。

  • 当然,每个对app1.yourmodel的引用都必须通过项目更改为app2.yourmodel。另外,不要忘记迁移中app1.yourmodel的所有可能的外键都必须更改为app2.yourmodel。

  • 现在,如果您执行$python manage.py迁移,则不会发生任何更改,而且当您执行$python manage.py makemigrations时,也不会检测到任何新的更改。

  • 现在,最后一步:从app2.yourmodel中移除类meta,并执行$python manage.py makemigrations app2&;python manage.py migrate app2(如果您查看此迁移,您将看到类似的情况:)

    1
    2
    3
    4
        migrations.AlterModelTable(
        name='yourmodel',
        table=None,
    ),

  • table=none,表示它将采用默认的表名,在本例中是app2 yourmodel。

  • 完成,保存数据。
  • p.s在迁移过程中,它将看到内容"type app1.yourmodel"已被删除,可以删除。你可以这样说,但只有当你不使用它。如果您严重依赖于FK使该内容类型保持完整,请不要回答"是"或"否",而是手动进入数据库,删除ContentType App2.YourModel,并将ContentType App1.YourModel重命名为App2.YourModel,然后继续回答"否"。


    我正在删除旧的答案,因为这可能会导致数据丢失。正如Ozan提到的,我们可以在每个应用程序中创建两个迁移。

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

    1
    $ python manage.py makemigrations old_app --empty

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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)
        ]

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

    1
    $ python manage.py makemigrations new_app

    把迁移文件编辑成这样的文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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)
        ]


    我感到紧张的手编码迁移(按照Ozan的回答要求),因此下面结合了Ozan和Michael的策略,以尽量减少所需的手编码量:好的。

  • 在移动任何模型之前,通过运行makemigrations,确保您使用的是一个干净的基线。
  • 将模型的代码从app1移到app2中。
  • 按照@michael的建议,我们使用"新"模型上的db_tablemeta选项将新模型指向旧的数据库表:好的。

    1
    2
    class Meta:
        db_table = 'app1_yourmodel'
  • 运行makemigrations。这将在app2中生成CreateModel,在app1中生成DeleteModel。从技术上讲,这些迁移引用完全相同的表,并将删除(包括所有数据)并重新创建该表。好的。

  • 实际上,我们不想(或不需要)对桌子做任何事情。我们只需要Django相信改变已经发生。根据@ozan的回答,SeparateDatabaseAndState中的state_operations标志就是这样做的。所以我们用SeparateDatabaseAndState(state_operations=[...])把所有migrations条目包装在两个迁移文件中。例如,好的。

    1
    2
    3
    4
    5
    6
    7
    operations = [
        ...
        migrations.DeleteModel(
            name='YourModel',
        ),
        ...
    ]

    变成好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    operations = [
        migrations.SeparateDatabaseAndState(state_operations=[
            ...
            migrations.DeleteModel(
                name='YourModel',
            ),
            ...
        ])
    ]
  • 编辑:您还需要确保新的"虚拟"CreateModel迁移依赖于实际创建或更改原始表的任何迁移。例如,如果您的新迁移是app2.migrations.0004_auto_(对于Create)和app1.migrations.0007_auto_(对于Delete),最简单的做法是:好的。

    • 打开app1.migrations.0007_auto_并复制其app1依赖项(如('app1', '0006...'),)。这是app1中的"立即优先"迁移,应该包括对所有实际模型构建逻辑的依赖。
    • 打开app2.migrations.0004_auto_并将刚才复制的依赖项添加到其dependencies列表中。
    • 小精灵

      编辑:如果您与要移动的模型有ForeignKey关系,则上述操作可能不起作用。这是因为:好的。

      • 不会为ForeignKey更改自动创建依赖项
      • 我们不想包装ForeignKey中的state_operations更改,因此我们需要确保它们与表操作分开。
      • 小精灵

        "最小"操作集因情况而异,但以下步骤适用于大多数/所有ForeignKey迁移:好的。

      • 将模型从app1复制到app2,设置db_table,但不要更改任何fk引用。
      • 运行makemigrations并在state_operations中包装所有app2迁移(见上文)
        • 如上所述,在app2CreateTable中添加对最新app1迁移的依赖项。
        • 小精灵

        • 将所有FK引用指向新模型。如果不使用字符串引用,请将旧模型移动到models.py的底部(不要删除它),这样它就不会与导入的类竞争。
        • 运行makemigrations,但不要在state_operations中包装任何内容(实际上应该发生fk更改)。在所有ForeignKey迁移(即AlterField迁移)中添加一个依赖项到app2中的CreateTable迁移(下一步需要这个列表,所以要跟踪它们)。例如:好的。

          • 找到包含CreateModel的迁移,例如app2.migrations.0002_auto_并复制该迁移的名称。
          • 查找具有该模型的foreignkey的所有迁移(例如,通过搜索app2.YourModel查找如下迁移:好的。

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            class Migration(migrations.Migration):

                dependencies = [
                    ('otherapp', '0001_initial'),
                ]

                operations = [
                    migrations.AlterField(
                        model_name='relatedmodel',
                        name='fieldname',
                        field=models.ForeignKey(... to='app2.YourModel'),
                    ),
                ]
          • CreateModel迁移作为依赖项添加:好的。

            1
            2
            3
            4
            5
            6
            class Migration(migrations.Migration):

                dependencies = [
                    ('otherapp', '0001_initial'),
                    ('app2', '0002_auto_<date>'),
                ]

          • 小精灵

          • app1中删除模型好的。

          • 运行makemigrations并在state_operations中包装app1迁移。
            • 从上一步(可能包括app1app2中的迁移)添加对所有ForeignKey迁移(即AlterField的依赖关系。
            • 当我建立这些迁移时,DeleteTable已经依赖于AlterField迁移,所以我不需要手动执行它(即AlterDelete早)。
            • 小精灵

              在这一点上,姜戈是好去。新模型指向旧表,Django的迁移使它确信所有的东西都被适当地重新定位了。(从@michael的回答中)最大的警告是,为新型号创建了一个新的ContentType。如果(例如,通过ForeignKey链接到内容类型),则需要创建一个迁移来更新ContentType表。好的。

              我想在自己之后进行清理(元选项和表名),因此使用了以下过程(来自@michael):好的。

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


              如果数据不太大或太复杂,但仍需维护,另一个黑客选择是:

              • 使用manage.py dumpdata获取数据设备
              • 正确进行模型更改和迁移,而不关联更改
              • 全局将旧模型和应用程序名称中的设备替换为新的
              • 使用manage.py load data加载数据
              • 小精灵


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

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

                在本例中,我将"mymodel"从旧的"app"传递到myapp。

                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                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))
                            ],
                        ),
                    ]


              • 将旧型号的名称更改为"型号名称旧"
              • 进行迁移
              • 在相关模型上创建具有相同关系的名为"modelu nameu new"的新模型(例如,用户模型现在有user.blog旧的和user.blog新的)
              • 进行迁移
              • 编写将所有数据迁移到新模型表的自定义迁移
              • 通过在运行迁移之前和之后将备份与新的DB拷贝进行比较,测试这些迁移的性能。
              • 当一切都满意时,删除旧型号
              • 进行迁移
              • 将新型号更改为正确的名称'modelu nameu new'->'modelu name'
              • 在临时服务器上测试整个迁移过程
              • 将生产站点关闭几分钟,以便在不受用户干扰的情况下运行所有迁移。
              • 对需要移动的每个模型分别执行此操作。我不建议做另一个答案所说的,换成整数,再换回外键。在迁移之后,新的外键可能会不同,行可能具有不同的ID,我不想在切换回外键时冒任何ID不匹配的风险。


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

                另一种解决方案是手动更改现有迁移。这个想法是,每次你看到一个操作改变了应用程序迁移中的模型,你就把这个操作复制到应用程序初始迁移的末尾。每次你在app-a的迁移中看到引用"app-a.themodel",你就会把它改为"app-b.themodel"。

                我只是为一个现有的项目做了这个,我想从中提取一个特定的模型到一个可重用的应用程序。手术进行得很顺利。我想如果有从app_b到app_a的引用的话事情会更难一些。另外,我的模型有一个手工定义的meta.db_表,这可能会有所帮助。

                值得注意的是,您最终会改变迁移历史。这并不重要,即使应用了原始迁移的数据库也是如此。如果原始迁移和重写迁移以相同的数据库模式结束,那么这样的重写应该是正常的。


                您可以尝试以下操作(未测试):

              • 将模型从src_app移到dest_app
              • 迁移dest_app;确保模式迁移依赖于最新的src_app迁移(https://docs.djangoproject.com/en/dev/topics/migrations/迁移文件)
              • 添加一个到dest_app的数据迁移,复制src_app的所有数据。
              • 迁移src_app;确保模式迁移依赖于dest_app的最新(数据)迁移,即步骤3的迁移
              • 请注意,您将复制整个表,而不是移动它,但这样两个应用程序就不必触摸属于另一个应用程序的表,我认为这更重要。


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

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

                为两个应用程序创建空迁移:

                1
                2
                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标签重命名为XXX1_dst_app_new。不要忘记添加对XXX0_src_app_old迁移的依赖。由此产生的XXX1_dst_app_new迁移是:

                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                33
                34
                35
                36
                37
                38
                39
                40
                41
                42
                43
                44
                45
                46
                47
                48
                49
                50
                51
                52
                53
                54
                55
                56
                57
                58
                59
                60
                61
                62
                63
                64
                65
                66
                67
                68
                69
                70
                71
                72
                73
                74
                75
                76
                77
                78
                79
                80
                81
                82
                83
                84
                85
                86
                87
                88
                89
                90
                91
                92
                93
                94
                95
                96
                97
                98
                99
                100
                101
                102
                # -*- 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_src_app_new中增加对XXX1_dst_app_new的依赖。XXX1_src_app_new不是为了确保将来的src_app迁移将在XXX1_dst_app_new之后执行而需要的OP迁移。

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

                1
                python manage.py migrate

                这就是全部!