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


133

因此,大约一年前,我开始了一个项目,像所有新开发人员一样,我并没有真正专注于结构,但是现在我与Django一起走得更远,它开始似乎表明我的项目布局主要是我的模型在结构上很糟糕。

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

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


4
您可能需要考虑更改接受的答案。
巴肯·瓦尔达尼扬

对于将来遇到这种情况的人:此处为Django 3.x,realpython.com / move-django-model /…中详细介绍的方法对我有用。在旧应用程序的模型和新应用程序的模型之间,我有多个外键。
pradeepcep

Answers:


16

我正在删除旧答案,因为这可能会导致数据丢失。如ozan所述,我们可以在每个应用中创建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修改了代码。这将保留现有数据。
ChillarAnand 2014年

@SamBuckingham是的,您可以尝试使用修改后的代码进行迁移,而不会丢失数据。
ChillarAnand 2014年

2
我认为这确实将是最好的方法,感谢所有出色的帮助人员。
山姆·白金汉

1
IMO这是一个错误的解决方案,迁移的基本假设是,如果运行,./manage.py migrate一切都会以良好状态结束。手动伪造迁移是IMO的错误方法。
jb。

341

使用可以很容易地做到这一点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)
    ]

14
真的很好的解释。这应该是答案,通过重命名表可以避免丢失任何数据。
2014年

11
这是最好的方法,远胜于我的方法。在我的答案上方添加了注释。
ChillarAnand 2014年

4
我这样做了,但是当我在newapp上运行“ makemigrations”时,它会生成一个AlterModelTable迁移,并将其重命名为None。
Diego Ponciano

4
根据这些说明找到了解决我的问题的方法。如果您具有必填字段的外键引用,则问题会更加复杂。我必须添加几个步骤才能将引用移到上方。
Nostalg.io 2015年

14
由于有多个请求,我使用GitHub示例创建了有关FK模型迁移的详细答案。stackoverflow.com/questions/30601107/...
Nostalg.io

26

我遇到了同样的问题。 奥赞的回答对我有很大帮助,但不幸的是还不够。确实,我有几个ForeignKey链接到我想移动的模型。经过一番头痛之后,我发现了解决方案,因此决定发布该解决方案以解决人们的时间问题。

您还需要2个步骤:

  1. 在执行任何操作之前,请将所有ForeignKey链接更改TheModelIntegerfield。然后跑python manage.py makemigrations
  2. 完成Ozan的步骤后,请重新转换外键:放回ForeignKey(TheModel)而不是IntegerField()。然后再次进行迁移(python manage.py makemigrations)。然后,您可以迁移,它应该可以工作(python manage.py migrate

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


8
那ManyToManyField关系呢?
tomcounsell 2015年

1
@tomcounsell很好的评论,我假设仅出于迁移目的而添加特定的直通模型。要保持数据完整,需要进行大量工作……
Wtower

由于多对多关系通常只是一个带有两个外键的表,因此从SQL的角度来看,您可以应用此答案的技巧。但是,为了仅通过Django实现此目标,我可以想到的一种方法是遵循@ozan答案,除非第一步是复制涉及MTM关系的表(每个应用程序中有一个重复版本) ,将所有外键迁移到新应用程序,然后再删除旧应用程序中的重复对象。免责声明:我尚未测试过:)
Arnaud P

15

我是如何做到的(在PostgreSQL == 1.8上进行了测试,使用postgres,可能也是1.7)

情况

app1.YourModel

但您希望它转到: app2.YourModel

  1. 将YourModel(代码)从app1复制到app2。
  2. 将此添加到app2.YourModel:

    Class Meta:
        db_table = 'app1_yourmodel'
  3. $ python manage.py makemigrations app2

  4. 使用migrations.CreateModel()语句在app2中进行了新的迁移(例如0009_auto_something.py),将该语句移至app2的初始迁移(例如0001_initial.py)(就像它一直在那儿一样)。现在删除创建的迁移= 0009_auto_something.py

  5. 就像您执行操作一样,就像app2.YourModel一直存在一样,现在从迁移中删除app1.YourModel的存在。含义:注释掉CreateModel语句,以及之后使用的所有调整或数据迁移。

  6. 当然,必须在您的项目中将对app1.YourModel的每个引用都更改为app2.YourModel。另外,不要忘记迁移中所有可能的app1.YourModel外键都必须更改为app2.YourModel。

  7. 现在,如果您执行$ python manage.py迁移,则什么都没有改变,同样,当您执行$ python manage.py makemigrations时,也不会检测到新的东西。

  8. 现在画龙点睛:从app2.YourModel中删除Class Meta并执行$ python manage.py makemigrations app2 && python manage.py migration app2(如果您研究此迁移,您将看到类似以下的内容:)

        migrations.AlterModelTable(
        name='yourmodel',
        table=None,
    ),

