Django: 'unique_together' 和 'blank=True

25 投票
5 回答
12702 浏览
提问于 2025-04-16 16:20

我有一个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")

这个模型的工作方式是这样的:如果在同一个parent下,有多个相同的name,我就会收到一个错误提示:“这个名称和父级的组合已经存在。”

不过,当我尝试保存多个MyModel,它们的parent相同,但name字段是空白的时候,我也会收到错误提示,但这其实是应该允许的。所以,简单来说,当name字段为空时,我不想看到上面的错误提示。有没有办法做到这一点呢?

5 个回答

3

你可以使用约束来设置一个部分索引,方法如下:

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

13

使用 unique_together 的时候,你是在告诉 Django,你不希望有两个 MyModel 的实例,它们的 parentname 属性是一样的——即使 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)

希望这对你有帮助。

18

首先,空字符串(就是没有内容的字符串)和空值(null)是两回事('' != 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, ...)

这样的话,如果你把这个字段留空,它在数据库中就会存储空值,而不是空字符串('')。

撰写回答