Django unique_和可为空的外键
我在使用Sqlite的开发机器中使用Django 1.8.4,我有以下模型: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
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记录
我发现这个问题的最新答案是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.")