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

所以大约一年前,我开始了一个项目,像所有的新开发者一样,我并没有真正关注太多的结构,但是现在我和 Django 进一步发展,它已经开始显示我的项目布局主要是我的模型在结构上是糟糕的。

我有模型主要举行在一个单一的应用程序,真的大多数这些模型应该在自己的个人应用程序,我确实试图解决这一点,并移动他们与南,但我发现这很棘手,真的很难,由于外键等。

但是,由于 Django 1.7和内置的对迁移的支持,现在是否有更好的方法来做到这一点?

39902 次浏览

我正在删除可能导致数据丢失的旧答案。作为 Ozan 提到过,我们可以在每个应用程序中创建两个迁移。下面的评论是我以前的回答。

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

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

这是一个粗略的测试,所以不要忘记备份您的 DB! ! !

例如,有两个应用程序: src_appdst_app,我们希望移动模型 MoveMesrc_appdst_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

仅此而已!

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

  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步的迁移

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

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

我是怎么做到的(在 Django = = 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. A new migration (e.g. 0009_auto_something.py) is made in app2 with a migrations.CreateModel() statement, move this statement to the initial migration of app2 (e.g. 0001_initial.py) (it will be just like it always have been there). And now remove the created migration = 0009_auto_something.py

  5. Just as you act, like app2.YourModel always has been there, now remove the existence of app1.YourModel from your migrations. Meaning: comment out the CreateModel statements, and every adjustment or datamigration you used after that.

  6. And of course, every reference to app1.YourModel has to be changed to app2.YourModel through your project. Also, don't forget that all possible foreign keys to app1.YourModel in migrations have to be changed to app2.YourModel

  7. Now if you do $ python manage.py migrate, nothing has changed, also when you do $ python manage.py makemigrations, nothing new has been detected.

  8. Now the finishing touch: remove the Class Meta from app2.YourModel and do $ python manage.py makemigrations app2 && python manage.py migrate app2 (if you look into this migration you'll see something like this:)

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

table=None, means it will take the default table-name, which in this case will be app2_yourmodel.

  1. DONE, with data saved.

P.S during the migration it will see that that content_type app1.yourmodel has been removed and can be deleted. You can say yes to that but only if you don't use it. In case you heavily depend on it to have FKs to that content-type be intact, don't answer yes or no yet, but go into the db that time manually, and remove the contentype app2.yourmodel, and rename the contenttype app1.yourmodel to app2.yourmodel, and then continue by answering no.

假设您正在将模型 TheModel 从 app _ a 移动到 app _ b。

另一种解决方案是手动更改现有的迁移。其思想是,每次在 app _ a 的迁移中看到修改 TheModel 的操作时,都将该操作复制到 app _ b 的初始迁移的末尾。每次你看到一个参考‘ app _ a。模型在 app _ a 的迁移中,你把它改为‘ app _ b。模特。

我只是为一个现有的项目做了这个,我想提取一个特定的模型到一个可重用的应用程序。手术很顺利。我想如果 app _ b 和 app _ a 之间有引用的话,事情会变得更加困难。此外,我还为我的模型手动定义了 Meta.db _ table,这可能会有所帮助。

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

  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 不匹配的风险。

如果数据不大或过于复杂,但仍然需要维护,另一种可行的方法是:

  • 使用 Py 转储数据获取数据装置
  • 继续正确地对更改和迁移建模,而不关联更改
  • Global 将从旧模型和应用程序名称中的 fixture 替换为新的 fixture
  • 使用 Py loaddata加载数据

抄袭我在 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))
],
),
]

