Django的unique_together与可为空的ForeignKey

21 浏览
0 Comments

Django的unique_together与可为空的ForeignKey

我在我的开发机上使用Django 1.8.4和Sqlite,并有以下这些模型:\n

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',)

\n我已经运行了适当的迁移并在Django Admin中注册了它们。因此,我使用Admin进行了以下测试:\n

    \n

  • 我能够创建ModelA记录,并且Django禁止我创建重复记录-如预期的那样!
  • \n

  • 当field_b不为空时,我无法创建相同的ModelB记录
  • \n

  • 但是,当使用空的field_d时,我能够创建相同的ModelB记录
  • \n

\n我的问题是:我如何在可为空的外键上应用unique_together?\n我找到的最近的答案已经有5年了...我认为Django已经发展了,问题可能不一样了。

0
0 Comments

Django中的unique_together与可空ForeignKey的问题出现的原因是因为在DB层面上没有对逻辑进行强制约束,可能会导致重复记录的出现,并且在查询结果时需要进行检查。解决方法是将逻辑委托给DB来处理,可以使用两个部分索引来实现。可以创建一个Django migration,向DB中添加两个部分索引。

代码示例:

# 假设应用程序名称为`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`方法时才会起作用。另一方面,除了数据迁移外,我通常尽量避免在模型代码之外进行数据库修改。

数据完整性约束应尽可能靠近数据,并且最好的方法是使用DB约束。任何Python的方法都会有竞态条件问题。

关于MySQL是否可以实现这个功能的问题,答案是否定的。MySQL没有部分索引的功能。可以参考dba.stackexchange.com/a/106593。唯一的合理解决方法是将此功能作为代码库的一部分,正如Ivan所建议的那样。

0
0 Comments

问题的出现的原因是,在Django中,如果有一个nullable的ForeignKey字段,当该字段为null时,与其他对象的该字段也为null的对象不被认为是重复的。这是因为在SQL中,null不等于null。因此,可以创建两个field_c为null且field_d为null的对象。

为了解决这个问题,可以重写Model的validate_unique方法来添加自定义的检查。具体代码如下:

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)

如果在forms之外使用,需要调用full_clean或validate_unique方法。同时,需要注意处理竞争条件。

另外,还有其他一些建议和问题:

- 如果想要创建两个field_c为"a"且field_d为null的ModelB记录,应该如何更改这种行为?

- 在使用formset时,通过重写clean方法,当用户填写了两个表单时,两个表单都被认为是有效的(formset.is_valid()),但当保存第一个表单后,第二个表单变为无效。有关formset和此问题的任何提示吗?

- 相比于重写clean方法,重写validate_unique方法可能会更好一些。

- 这段代码对我来说是有效的,但我需要检查正在验证的实例的nullable字段是否设置为null,这里似乎缺少了。

注意:本文完全按照原文内容进行整理,未进行任何修改。

0
0 Comments

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'),
        ]

这绝对是处理这种情况最清晰的方式。

这应该是首选的答案。

很好的答案。可惜MySQL不支持。

很好的答案,但在我的情况下,在同一个迁移中应该创建一个被引用的字段,并且这个步骤被放置在创建约束条件之后,导致了异常django.core.exceptions.FieldDoesNotExist。我不得不手动重新排序迁移步骤,这对我起作用了。

对于那些感兴趣的人,这是我在使用原始SQL时找到的最佳答案:dba.stackexchange.com/a/9760/212269

此外,正如在Django文档中所述,unique_together可能最终会被弃用,以支持UniqueConstraints。因此,一般来说,UniqueConstraints似乎是首选解决方案。

不确定MySQL,但在MariaDB 10.x中,如果其中一个列为NULL,复合唯一约束(unique index)可以为NULL。这个答案中的代码可以在MariaDB中使用,用于具有多个外键的复合唯一约束。

0