首页 > 解决方案 > 如何将自定义用户模型迁移到 Django 中的不同应用程序?

问题描述

在 Django 中,我试图将自定义用户模型从一个应用程序移动到另一个应用程序。当我按照此处找到的说明并尝试应用迁移时,我收到错误,ValueError: Related model 'newapp.user' cannot be resolved该错误源自django.contrib.admin.models.LogEntry.user,定义为models.ForeignKey(settings.AUTH_USER_MODEL, ...),因此这是来自 Django 管理员(我也使用)的模型,具有用户模型。我该如何进行此迁移?

标签: djangodjango-modelsdjango-migrationsdjango-users

解决方案


对于可交换模型(可以通过 in 的值交换出来的模型settings),这样的举动并非易事。这里问题的根本原因是它LogEntry有一个外键,但是Django 迁移不管理或不知道它本身settings.AUTH_USER_MODEL的历史。settings.AUTH_USER_MODEL当您更改AUTH_USER_MODEL为指向新的 User 模型时,它将追溯更改 Django 看到的迁移历史记录。对于 Django,现在看起来 LogEntry 的外键总是引用 destapp 中的新用户模型。当您运行为 LogEntry 创建表的迁移时(例如,在重新初始化数据库或运行测试时),Django 无法解析模型并失败。

另请参阅此问题和那里的评论。

要解决此问题,AUTH_USER_MODEL需要指向新应用的初始迁移中存在的模型。有几种方法可以让这个工作。我假设User模型从移动sourceappdestapp.

  1. 将新用户模型的迁移定义从上次迁移(Django makemigrations 会自动放置它的位置)移动到 destapp 的初始迁移。把它包裹在一个SeparateDatabaseAndStatestate 操作,因为数据库表已经由 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. sourceapp_user自从删除了该信息以来,Django 无法再跟踪任何数据库级别的更改。

    如果这对您有用,您可以放弃 sourceapp 和 destapp 之间的任何依赖关系,如果 sourceapp 的迁移不需要用户在那里,或者让 sourceapp 的初始迁移依赖于 destapp 的初始迁移,以便在 sourceapp 的迁移之前创建 User 的表正在运行。

  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()

推荐阅读