我得到了紧张的手工编码迁移(这是 Ozan 的答案所要求的) ,因此下面结合了 Ozan 和 迈克尔的策略,以最大限度地减少所需的手工编码量:

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

    class Meta:
    db_table = 'app1_yourmodel'
    
  4. Run makemigrations. This will generate CreateModel in app2 and DeleteModel in app1. Technically, these migrations refer to the exact same table and would remove (including all data) and re-create the table.

  5. In reality, we don't want (or need) to do anything to the table. We just need Django to believe that the change has been made. Per @Ozan's answer, the state_operations flag in SeparateDatabaseAndState does this. So we wrap all of the migrations entries IN BOTH MIGRATIONS FILES with SeparateDatabaseAndState(state_operations=[...]). For example,

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

    变成了

    operations = [
    migrations.SeparateDatabaseAndState(state_operations=[
    ...
    migrations.DeleteModel(
    name='YourModel',
    ),
    ...
    ])
    ]
    
  6. You also need to make sure the new "virtual" CreateModel migration depends on any migration that actually created or altered the original table. For example, if your new migrations are app2.migrations.0004_auto_<date> (for the Create) and app1.migrations.0007_auto_<date> (for the Delete), the simplest thing to do is:

    • Open app1.migrations.0007_auto_<date> and copy its app1 dependency (e.g. ('app1', '0006...'),). This is the "immediately prior" migration in app1 and should include dependencies on all of the actual model building logic.
    • Open app2.migrations.0004_auto_<date> and add the dependency you just copied to its dependencies list.

If you have ForeignKey relationship(s) to the model you're moving, the above may not work. This happens because:

  • Dependencies are not automatically created for the ForeignKey changes
  • We do not want to wrap the ForeignKey changes in state_operations so we need to ensure they are separate from the table operations.

NOTE: Django 2.2 added a warning (models.E028) that breaks this method. You may be able to work around it with managed=False but I have not tested it.

The "minimum" set of operations differ depending on the situation, but the following procedure should work for most/all ForeignKey migrations:

  1. COPY the model from app1 to app2, set db_table, but DON'T change any FK references.
  2. Run makemigrations and wrap all app2 migration in state_operations (see above)
    • As above, add a dependency in the app2 CreateTable to the latest app1 migration
  3. Point all of the FK references to the new model. If you aren't using string references, move the old model to the bottom of models.py (DON'T remove it) so it doesn't compete with the imported class.
  4. Run makemigrations but DON'T wrap anything in state_operations (the FK changes should actually happen). Add a dependency in all the ForeignKey migrations (i.e. AlterField) to the CreateTable migration in app2 (you'll need this list for the next step so keep track of them). For example:

    • Find the migration that includes the CreateModel e.g. app2.migrations.0002_auto_<date> and copy the name of that migration.
    • Find all migrations that have a ForeignKey to that model (e.g. by searching app2.YourModel to find migrations like:

      class Migration(migrations.Migration):
      
      
      dependencies = [
      ('otherapp', '0001_initial'),
      ]
      
      
      operations = [
      migrations.AlterField(
      model_name='relatedmodel',
      name='fieldname',
      field=models.ForeignKey(... to='app2.YourModel'),
      ),
      ]
      
    • Add the CreateModel migration as as a dependency:

      class Migration(migrations.Migration):
      
      
      dependencies = [
      ('otherapp', '0001_initial'),
      ('app2', '0002_auto_<date>'),
      ]
      
  5. Remove the models from app1

  6. Run makemigrations and wrap the app1 migration in state_operations.
    • Add a dependency to all of the ForeignKey migrations (i.e. AlterField) from the previous step (may include migrations in app1 and app2).
    • When I built these migrations, the DeleteTable already depended on the AlterField migrations so I didn't need to manually enforce it (i.e. Alter before Delete).

At this point, Django is good to go. The new model points to the old table and Django's migrations have convinced it that everything has been relocated appropriately. The big caveat (from @Michael's answer) is that a new ContentType is created for the new model. If you link (e.g. by ForeignKey) to content types, you'll need to create a migration to update the ContentType table.

I wanted to cleanup after myself (Meta options and table names) so I used the following procedure (from @Michael):

  1. Remove the db_table Meta entry
  2. Run makemigrations again to generate the database rename
  3. Edit this last migration and make sure it depends on the DeleteTable migration. It doesn't seem like it should be necessary as the Delete should be purely logical, but I've run into errors (e.g. app1_yourmodel doesn't exist) if I don't.