158

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

这是我的模型:

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:

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

当使用管理界面创建超过 1 个 bar 为空的 foo 对象时,它给了我一个错误:“带有这个 Bar 的 Foo 已经存在。”

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

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

这工作,很好,它允许我插入超过 1 条记录 bar 为空,所以数据库允许我做我想做的事,这只是 Django 模型有问题。有任何想法吗?

编辑

就 DB 而言,解决方案的可移植性不是问题,我们对 Postgres 很满意。我尝试为可调用设置唯一性,这是我的函数为bar的特定值返回 True/False ,它没有给出任何错误,但是缝合起来就像它根本没有效果一样。

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

4

10 回答 10

175

自从票证 #9039 被修复以来,Django 并没有考虑 NULL 等于 NULL 来进行唯一性检查,请参阅:

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

这里的问题是表单 CharField 的规范化“空白”值是一个空字符串,而不是 None。因此,如果您将该字段留空,您会得到一个空字符串,而不是 NULL,存储在 DB 中。在 Django 和数据库规则下,空字符串等于用于唯一性检查的空字符串。

您可以强制管理界面为空字符串存储 NULL,方法是为 Foo 提供您自己的自定义模型表单,并使用 clean_bar 方法将空字符串转换为 None:

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
于 2009-09-09T14:26:02.297 回答
65

** 2015 年11 月 30 日编辑:在 python 3 中,不再支持__metaclass__模块全局变量。另外,该类已被弃用Django 1.10SubfieldBase

来自文档

django.db.models.fields.subclassing.SubfieldBase已被弃用并将在 Django 1.10 中删除。从历史上看,它用于处理从数据库加载时需要类型转换的字段,但它不用于.values()调用或聚合中。它已被替换为from_db_value()。 请注意,新方法不像to_python().SubfieldBase

因此,正如from_db_value() 文档和此示例所建议的,此解决方案必须更改为:

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

我认为比在管理员中覆盖cleaned_data 更好的方法是对charfield 进行子类化——这样无论什么形式访问该字段,它都会“正常工作”。您可以''在它被发送到数据库之前捕获它,并在它从数据库中出来之后捕获 NULL,而 Django 的其余部分将不知道/关心。一个快速而肮脏的例子:

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我网站根目录中的文件中,然后我可以from mysite.extras import CharNullField在我的应用程序models.py文件中。该字段的行为就像一个 CharField - 只记得blank=True, null=True在声明字段时设置,否则 Django 将抛出验证错误(需要字段)或创建一个不接受 NULL 的 db 列。

于 2009-12-20T03:40:23.593 回答
17

因为我是stackoverflow的新手,所以我还不允许回复答案,但我想指出,从哲学的角度来看,我不能同意这个问题最流行的答案。(由凯伦特蕾西)

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

因此,要使您的代码真正保持 OOP,您必须使用 Foo 模型的内部方法。修改 save() 方法或字段是不错的选择,但使用表单来执行此操作肯定不是。

我个人更喜欢使用建议的 CharNullField,以便移植到我将来可能定义的模型。

于 2012-08-09T11:52:37.253 回答
14

快速解决方法是:

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
于 2010-06-26T16:24:25.553 回答
13

您可以在列表中添加UniqueConstraint条件nullable_field=null而不包括此字段fields。如果您还需要具有nullable_field极值 is not的约束null,您可以添加额外的一个。

注意:从 django 2.2 开始添加 UniqueConstraint

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)
    
    class Meta:
        constraints = [
            # For bar == null only
            models.UniqueConstraint(fields=['name'], name='unique__name__when__bar__null',
                                    condition=Q(bar__isnull=True)),
            # For bar != null only
            models.UniqueConstraint(fields=['name', 'bar'], name='unique__name__when__bar__not_null')
        ]
于 2020-07-07T11:25:21.670 回答
10

现在解决了https://code.djangoproject.com/ticket/4136的问题。在 Django 1.11+ 中,您models.CharField(unique=True, null=True, blank=True)无需手动将空白值转换为None.

于 2020-05-28T11:48:25.473 回答
6

另一种可能的解决方案

class Foo(models.Model):
    value = models.CharField(max_length=255, unique=True)

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)
于 2011-07-07T08:10:33.523 回答
1

我最近有同样的要求。我没有对不同的字段进行子类化,而是选择覆盖我的模型(下面命名为“MyModel”)上的 save() 方法,如下所示:

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()    
于 2010-06-22T11:50:20.800 回答
1

如果您有一个模型 MyModel 并且希望 my_field 为 Null 或唯一,您可以覆盖模型的保存方法:

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)

这样,该字段不能为空,只会是非空或空。空值与唯一性不矛盾

于 2019-04-06T11:56:46.830 回答
0

无论好坏,Django 认为NULL等同于NULL用于唯一性检查。除了编写自己的唯一性检查实现之外,真的没有办法解决它,NULL无论它在表中出现多少次,它都认为是唯一的。

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

于 2009-01-18T03:49:52.673 回答