关于ORM:在Django中允许空值的唯一字段

Unique fields that allow nulls in Django

我有FOO模型,它有场栏。bar字段应该是唯一的,但允许空值,这意味着如果bar字段是null,我希望允许多个记录,但如果不是null,则值必须是唯一的。

这是我的模型:

1
2
3
class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)

下面是表的对应SQL:

1
2
3
4
5
6
7
8
CREATE TABLE appl_foo
(
    id serial NOT NULL,
    "name" character varying(40) NOT NULL,
    bar character varying(40),
    CONSTRAINT appl_foo_pkey PRIMARY KEY (id),
    CONSTRAINT appl_foo_bar_key UNIQUE (bar)
)

当使用管理界面创建多个BAR为空的foo对象时,它会给出一个错误:"此BAR的foo已经存在。"

但是,当我插入数据库(PostgreSQL)时:

1
2
insert into appl_foo ("name", bar) values ('test1', null)
insert into appl_foo ("name", bar) values ('test2', null)

这是可行的,很好,它允许我插入一个以上的记录,其中条为空,所以数据库允许我做我想做的,这只是Django模型的一些问题。有什么想法吗?

编辑

解决方案的可移植性,只要数据库不是问题,我们对Postgres很满意。我试过将unique设置为callable,这是我的函数,它为特定的bar值返回true/false,它没有给出任何错误,但是像这样的接缝根本没有效果。

到目前为止,我已经从BAR属性中删除了唯一的说明符,并在应用程序中处理了BAR的唯一性,但是仍然在寻找更优雅的解决方案。有什么建议吗?


由于票9039是固定的,为了唯一性检查,Django认为空值不等于空值,请参见:

http://code.djangoproject.com/ticket/9039

这里的问题是表单charfield的规范化"blank"值是空字符串,而不是空字符串。因此,如果将字段留空,将得到一个存储在数据库中的空字符串,而不是空字符串。在django和数据库规则下,空字符串等于用于唯一性检查的空字符串。

您可以强制管理界面为空字符串存储空值,方法是为foo提供自己的自定义模型表单,并使用一个干净的u bar方法将空字符串转换为无:

1
2
3
4
5
6
7
8
class FooForm(forms.ModelForm):
    class Meta:
        model = Foo
    def clean_bar(self):
        return self.cleaned_data['bar'] or None

class FooAdmin(admin.ModelAdmin):
    form = FooForm


**2015年11月30日编辑:在python 3中,不再支持模块global __metaclass__变量。另外,从Django 1.10起,SubfieldBase类被否决:

from the docs:

django.db.models.fields.subclassing.SubfieldBase has been deprecated and will be removed in Django 1.10.
Historically, it was used to handle fields where type conversion was needed when loading from the database,
but it was not used in .values() calls or in aggregates. It has been replaced with from_db_value().
Note that the new approach does not call the to_python() method on assignment as was the case with SubfieldBase.

因此,根据from_db_value()文档和本示例的建议,必须将此解决方案更改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class CharNullField(models.CharField):

   """
    Subclass of the CharField that allows empty strings to be stored as NULL.
   """

    description ="CharField that stores NULL but returns ''."

    def from_db_value(self, value, expression, connection, contex):
       """
        Gets value right out of the db and changes it if its ``None``.
       """
        if value is None:
            return ''
        else:
            return value


    def to_python(self, value):
       """
        Gets value right out of the db or an instance, and changes it if its ``None``.
       """
        if isinstance(value, models.CharField):
            # If an instance, just return the instance.
            return value
        if value is None:
            # If db has NULL, convert it to ''.
            return ''

        # Otherwise, just return the value.
        return value

    def get_prep_value(self, value):
       """
        Catches value right before sending to db.
       """
        if value == '':
            # If Django tries to save an empty string, send the db None (NULL).
            return None
        else:
            # Otherwise, just pass the value.
            return value

我认为一个比重写管理员中清理过的_数据更好的方法是对charfield进行子类化——这样无论哪个表单访问该字段,它都将"正常工作"。您可以在将''发送到数据库之前捕获它,并在它从数据库中出来之后捕获空值,Django的其余部分将不知道/不关心。一个快速而肮脏的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.db import models


class CharNullField(models.CharField):  # subclass the CharField
    description ="CharField that stores NULL but returns ''"
    __metaclass__ = models.SubfieldBase  # this ensures to_python will be called

    def to_python(self, value):
        # this is the value right out of the db, or an instance
        # if an instance, just return the instance
        if isinstance(value, models.CharField):
            return value
        if value is None:  # if the db has a NULL (None in Python)
            return ''      # convert it into an empty string
        else:
            return value   # otherwise, just return the value

    def get_prep_value(self, value):  # catches value right before sending to db
        if value == '':  
            # if Django tries to save an empty string, send the db None (NULL)
            return None
        else:
            # otherwise, just pass the value
            return value

在我的项目中,我把它转储到一个位于我站点根目录的extras.py文件中,然后我就可以在我的应用程序的models.py文件中使用from mysite.extras import CharNullField。该字段的作用与charfield类似-只需记住在声明字段时设置blank=True, null=True,否则django将抛出验证错误(需要字段)或创建不接受空值的db列。


因为我刚接触StackOverflow,所以我还没有被允许回复答案,但我想从哲学的角度指出,我不同意这个问题最流行的答案。(凯伦·特雷西)

OP要求他的bar字段是唯一的(如果它有值),否则为空。那么一定是模型本身确保了这一点。不能让外部代码检查,因为这意味着可以绕过它。(或者,如果以后编写新视图,可以忘记检查它)

因此,要保持代码的真实OOP,必须使用FOO模型的内部方法。修改save()方法或字段是很好的选项,但是使用表单来完成这一操作肯定不是很好。

我个人更喜欢使用建议的charnullfield,因为它可以移植到我将来可能定义的模型中。


快速解决方法是:

1
2
3
4
5
6
def save(self, *args, **kwargs):

    if not self.bar:
        self.bar = None

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


另一个可能的解决方案

1
2
3
4
5
class Foo(models.Model):
    value = models.CharField(max_length=255, unique=True)

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)

无论好坏,为了唯一性检查,Django认为NULL等同于NULL。实际上,除了编写自己的唯一性检查实现之外,没有其他方法可以解决这个问题,因为无论表中出现了多少次,它都认为NULL是唯一的。

(请记住,有些数据库解决方案对NULL的看法相同,因此依赖于某个数据库对NULL的想法的代码可能无法移植到其他数据库)


我最近也有同样的要求。我没有对不同的字段进行子类化,而是选择重写我的模型上的save()metod(下面称为"my model"),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
def save(self):
       """overriding save method so that we can save Null to database, instead of empty string (project requirement)"""
        # get a list of all model fields (i.e. self._meta.fields)...
        emptystringfields = [ field for field in self._meta.fields \
                # ...that are of type CharField or Textfield...
                if ((type(field) == django.db.models.fields.CharField) or (type(field) == django.db.models.fields.TextField)) \
                # ...and that contain the empty string
                and (getattr(self, field.name) =="") ]
        # set each of these fields to None (which tells Django to save Null)
        for field in emptystringfields:
            setattr(self, field.name, None)
        # call the super.save() method
        super(MyModel, self).save()

如果您有一个模型my model,并且希望my_字段为空或唯一,则可以重写模型的save方法:

1
2
3
4
5
6
class MyModel(models.Model):
    my_field = models.TextField(unique=True, default=None, null=True, blank=True)

    def save(self, **kwargs):
        self.my_field = self.my_field or None
        super().save(**kwargs)

这样,字段不能为空,只能为非空或空。空值并不矛盾唯一性