table = None,表示它将采用默认的表名,在这种情况下为app2_yourmodel。

  1. 完成,保存数据。

PS在迁移期间,它将看到content_type app1.yourmodel已被删除并且可以删除。您可以对此说是,但前提是您不使用它。如果您非常依赖它来使该内容类型的FK保持完整,则不要回答yes或no,而是手动进入该时间的db,并删除co​​ntentype app2.yourmodel,然后重命名contenttype app1。 yourmodel到app2.yourmodel,然后继续回答否。


3
虽然此解决方案绝对比@ozan的解决方案“黑客”多,而且肯定需要更多编辑,但对我来说效果很好(并且可以编辑迁移-根据文档,它们应该是可编辑的)。
pgcd,2016年

1
可能还使用app_label = 'app1'meta选项。
Wtower

天才!对于我与ForeignKey关系而言,这非常有用。我想这也将适用于ManyToMany领域。
巴本·瓦尔丹扬

我按照您的步骤进行操作,但是属于app1的某些模型中的字段由与要移动的model(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'
Deesha

12

我会感到紧张的手工编码迁移(这是Ozan的回答所要求),因此以下内容结合了Ozan和Michael的策略以最大程度地减少所需的手工编码量:

  1. 在移动任何模型之前,请通过运行确保使用干净的基线makemigrations
  2. 将模型的代码从app1移至app2
  3. 按照@Michael的建议,我们使用db_table“新”模型上的Meta选项将新模型指向旧数据库表:

    class Meta:
        db_table = 'app1_yourmodel'
  4. 运行makemigrations。这将CreateModelapp2DeleteModel中生成app1。从技术上讲,这些迁移将引用完全相同的表,并且将删除(包括所有数据)并重新创建表。

  5. 实际上,我们不希望(或不需要)对表做任何事情。我们只需要Django相信所做的更改即可。根据@Ozan的回答,中的state_operations标志将SeparateDatabaseAndState执行此操作。因此,我们使用来将所有migrations条目包装在两个迁移文件SeparateDatabaseAndState(state_operations=[...])。例如,

    operations = [
        ...
        migrations.DeleteModel(
            name='YourModel',
        ),
        ...
    ]

    变成

    operations = [
        migrations.SeparateDatabaseAndState(state_operations=[
            ...
            migrations.DeleteModel(
                name='YourModel',
            ),
            ...
        ])
    ]
  6. 您还需要确保新的“虚拟” CreateModel迁移取决于实际创建或更改原始表的任何迁移。例如,如果您的新迁移是app2.migrations.0004_auto_<date>(针对Create)和app1.migrations.0007_auto_<date>(针对Delete),则最简单的操作是:

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

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

  • 不会自动为ForeignKey更改创建依赖关系
  • 我们不想包装这些ForeignKey更改,state_operations因此我们需要确保它们与表操作是分开的。

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

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

  1. app1复制模型app2,设置db_table,但不要更改任何FK引用。
  2. 运行makemigrations并包装所有app2迁移state_operations(请参见上文)
    • 如上所述,将依赖项添加app2 CreateTable到最新的app1迁移中
  3. 将所有FK参考指向新模型。如果您不使用字符串引用,请将旧模型移到models.py(不要删除它)的底部,这样它就不会与导入的类竞争。
  4. 运行,makemigrations但不要包装任何内容state_operations(实际上应该发生FK更改)。将所有ForeignKey迁移(即AlterField)中的依赖项添加到其中的CreateTable迁移中app2(下一步将需要此列表,因此请对其进行跟踪)。例如:

    • 找到包含迁移CreateModelapp2.migrations.0002_auto_<date>和复制迁移的名称。
    • 查找所有具有该模型的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_<date>'),
          ]  
  5. 从中删除模型 app1

  6. 运行迁移makemigrations并将其包装在app1state_operations
    • 为上一步的所有ForeignKey迁移(即AlterField)添加一个依赖项(可能包括app1和中的迁移app2)。
    • 当我构建这些迁移时,DeleteTable已经依赖于AlterField迁移了,因此我不需要手动执行它(即Alterbefore Delete)。

