如何将自定义用户模型迁移到 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.userValueError: 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 中将数据从原始用户模型迁移到自定义用户模型

从 django 用户模型迁移到自定义用户模型

将现有 auth.User 数据迁移到新的 Django 1.5 自定义用户模型?

如何正确地为 Django 的用户模型添加权限?

Django 1.8 迁移、自定义用户模型和 Postgres/MySQL 的奇怪问题

如何将自定义视图+控制器添加到模型?