Django:'unique_together'和'blank = True'

时间:2011-04-24 17:47:53

标签: python sql django

我有一个看起来像这样的Django模型:

class MyModel(models.Model):
    parent = models.ForeignKey(ParentModel)
    name   = models.CharField(blank=True, max_length=200)
    ... other fields ...

    class Meta:
        unique_together = ("name", "parent")

这按预期工作;如果在同一个name中多次出现parent次,则会出现错误:“具有此名称和父级的MyModel已存在。”

但是,如果我使用相同的MyModel保存多个parentname字段为空,我也会收到错误,但应该允许这样做。所以基本上我不想在name字段为空时得到上述错误。这有可能吗?

5 个答案:

答案 0 :(得分:16)

首先,空白(空字符串)与空('' != None)不相同。

其次,当您将字段留空时,通过表单使用的Django CharField将存储空字符串

因此,如果您的字段不是CharField,则只需添加null=True即可。 但在这种情况下你需要做更多的事情。您需要创建forms.CharField的子类并覆盖它的clean方法以在空字符串上返回None,如下所示:

class NullCharField(forms.CharField):
    def clean(self, value):
        value = super(NullCharField, self).clean(value)
        if value in forms.fields.EMPTY_VALUES:
            return None
        return value

然后在ModelForm的表单中使用它:

class MyModelForm(forms.ModelForm):
    name = NullCharField(required=False, ...)

这样,如果你把它留空,它将在数据库中存储null而不是空字符串(''

答案 1 :(得分:11)

使用unique_together,您告诉Django您不希望任何两个具有相同MyModelparent属性的name个实例 - 即使在name是一个空字符串。

使用相应数据库列上的unique属性在数据库级别强制执行此操作。因此,要对此行为进行任何例外处理,您必须避免在模型中使用unique_together

相反,您可以通过覆盖模型上的save方法并在那里强制执行唯一约束来获得所需内容。当您尝试保存模型的实例时,您的代码可以检查是否存在具有相同parentname组合的任何现有实例,并且如果存在,则拒绝保存实例。但是如果name是空字符串,您也可以允许保存实例。这个的基本版本可能如下所示:

class MyModel(models.Model):
    ...

    def save(self, *args, **kwargs):

        if self.name != '':
            conflicting_instance = MyModel.objects.filter(parent=self.parent, \
                                                          name=self.name)
            if self.id:
                # This instance has already been saved. So we need to filter out
                # this instance from our results.
                conflicting_instance = conflicting_instance.exclude(pk=self.id)

            if conflicting_instance.exists():
                raise Exception('MyModel with this name and parent already exists.')

        super(MyModel, self).save(*args, **kwargs)

希望有所帮助。

答案 2 :(得分:2)

此解决方案与@bigmattyh给出的解决方案非常相似,但是,我找到了下面的页面,其中描述了应该在何处进行验证:

http://docs.djangoproject.com/en/1.3/ref/models/instances/#validating-objects

我最终使用的解决方案如下:

from django    import forms

class MyModel(models.Model):
...

def clean(self):
    if self.name != '':
        instance_exists = MyModel.objects.filter(parent=self.parent,
                                                 name=self.name).exists()
        if instance_exists:
            raise forms.ValidationError('MyModel with this name and parent already exists.')

请注意,引发了ValidationError而不是泛型异常。此解决方案的好处是,在使用.is_valid()验证ModelForm时,会自动调用上面的模型.clean()方法,并将ValidationError字符串保存在.errors中,以便它可以显示在html模板中。

如果您不同意此解决方案,请与我们联系。

答案 3 :(得分:0)

您可以使用约束来设置部分索引,如下所示:

class MyModel(models.Model):
    parent = models.ForeignKey(ParentModel)
    name   = models.CharField(blank=True, max_length=200)
    ... other fields ...

    class Meta:    
      constraints = [
        models.UniqueConstraint(
          fields=['name', 'parent'],
          condition=~Q(name='')
          name='unique_name_for_parent'
        )
      ]

这允许像 UniqueTogether 这样的约束只应用于某些行(基于您可以使用 Q 定义的条件)。

顺便说一句,这恰好也是 Django 推荐的前进路径:https://docs.djangoproject.com/en/3.2/ref/models/options/#unique-together

更多文档:https://docs.djangoproject.com/en/3.2/ref/models/constraints/#django.db.models.UniqueConstraint

答案 4 :(得分:-1)

bigmattyh对正在发生的事情给出了很好的解释。我只想添加一个save方法。

def save(self, *args, **kwargs):
    if self.parent != None and MyModels.objects.filter(parent=self.parent, name=self.name).exists():
        raise Exception('MyModel with this name and parent exists.')
    super(MyModel, self).save(*args, **kwargs)

我认为我选择通过覆盖我的模型的干净方法来做类似的事情,它看起来像这样:

from django.core.exceptions import ValidationError
def clean(self):
    if self.parent != None and MyModels.objects.filter(parent=self.parent, name=self.name).exists():
        raise ValidationError('MyModel with this name and parent exists.')