Django unique_together 与可为空的 ForeignKey

Posted

技术标签:

【中文标题】Django unique_together 与可为空的 ForeignKey【英文标题】:Django unique_together with nullable ForeignKey 【发布时间】:2016-01-23 07:37:37 【问题描述】:

我在使用 Sqlite 的开发机器中使用 Django 1.8.4,我有这些模型:

class ModelA(Model):
    field_a = CharField(verbose_name='a', max_length=20)
    field_b = CharField(verbose_name='b', max_length=20)

    class Meta:
        unique_together = ('field_a', 'field_b',)


class ModelB(Model):
    field_c = CharField(verbose_name='c', max_length=20)
    field_d = ForeignKey(ModelA, verbose_name='d', null=True, blank=True)

    class Meta:
        unique_together = ('field_c', 'field_d',)

我已经运行了正确的迁移并在 Django Admin 中注册了它们。所以,使用管理员我已经完成了这个测试:

我能够创建 ModelA 记录,但 Django 禁止我创建重复记录 - 正如预期的那样! 当 field_b 不为空时,我无法创建相同的 ModelB 记录 但是,当使用 field_d 为空时,我能够创建相同的 ModelB 记录

我的问题是:如何将 unique_together 应用于可为空的 ForeignKey?

我为这个问题找到的最新答案是 5 年...我确实认为 Django 已经发展,问题可能不一样。

【问题讨论】:

Django unique together constraint failure?的可能重复 @Ivan 我也尝试过这篇文章,但它对模型表单集没有帮助。当试图在同一个表单集中创建相同的记录时,Django 并不禁止。用户仍然可以创建重复项 “当 field_b 不为空时,我无法创建相同的 ModelB 记录”您的意思是“field_d”吗? 【参考方案1】:

Django 2.2 添加了一个新的constraints API,这使得在数据库中更容易解决这种情况。

您将需要两个约束:

    现有的元组约束;和 剩余键减去可空键,带条件

如果您有多个可为空的字段,我想您将需要处理排列。

这是一个示例,其中包含必须全部唯一的 thruple 字段,其中只允许一个 NULL

from django.db import models
from django.db.models import Q
from django.db.models.constraints import UniqueConstraint

class Badger(models.Model):
    required = models.ForeignKey(Required, ...)
    optional = models.ForeignKey(Optional, null=True, ...)
    key = models.CharField(db_index=True, ...)

    class Meta:
        constraints = [
            UniqueConstraint(fields=['required', 'optional', 'key'],
                             name='unique_with_optional'),
            UniqueConstraint(fields=['required', 'key'],
                             condition=Q(optional=None),
                             name='unique_without_optional'),
        ]

【讨论】:

这是迄今为止处理此类情况最简洁的方法。 这应该是首选答案。 很好的答案。可惜 mysql 不支持它:( 很好的答案,但在我的情况下,应该在同一个迁移中创建一个引用字段,并且这一步是在创建约束之后放置的,它导致异常django.core.exceptions.FieldDoesNotExist。我不得不手动重新排序迁移步骤,它对我有用。 对于那些感兴趣的人,这是我找到的最佳答案:原始 SQL dba.stackexchange.com/a/9760/212269【参考方案2】:

更新:我的答案的先前版本是功能性的,但设计不好,这个版本考虑了一些 cmets 和其他答案。

在 SQL 中,NULL 不等于 NULL。这意味着如果您有两个对象,其中field_d == None and field_c == "somestring" 不相等,那么您可以同时创建这两个对象。

您可以覆盖Model.clean 以添加您的支票:

class ModelB(Model):
    #...
    def validate_unique(self, exclude=None):
        if ModelB.objects.exclude(id=self.id).filter(field_c=self.field_c, \
                                 field_d__isnull=True).exists():
            raise ValidationError("Duplicate ModelB")
        super(ModelB, self).validate_unique(exclude)

如果在表单之外使用,您必须致电 full_cleanvalidate_unique

但请注意处理竞争条件。

【讨论】:

Tx @Ivan,但我应该能够使用field_c=="a", field_d==None 创建两条 ModelB 记录吗?? 我该如何改变这种行为:) 发送!它仍然没有为我解决。我正在使用表单集,并且仅通过覆盖 clean 方法,当用户填写两个表单时,两者都是有效的(formset.is_valid()),但是当第一个被保存时,第二个变得无效。你明白我的意思吗?关于表单集和这篇文章的任何提示? 我认为validate_unique 会是比clean 更好的覆盖方法...否? 它对我有用,但我必须检查我正在运行验证的实例的可空字段是否设置为空。这里看起来不见了。【参考方案3】:

