Django unique_together与可空的ForeignKey

Mat*_*imB 18 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 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',)
Run Code Online (Sandbox Code Playgroud)

我已经运行了正确的迁移并在Django Admin中注册了它们.所以,使用Admin我做过这个测试:

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

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

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

Dan*_*ley 47

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

您将需要两个约束:

  1. 现有的元组约束;和
  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'),
        ]
Run Code Online (Sandbox Code Playgroud)

  • 很好的答案。可惜 MySQL 不支持:( (2认同)

Iva*_*van 18

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

在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)
Run Code Online (Sandbox Code Playgroud)

如果在表格之外使用,则必须致电full_cleanvalidate_unique.

尽管如此,要小心处理竞争状况.


vvk*_*sov 9

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

并且因为您没有在数据库级别强制使用此逻辑,实际上可能会有双倍的记录,您应该在查询结果时进行检查.

关于你的解决方案,它可能对表单有好处,但我不认为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)
    ]
Run Code Online (Sandbox Code Playgroud)