Django:“unique_together”和“空白=真”

Posted

技术标签:

【中文标题】Django:“unique_together”和“空白=真”【英文标题】:Django: 'unique_together' and 'blank=True' 【发布时间】:2011-08-11 22:41:45 【问题描述】:

我有一个看起来像这样的 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 中不止一次出现相同的 name,那么我会收到错误消息:“具有此名称和父级的 MyModel 已存在。”

但是,当我使用相同的 parent 保存多个 MyModelname 字段为空白时,我也会收到错误消息,但这应该是允许的。所以基本上我不想在name 字段为空白时出现上述错误。这有可能吗?

【问题讨论】:

【参考方案1】:

首先,空白(空字符串)与 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, ...)

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

【讨论】:

为什么您会看到行为差异是因为您的名称字段是 CharField。 CharFields 不存储 NULL,而是存储空字符串。快速谷歌搜索将引导您找到原因。 好电话。整件事实际上是由于 charfield 上缺少 null 造成的 +1 我更喜欢由数据库强制执行约束的解决方案。 你不应该继承models.CharField而不是forms.CharField吗?与this answer比较。 不,我改变行为的不是模型字段,而是 FORM 字段。将空字段转换为空字符串 '' 的形式非常好,但在唯一性值为 None(数据库中为 Null)的情况下更合适【参考方案2】:

使用unique_together,您是在告诉 Django 您不希望任何两个具有相同parentname 属性的MyModel 实例——即使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)

希望对您有所帮助。

【讨论】:

我认为使用 .exists 是首选方式,而不是 len(queryset)。 @bigmatty:这个解决方案不好,因为这意味着如果您想稍后更改实例的其他一些字段并使用 .save() 保存它们,那么您将得到一个异常。跨度> @WesDec:只有在尝试将具有相同 parentname 的实例保存为另一个对象时,您才应该得到一个异常——当然,除非 name 属性是一个空字符串。这就是你说你想要的行为,不是吗? @bigmattyh:是的,但想象一下我创建了一个实例,其中“父”和“名称”是唯一的,并且名称不是空字符串。如果我稍后从数据库中获取相同的实例并更改字段然后将实例保存到数据库中,则将再次对“父”和“名称”进行相同的检查,这一次我会得到一个异常。跨度> @WesDec:这并不难解决。有关处理这种情况的编辑代码,请参见上文。【参考方案3】:

这个解决方案与@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 而不是一般异常。该方案的好处是,在验证ModelForm时,使用.is_valid(),会自动调用上面的models.clean()方法,并将ValidationError字符串保存在.errors中,以便在html模板中显示。

如果您不同意此解决方案,请告诉我。

【讨论】:

这个解决方案的唯一问题是当模型的save() 方法被调用时clean() 没有被调用(参见docs)。但我从来没有真正理解为什么,因为它看起来确实是一个更干净(没有双关语)的解决方案。 可以在model'ssave方法中调用self.clean()【参考方案4】:

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

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

【讨论】:

【参考方案5】:

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)

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

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

【讨论】:

以上是关于Django:“unique_together”和“空白=真”的主要内容,如果未能解决你的问题,请参考以下文章

Django 数据库模型“unique_together”不起作用?

如何在 django 的核心模型中添加“unique_together”约束?

UniqueConstraint 与 unique_together 之间的区别 - Django 2.2?

如何在 django 中使用 Uniqueconstraints 函数创建 unique_together 索引?

Django REST 框架:“此字段是必需的。”与 required=False 和 unique_together

约束 unique_together 可能与 django 类中的唯一字段冲突