【问题标题】:How do you migrate a custom User model to a different app in Django?如何将自定义用户模型迁移到 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 管理员的模型(我也使用),它具有用户模型的外键。如何进行此迁移?

【问题讨论】:

    标签: django django-models django-migrations django-users


    【解决方案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

    1. 将新用户模型的迁移定义从上次迁移(Django makemigrations 会自动放置它的位置)移动到 destapp 的初始迁移。将其包裹在 SeparateDatabaseAndState 状态操作中,因为数据库表已由 sourceapp 中的迁移创建。然后,您需要添加从 destapp 的初始迁移到 sourceapp 的最后迁移的依赖项。问题是,如果您尝试像现在一样应用迁移,它将失败,因为 destapp 的初始迁移已经应用,而它的依赖项(sourceapp 的最后一次迁移)尚未应用。因此,在 destapp 中添加上述迁移之前,您需要在 sourceapp 中应用迁移。在应用 sourceapp 和 destapp 迁移之间的间隙中,用户模型将不存在,因此您的应用程序将暂时中断。

      除了暂时中断应用程序之外,这还有另一个问题,现在 destapp 将依赖于 sourceapp 的迁移。如果你能做到这一点,那很好,但如果已经存在从 sourceapp 迁移到 destapp 迁移的依赖关系,这将不起作用,你现在已经创建了一个循环依赖关系。如果是这种情况,请查看下一个选项。

    2. 忘记用户迁移历史。只需在 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 的迁移运行之前。

    3. 如果两者都不适用于您的情况,另一种选择是将 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 代码。
    猜你喜欢
    • 2017-08-05
    • 1970-01-01
    • 2013-03-06
    • 2018-05-26
    • 2015-10-10
    • 2015-08-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多