@ivan,我认为 django 没有简单的方法来管理这种情况。您需要考虑并非总是来自表单的所有创建和更新操作。此外,您应该考虑竞争条件...

并且由于您没有在数据库级别强制执行此逻辑,因此实际上可能会有双倍记录,您应该在查询结果时检查它。

关于你的解决方案,它可能对表单有好处,但我不认为 save 方法会引发 ValidationError。

如果可能的话,最好将此逻辑委托给 DB。在这种特殊情况下,您可以使用两个部分索引。 *** 上有一个类似的问题 - Create unique constraint with null columns

因此您可以创建 Django 迁移,为您的数据库添加两个部分索引

例子:

# Assume that app name is just `example`

CREATE_TWO_PARTIAL_INDEX = """
    CREATE UNIQUE INDEX model_b_2col_uni_idx ON example_model_b (field_c, field_d)
    WHERE field_d IS NOT NULL;

    CREATE UNIQUE INDEX model_b_1col_uni_idx ON example_model_b (field_c)
    WHERE field_d IS NULL;
"""

DROP_TWO_PARTIAL_INDEX = """
    DROP INDEX model_b_2col_uni_idx;
    DROP INDEX model_b_1col_uni_idx;
"""


class Migration(migrations.Migration):

    dependencies = [
        ('example', 'PREVIOUS MIGRATION NAME'),
    ]

    operations = [
        migrations.RunSQL(CREATE_TWO_PARTIAL_INDEX, DROP_TWO_PARTIAL_INDEX)
    ]

【讨论】:

是的,你是对的,在我的其他答案中,我通常会补充说,它只有在调用 clean 时才会起作用。另一方面,我通常会尽量避免在模型代码之外进行偷偷摸摸的数据库修改,数据迁移除外。 +1 数据完整性约束应尽可能靠近数据,最好的方法是使用数据库约束。任何 python 方法(如接受的答案)都会有竞争条件问题。 用Mysql可以做到吗?我的理解是,使用 Mysql 创建唯一约束时,没有像“Where is NULL”这样的功能 @PatrickKenekayoro,不,MySQL 没有部分索引这样的东西。查看更多信息dba.stackexchange.com/a/106593 唯一合理的解决方案是将此功能作为代码库的一部分,就像建议的 Ivan 一样【参考方案4】:

我认为这对于 Django 1.2+ 来说是更清晰的方法

在表单中,它会作为 non_field_error 引发,没有 500 错误,在其他情况下,例如 DRF,您必须查看此案例手册,因为它将是 500 错误。 但它总是会检查 unique_together!

class BaseModelExt(models.Model):
is_cleaned = False

def clean(self):
    for field_tuple in self._meta.unique_together[:]:
        unique_filter = 
        unique_fields = []
        null_found = False
        for field_name in field_tuple:
            field_value = getattr(self, field_name)
            if getattr(self, field_name) is None:
                unique_filter['%s__isnull' % field_name] = True
                null_found = True
            else:
                unique_filter['%s' % field_name] = field_value
                unique_fields.append(field_name)
        if null_found:
            unique_queryset = self.__class__.objects.filter(**unique_filter)
            if self.pk:
                unique_queryset = unique_queryset.exclude(pk=self.pk)
            if unique_queryset.exists():
                msg = self.unique_error_message(self.__class__, tuple(unique_fields))

                raise ValidationError(msg)

    self.is_cleaned = True

def save(self, *args, **kwargs):
    if not self.is_cleaned:
        self.clean()

    super().save(*args, **kwargs)

【讨论】:

【参考方案5】:

尚未提及的一种可能的解决方法是创建一个虚拟的ModelA 对象作为您的NULL 值。然后你可以依靠数据库来强制执行唯一性约束。

【讨论】:

【参考方案6】:

为您的模型添加一个干净的方法 - 见下文

def clean(self):
        if Variants.objects.filter("""Your filter """).exclude(pk=self.pk).exists():
            raise ValidationError("This variation is duplicated.")

【讨论】:

以上是关于Django unique_together 与可为空的 ForeignKey的主要内容,如果未能解决你的问题,请参考以下文章

UniqueConstraint 与 unique_together 之间的区别 - Django 2.2?

如何在 django 中使用 Uniqueconstraints 函数创建 unique_together 索引?

约束 unique_together 可能与 django 类中的唯一字段冲突

Django 数据库模型“unique_together”不起作用?

如何在 django 的核心模型中添加“unique_together”约束?

django rest 框架:内容类型 unique_together 和序列化