所以大约一年前我开始了一个项目,和所有新开发人员一样,我并没有真正关注结构,但是现在我与 Django 一起更进一步,它开始出现我的项目布局主要是我的模型在结构上很糟糕.
我的模型主要保存在一个应用程序中,实际上这些模型中的大多数应该在它们自己的单独应用程序中,我确实尝试解决这个问题并将它们向南移动,但是由于外键等,我发现它很棘手而且非常困难。
然而,由于 Django 1.7 并内置了对迁移的支持,现在有更好的方法吗?
settings.py
中引用并与之相关的任何其他模型),则移动会变得更加复杂。有关详细信息,请参阅 stackoverflow.com/questions/69473228/…
这可以使用 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)
]
我正在删除旧答案,因为可能会导致数据丢失。作为 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)
]
./manage.py migrate
,一切都会以良好状态结束。手动伪造迁移是 IMO 错误的方法。
我遇到了同样的问题。 Ozan's answer 帮了我很多,但不幸的是还不够。事实上,我有几个 ForeignKey 链接到我想要移动的模型。经过一番头痛后,我找到了解决方案,因此决定将其发布以解决人们的时间问题。
您还需要 2 个步骤:
在做任何事情之前,将所有链接到 TheModel 的 ForeignKey 更改为 Integerfield。然后运行 python manage.py makemigrations 完成 Ozan 的步骤后,重新转换你的外键:放回 ForeignKey(TheModel) 而不是 IntegerField()。然后再次进行迁移(python manage.py makemigrations)。然后您可以迁移,它应该可以工作(python manage.py migrate)
希望能帮助到你。当然,在尝试生产之前先在本地进行测试,以避免出现意外:)
我是怎么做到的(在 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 继续。
app_label = 'app1'
元选项。
field1 = models.ForeignKey('app1.myModel').
当我迁移时,我收到一个 ValueError 说明 field1 was declared with a lazy reference to 'app1.myModel' but app 'app1' doesn't provide model 'MyModel'
我对手工编码迁移感到紧张(正如 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_
如果您与要移动的模型有 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_
在这一点上,Django 很好。新模型指向旧表,Django 的迁移已经说服它所有内容都已适当地重新定位。最大的警告(来自@Michael 的回答)是为新模型创建了一个新的 ContentType
。如果您链接(例如通过 ForeignKey
)到内容类型,则需要创建迁移以更新 ContentType
表。
我想自己清理(元选项和表名),所以我使用了以下过程(来自@Michael):
删除 db_table 元条目 再次运行 makemigrations 以生成数据库重命名 编辑最后一次迁移并确保它依赖于 DeleteTable 迁移。似乎没有必要,因为删除应该是纯逻辑的,但如果我不这样做,我会遇到错误(例如 app1_yourmodel 不存在)。
table_name: (models.E028) db_table 'table_name' is used by multiple models: app1.Model, app2.Model.
managed=False
,但我无处可查。
如果数据不大或太复杂,但维护仍然很重要,另一种 hacky 选择是:
使用 manage.py dumpdata 获取数据夹具
继续正确建模更改和迁移,而不关联更改
将旧模型和应用程序名称中的固定装置全局替换为新的
使用 manage.py loaddata 加载数据
复制自我在 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))
],
),
]
这是经过粗略测试的,所以不要忘记备份你的数据库!!!
例如,有两个应用程序:src_app
和 dst_app
,我们要将模型 MoveMe
从 src_app
移动到 dst_app
。
为两个应用创建空迁移:
python manage.py makemigrations --empty src_app
python manage.py makemigrations --empty dst_app
假设,新的迁移是 XXX1_src_app_new
和 XXX1_dst_app_new
,之前的顶级迁移是 XXX0_src_app_old
和 XXX0_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_new
。 XXX1_src_app_new
是无操作迁移,需要确保将来的 src_app
迁移将在 XXX1_dst_app_new
之后执行。
将 MoveMe
从 src_app/models.py
移动到 dst_app/models.py
。然后运行:
python manage.py migrate
就这样!
您可以尝试以下(未经测试):
将模型从 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 的迁移
请注意,您将复制整个表格,而不是移动它,但是这样两个应用程序都不必接触属于另一个应用程序的表格,我认为这更重要。
假设您将模型 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,这可能会有所帮助。
值得注意的是,您最终会改变迁移历史。这无关紧要,即使您有一个应用了原始迁移的数据库。如果原始迁移和重写迁移最终都具有相同的数据库模式,那么这种重写应该是可以的。
将旧模型的名称更改为 'model_name_old' makemigrations 创建名为 'model_name_new' 的新模型,在相关模型上具有相同的关系(例如,用户模型现在具有 user.blog_old 和 user.blog_new) makemigrations 编写迁移所有数据的自定义迁移到新模型表通过在运行迁移之前和之后比较备份与新数据库副本来测试这些迁移的地狱当一切都令人满意时,删除旧模型 makemigrations 将新模型更改为正确的名称'model_name_new'->'model_name ' 在暂存服务器上测试全部迁移,让您的生产站点停机几分钟,以便在没有用户干预的情况下运行所有迁移
为需要移动的每个模型单独执行此操作。我不建议通过更改为整数并返回外键来做其他答案所说的那样有可能新的外键会有所不同,并且迁移后行可能具有不同的 ID,我不想冒任何风险切换回外键时不匹配的 id。