关于python:Django:’unique_together’和’blank = True’

Django: 'unique_together' and 'blank=True'

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

1
2
3
4
5
6
7
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已经存在。"

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


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

其次,当通过表单使用django charfield时,当您将字段留空时,将存储空字符串。

所以,如果你的字段不是charfield,你应该在其中添加null=True。但在这种情况下,你需要做的不止这些。您需要创建forms.CharField的子类,并重写它的clean方法来在空字符串上不返回任何值,如下所示:

1
2
3
4
5
6
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

然后在模型窗体中使用它:

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

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


使用unique_together时,您告诉django您不需要任何两个具有相同parentname属性的MyModel实例,即使name是空字符串也适用。

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

相反,您可以通过在模型上覆盖save方法并在模型上强制实施唯一的约束来获得您想要的。当您试图保存模型的实例时,您的代码可以检查是否有任何现有实例具有相同的parentname组合,如果存在,则拒绝保存该实例。但是,如果name是空字符串,也可以允许保存实例。基本版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

希望有帮助。


这个解决方案与@bigmattyh给出的解决方案非常相似,但是,我发现下面的页面描述了应该在哪里进行验证:

http://docs.djangoproject.com/en/1.3/ref/models/instances/验证对象

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

1
2
3
4
5
6
7
8
9
10
11
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()验证模型窗体时,将自动调用上面的models.clean()方法,并将validationError字符串保存在.errors中,以便在HTML模板中显示。

如果你不同意这个解决方案,请告诉我。


Bigmatthy很好地解释了正在发生的事情。我将添加一个可能的save方法。

1
2
3
4
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方法来做类似的事情,它看起来是这样的:

1
2
3
4
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.')