在这一点上,Django是个不错的选择。新模型指向旧表,并且Django的迁移使它确信所有内容都已适当地重新放置。最大的警告(来自@Michael的答案)是ContentType为新模型创建了一个新模型。如果您链接(例如通过ForeignKey)到内容类型,则需要创建一个迁移来更新ContentType表。

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

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

效果很好,谢谢!我认为编辑最后一次迁移并不重要,因为无论如何这都在依赖关系树的底部。
James Meakin

1
好答案!我认为您需要在migrations.SeparateDatabaseAndState上添加右括号,对吗?
atm

这对我有用。我也没有像@JamesMeakin编辑最后迁移(第3步,整个答案的最后一行),它仍然能正常工作
兆瓦

在第二种情况下,有FK的人,第二步对我来说失败了,并且出现了一个有意义的错误:table_name: (models.E028) db_table 'table_name' is used by multiple models: app1.Model, app2.Model.
Mihai Zamfir

我已经使用了几次该程序。如果您比较2.2(docs.djangoproject.com/en/2.2/ref/checks)和2.1(docs.djangoproject.com/en/2.1/ref/checks)的文档,则可以看到它是在2.2中添加的。可能可以解决,managed=False但我无处可查。
克莱顿

1

如果数据不是很大或太复杂,但仍然很重要,则另一种骇人听闻的选择是:

  • 使用manage.py dumpdata获取数据固定装置
  • 继续正确建模更改和迁移,而不涉及更改
  • 全局将设备从旧型号和应用名称替换为新的
  • 使用manage.py loaddata加载数据

1

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

万一您需要移动模型而又无权访问该应用程序(或者您不想访问该应用程序),则可以创建一个新的Operation并考虑仅在迁移的模型不具有该模型的情况下创建一个新模型存在。

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

请不要对多个问题添加相同的答案。回答最好的一个,并将其余的标记为重复。请参阅为几个问题添加重复答案是否可以接受?
佩特·弗里伯格

0

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

例如,有两个应用程序: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_newXXX1_src_app_newXXX1_src_app_new是No-op迁移,可确保src_app在之后执行将来的迁移XXX1_dst_app_new

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

python manage.py migrate

就这样!


请注意,此代码可能仅对Django 1.7有用。在django 2.0中尝试此操作将不起作用。这也意味着使用这种机制移动模型会增加维护费用,从而升级django版本。
Paul in Hout

0

您可以尝试以下(未试用):

  1. 将模型从src_app移至dest_app
  2. 迁移dest_app; 确保模式迁移依赖于最新的src_app迁移(https://docs.djangoproject.com/en/dev/topics/migrations/#migration-files
  3. 将数据迁移添加到dest_app,从中复制所有数据src_app
  4. 迁移src_app; 确保模式迁移依赖于-的最新(数据)迁移,dest_app即:步骤3的迁移

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


0

假设您要将模型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,这可能有所帮助。

值得注意的是,您最终会更改迁移历史记录。即使您的数据库已应用原始迁移,也没关系。如果原始迁移和重写迁移最终都具有相同的数据库模式,则这种重写应该可以。


0
  1. 将旧模型的名称更改为“ model_name_old”
  2. 移民
  3. 制作名为“ model_name_new”的新模型,并在相关模型上具有相同的关系(例如,用户模型现在具有user.blog_old和user.blog_new)
  4. 移民
  5. 编写自定义迁移,将所有数据迁移到新模型表
  6. 通过在运行迁移之前和之后将备份与新的数据库副本进行比较来测试这些迁移的难处
  7. 当一切都令人满意时,删除旧模型
  8. 移民
  9. 将新模型更改为正确的名称'model_name_new'->'model_name'
  10. 在登台服务器上测试整个迁移过程
  11. 将您的生产站点关闭几分钟,以便在不干扰用户的情况下运行所有​​迁移

对每个需要移动的模型分别执行此操作。我不建议通过更改为整数然后再返回外键来执行其他答案所说的事情,在迁移之后,新的外键可能会有所不同,并且行的ID可能会有所不同,并且我不想冒任何风险切换回外键时ID不匹配的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.