如何将自定义用户模型迁移到 Django 中的不同应用程序?
Posted
技术标签:
【中文标题】如何将自定义用户模型迁移到 Django 中的不同应用程序?【英文标题】:How do you migrate a custom User model to a different app in Django? 【发布时间】:2021-11-27 01:51:11 【问题描述】:在 Django 中,我试图将自定义用户模型从一个应用程序移动到另一个应用程序。当我按照诸如找到here 之类的说明并尝试应用迁移时,我收到源自django.contrib.admin.models.LogEntry.user
的ValueError: Related model 'newapp.user' cannot be resolved
错误,它被定义为models.ForeignKey(settings.AUTH_USER_MODEL, ...)
,所以这是来自Django 管理员的模型(我也使用),它具有用户模型的外键。如何进行此迁移?
【问题讨论】:
【参考方案1】:对于可交换模型(可以通过settings
中的值交换出来的模型),这样的举动并非易事。这里问题的根本原因是LogEntry
有一个指向settings.AUTH_USER_MODEL
的外键,但是settings.AUTH_USER_MODEL
本身的历史并没有被Django 迁移管理或知道。当您更改 AUTH_USER_MODEL
以指向新的用户模型时,这会追溯地更改 Django 看到的迁移历史记录。对于 Django,现在看起来 LogEntry 的外键总是引用 destapp 中的新用户模型。当您运行为 LogEntry 创建表的迁移时(例如,在重新初始化数据库或运行测试时),Django 无法解析模型并失败。
另请参阅this issue 和那里的 cmets。
要解决此问题,AUTH_USER_MODEL
需要指向新应用的初始迁移中存在的模型。有几种方法可以让这个工作。我假设User
模型从sourceapp
移动到destapp
。
将新用户模型的迁移定义从上次迁移(Django makemigrations 会自动放置它的位置)移动到 destapp 的初始迁移。将其包裹在 SeparateDatabaseAndState
状态操作中,因为数据库表已由 sourceapp 中的迁移创建。然后,您需要添加从 destapp 的初始迁移到 sourceapp 的最后迁移的依赖项。问题是,如果您尝试像现在一样应用迁移,它将失败,因为 destapp 的初始迁移已经应用,而它的依赖项(sourceapp 的最后一次迁移)尚未应用。因此,在 destapp 中添加上述迁移之前,您需要在 sourceapp 中应用迁移。在应用 sourceapp 和 destapp 迁移之间的间隙中,用户模型将不存在,因此您的应用程序将暂时中断。
除了暂时中断应用程序之外,这还有另一个问题,现在 destapp 将依赖于 sourceapp 的迁移。如果你能做到这一点,那很好,但如果已经存在从 sourceapp 迁移到 destapp 迁移的依赖关系,这将不起作用,你现在已经创建了一个循环依赖关系。如果是这种情况,请查看下一个选项。
忘记用户迁移历史。只需在 destapp 的初始迁移中定义 User 类,无需 SeparateDatabaseAndState
包装器。确保您有CreateModel(..., options='db_table': 'sourceapp_user', ...)
,这样数据库表的创建将与用户在 sourceapp 中时的创建方式相同。然后编辑定义 User 的 sourceapp 的迁移,并删除这些定义。之后,您可以创建一个常规迁移,在其中删除用户的 db_table
设置,以便将数据库表重命名为应该用于 destapp 的内容。
这仅适用于 sourceapp.User 的迁移历史记录中没有迁移或迁移最少的情况。 Django 现在认为 User 一直存在于 destapp 中,但它的表被命名为 sourceapp_user
。 Django 无法再跟踪对 sourceapp_user
的任何数据库级更改,因为该信息已被删除。
如果这对您有用,您可以放弃 sourceapp 和 destapp 之间的任何依赖关系,如果 sourceapp 的迁移不需要用户存在,或者让 sourceapp 的初始迁移依赖于 destapp 的初始迁移,以便创建 User 的表在 sourceapp 的迁移运行之前。
如果两者都不适用于您的情况,另一种选择是将 User 的定义添加到 sourceapp 的初始迁移(不带 SeparateDatabaseAndState
包装器),但让它使用虚拟表名称 (options='db_table': 'destapp_dummy_user'
)。然后,在您实际想要将用户从 sourceapp 移动到 destapp 的最新迁移中,执行
migrations.SeparateDatabaseAndState(database_operations=[
migrations.DeleteModel(
name='User',
),
], state_operations=[
migrations.AlterModelTable('User', 'destapp_user'),
])
这将删除数据库中的虚拟表,并将用户模型指向新表。 sourceapp 中的新迁移应包含
migrations.SeparateDatabaseAndState(state_operations=[
migrations.DeleteModel(
name='User',
),
], database_operations=[
migrations.AlterModelTable('User', 'destapp_user'),
])
所以它实际上是上次 destapp 迁移中操作的镜像。现在只有 destapp 中的最后一次迁移需要依赖于 sourceapp 中的最后一次迁移。
这种方法似乎有效,但它有一个很大的缺点。删除 destapp.User 的虚拟数据库表也会删除该表的所有外键约束(至少在 Postgres 上)。所以 LogEntry 现在不再有对 User 的外键约束。 User 的新表不会重新创建这些表。您将不得不手动重新添加缺少的约束。通过手动更新数据库或编写原始 sql 迁移。
更新内容类型
在应用上述三个选项之一后,仍有一个未解决的问题。 Django 在django_content_type
表中注册每个模型。该表包含sourceapp.User
的一行。如果没有干预,该行将作为陈旧的行留在那里。这不是什么大问题,因为 Django 会自动注册新的 destapp.User
模型。但是可以通过添加以下迁移将现有的内容类型注册重命名为 destapp 来清理它:
from django.db import migrations
# If User previously existed in sourceapp, we want to switch the content type object. If not, this will do nothing.
def change_user_type(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
ContentType.objects.filter(app_label="sourceapp", model="user").update(
app_label="destapp"
)
class Migration(migrations.Migration):
dependencies = [
("destapp", "00xx_previous_migration_here"),
]
operations = [
# No need to do anything on reversal
migrations.RunPython(change_user_type, reverse_code=lambda a, s: None),
]
此功能仅在django_content_type
中没有针对destapp.User
的条目时才有效。如果有,您将需要一个更智能的函数:
from django.db import migrations, IntegrityError
from django.db.transaction import atomic
def change_user_type(apps, schema_editor):
ContentType = apps.get_model("contenttypes", "ContentType")
ct = ContentType.objects.get(app_label="sourceapp", model="user")
with atomic():
try:
ct.app_label="destapp"
ct.save()
return
except IntegrityError:
pass
ct.delete()
【讨论】:
这是在 Postgres 数据库上测试的,但我希望它可以在任何支持的数据库上工作,因为这只是简单的 Django 代码。以上是关于如何将自定义用户模型迁移到 Django 中的不同应用程序?的主要内容,如果未能解决你的问题,请参考以下文章
在 django 1.8 中将数据从原始用户模型迁移到自定义用户模型
将现有 auth.User 数据迁移到新的 Django 1.5 自定义用户模型?