Django unique_和可为空的外键

Django unique_和可为空的外键,django,django-models,django-forms,django-validation,Django,Django Models,Django Forms,Django Validation,我在使用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 M

我在使用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管理员中注册了它们。因此,我使用管理员完成了以下测试:

  • 我可以创建ModelA记录,Django禁止我创建重复的记录——正如预期的那样
  • 当字段_b不为空时,我无法创建相同的ModelB记录
  • 但是,当使用字段d为空时,我能够创建相同的ModelB记录
我的问题是:如何将unique_一起应用于可为null的ForeignKey


我发现这个问题的最新答案是5年。。。我确实认为Django已经进化了,问题可能不一样。

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

在SQL中,NULL不等于NULL。这意味着如果您有两个对象,其中
field\u d==None和field\u 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\u clean
validate\u unique


不过要小心处理比赛情况。

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

而且,因为您不会在DB级别强制执行此逻辑,所以实际上可能会有加倍的记录,您应该在查询结果时检查它

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

如果可能的话,最好将此逻辑委托给DB。在这种特殊情况下,可以使用两个部分索引。关于StackOverflow也有一个类似的问题-

因此,您可以创建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)
    ]

我认为对于Django1.2来说,这是一种更清晰的方法+

在表单中,它将作为无500错误的非字段错误提出,在其他情况下,如DRF,您必须检查本案例手册,因为它将是500错误。 但它将始终检查独特的_在一起

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)
Django 2.2添加了一个新的实例,使得在数据库中处理这种情况更加容易

您将需要两个约束:

  • 现有的元组约束;及
  • 剩余的键减去可为空的键,并带有一个条件
  • 如果您有多个可为空的字段,我想您将需要处理排列

    下面是一个示例,其中有一系列字段必须是唯一的,其中只允许一个
    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'),
            ]
    

    一个可能的解决方法是创建一个虚拟
    ModelA
    对象作为
    NULL
    值。然后,您可以依靠数据库强制实施唯一性约束。

    向模型中添加一个干净的方法-请参见以下内容:


    Tx@Ivan,但我应该能够创建两个具有
    字段c==“a”,字段d==无的ModelB记录吗?)Tx!这对我来说还没有解决。我使用的是一个表单集,仅通过重写clean方法,当用户填写两个表单时,这两个表单都是有效的(formset.is_valid()),但当保存第一个表单时,第二个表单将无效。你明白我的意思吗?关于表单集和这篇文章有什么提示吗?我认为
    validate\u unique
    是一种比
    clean
    更好的重写方法……没有?它对我有用,但我必须检查我正在运行验证的实例是否将其可空字段设置为空。这里看起来不见了。可能是@Ivan的副本我也试过这篇文章,但它对我的模型集没有帮助。当试图在同一表单集中创建相同的记录时,Django并不禁止。用户仍然能够创建重复的条目Y,你是对的,在我的其他回答中,我通常会补充说,只有调用
    clean
    时,它才会工作。另一方面,除了数据迁移之外,我通常会尽量避免在模型代码之外偷偷修改数据库。+1数据完整性约束应该尽可能靠近数据,最好的方法是使用数据库约束。任何python方法(如公认的答案)都会有竞争条件问题。这可以通过Mysql实现吗?我的理解是,在使用创建唯一约束时,没有“Where is NULL”这样的特性Mysql@PatrickKenekayoro,不,MySQL没有部分索引。了解更多信息,唯一合理的解决方案是将此功能作为代码库的一部分,例如,这是迄今为止处理此类场景最干净的方法。这应该是首选答案。答案很好。太糟糕了,MySQL不支持它:(回答很好,但在我的例子中,在同一个迁移中,应该创建一个引用字段,而这个步骤是在创建约束之后进行的,它导致了异常
    django.core.exceptions.FieldDoesNotExist
    。我必须手动重新排序迁移中的步骤,这对我来说很有效。对于那些感兴趣的人来说,这是我找到的最佳答案。)e:原始SQL
    def clean(self):
            if Variants.objects.filter("""Your filter """).exclude(pk=self.pk).exists():
                raise ValidationError("This variation is duplicated.")