向现有 django 模型添加新的唯一字段时的最佳实践

Posted

技术标签:

【中文标题】向现有 django 模型添加新的唯一字段时的最佳实践【英文标题】:Best practice when add a new unique field to an existing django model 【发布时间】:2019-11-17 23:05:08 【问题描述】:

我有一个类似于以下的现有模型...

class Resource(models.Model):

    id = models.AutoField(primary_key=True)

我们已经使用了一段时间,现在我们的数据库中有大约 100 万个这些 Resource 对象(以及相关的 ForeignKey/else 用法)的实例。

我现在需要在此模型上跟踪另一个 ID,我要强制执行的 ID 是唯一的。

other_id = models.IntegerField(unique=True)

other_id 信息当前存储在一些外部 CSV 中,我想(在此过程中的某个时刻)将此信息加载到所有现有的 Resource 实例中。

添加上述字段后,Django 的makemigrations 工作正常。但是,当我对现有数据库应用所述迁移时,我收到一个错误,表明我需要为所有现有 Resource 实例提供默认值。我相信你们中的许多人都见过类似的东西。

绕过此限制的最佳方法是什么?我想到的一些方法……

    删除unique=True 要求 应用迁移 将other_id 值从外部加载到所有现有模型(通过一些管理命令或一键脚本) 重新添加 unique=True 并应用迁移 将所有现有数据转储到 JSON 刷新所有表 应用迁移(使用 unique=True) 编写一个脚本来重新加载数据,添加正确的other_id值 (不确定这是否可行)- 编写一些自定义迁移逻辑以在我运行 manage.py migrate 时自动引用这些外部 CSV 以加载 other_id 值。如果(在将来的某个时间点)有人重新运行这些迁移并且这部分失败(无法在 CSV 中找到现有资源 id 以提取 other_id),这可能会遇到问题。

所有这些都感觉很复杂,但我想我想要做的也不是最简单的事情。

有什么想法吗?我不得不想象过去有人不得不解决类似的问题。

谢谢!

【问题讨论】:

【参考方案1】:

实际上,来源或您的问题本身并不是唯一约束,而是您的字段不允许空值且没有默认值这一事实 - 您会遇到与非唯一字段完全相同的错误。

这里正确的解决方案是允许该字段为空(null=True)并将其默认为None(将转换为sql“null”)。由于 null 值被排除在唯一约束之外(至少如果您的数据库供应商尊重 SQL 标准),这允许您应用架构更改,同时仍然确保您不能有非空值的重复项。

然后您可能希望数据迁移加载已知的“other_id”值,并最终进行第三次架构迁移以禁止该字段为空值 - 当且仅当您知道您已为所有记录填写此字段时。

【讨论】:

感谢您的想法,并指出“必须提供默认值”的实际原因。似乎与我的 #2 非常相似,只是我不知道数据迁移是一回事! 这与您的第二个解决方案非常不同 - 您不必导出数据、刷新表、然后编辑和重新导入数据 - 所有这些都可能需要在大型数据库上花费一些时间,并且最明确容易出错。实际上,这更接近您的 #1,但更安全,因为它确保您不会有非空重复值(这会破坏第二次迁移添加唯一约束)。 我说的是#2吗?我的意思是#1,除了我有独特的和空的混合:)再次感谢【参考方案2】:

Django 有一个名为 Data Migrations 的东西,您可以在其中创建一个迁移文件,在应用迁移时修改/删除/添加数据到您的数据库。

在这种情况下,您将创建 3 个不同的迁移:

    使用null=True 创建允许空值的迁移。 创建填充数据的数据迁移。 通过删除在步骤 1 中添加的 null=True 创建一个不允许空值的迁移。

当您随后运行 python manage.py migrate 时,它将以正确的顺序应用步骤 1-3 中的所有迁移。

您的数据迁移将如下所示:

from django.db import migrations

def populate_reference(apps, schema_editor):
    MyModel = apps.get_model('yourappname', 'MyModel')
    for obj in MyModel.objects.all():
        obj.other_id = random_id_generator()
        obj.save()

class Migration(migrations.Migration):

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(populate_reference),
    ]

您可以使用./manage.py makemigrations --empty yourappname 命令创建一个空的迁移文件。

【讨论】:

感谢您的回答!我对使用 DataMigration 的唯一担忧是我需要依赖一些外部 CSV 来进行索引 (resource.other_id = external_data[resource.id])。我们的流程一直在创建新的Resource 实例,因此我不能只将 CSV 的“当前状态”复制到我的 django 应用程序中,并期望它适用于未来的任何迁移。 您可以在完成后squash your migrations 摆脱这些步骤,只需为未来的迁移创建模型的最终状态。 @wKavey 如果在创建新的Resource 时不能可靠地立即获得other_id 值,那么您需要保持该字段可为空(并且使用 None 默认值),如以及在“other_id”可用时设置一些自动更新资源的方式(当然,他的解决方案取决于上下文)。 @brunodesthuilliers 感谢您关注我。目前,other_id 的值保存在一个 CSV 文件映射中,将id 映射到other_id。每当在数据库中创建新资源时,都会在此 CSV 中添加一个新条目。如果我正在编写使用此 CSV 填充 other_id 的数据迁移,那么如果我确定所有资源及其 ids 都得到了代表,那就没问题了。我的问题是我需要检查此迁移到版本控制,大概与 CSV 一起。无法保证如果其他人在未来运行此迁移,所有 ID 仍然存在。

以上是关于向现有 django 模型添加新的唯一字段时的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

Django Crispy Forms 向 ModelFormSet 添加新的空表单

添加新的唯一字段时如何正确进行迁移

向 Django Auth User 模型添加便利方法的最佳方法?

Vapor Fluent 如何向现有表添加新的必填字段键

Django 访问不属于表单的字段

向 Django 模型添加动态字段