Django的unique_together与可为空的ForeignKey
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禁止我创建重复记录-如预期的那样!
- 当field_b不为空时,我无法创建相同的ModelB记录
- 但是,当使用空的field_d时,我能够创建相同的ModelB记录
\n
\n
\n
\n我的问题是:我如何在可为空的外键上应用unique_together?\n我找到的最近的答案已经有5年了...我认为Django已经发展了,问题可能不一样了。
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所建议的那样。
问题的出现的原因是,在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,这里似乎缺少了。
注意:本文完全按照原文内容进行整理,未进行任何修改。
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中使用,用于具有多个外键的复合唯一约束。