在Django中允许为空的唯一字段


135

我有模型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)
)   

当使用管理界面在bar为null的情况下创建多个1个foo对象时,它给我一个错误:“此Bar的Foo已经存在。”

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

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

这样做很好,它允许我插入1条以上且bar为null的记录,因此数据库允许我执行所需的操作,这是Django模型的错误。有任何想法吗?

编辑

就数据库而言,解决方案的可移植性不是问题,我们对Postgres感到满意。我尝试过设置可调用对象的唯一性,这是我的函数针对bar的特定值返回True / False ,它没有给出任何错误,但是缝起来根本没有任何作用。

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


我还不能发表评论,因此在这里添加了mayyhal:从Django 1.4开始,您将需要def get_db_prep_value(self, value, connection, prepared=False)作为方法调用。请访问groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ了解更多信息。以下方法也适用于我:def get_prep_value(self,value):if value ==“”:#如果Django尝试保存''字符串,则发送db None(NULL)return None:返回值#否则,仅传递值
Jens

我为此打开了一张Django票。添加您的支持。code.djangoproject.com/ticket/30210#ticket
卡尔·布鲁贝克

Answers:


154

自从票务#9039被修复以来,Django就唯一性检查的目的未将NULL视为NULL,请参阅:

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

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

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

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

2
如果bar为空白,则在pre_save方法中将其替换为None。我想代码会更干燥。
Ashish Gupta

6
该答案仅有助于基于表单的数据输入,而无助于实际保护数据完整性。可以通过导入脚本,从外壳,通过API或任何其他方式输入数据。覆盖save()方法要比为每种可能接触数据的表单创建自定义案例要好得多。
hacker's

Django 1.9+ 在实例中需要fieldsor exclude属性ModelForm。您可以通过Meta从ModelForm中省略内部类以在admin中使用来解决此问题。参考:docs.djangoproject.com/en/1.10/ref/contrib/admin/…– user85461 '01
15/18

62

** edit 11/30/2015:在python 3中,不再支持 module-global __metaclass__变量。Additionaly,为的阶级被弃用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

我认为,比在admin中覆盖cleaned_data更好的方法是将charfield子类化-这种方法无论以何种形式访问该字段,都将“正常工作”。您可以''在将其发送到数据库之前捕获NULL,并在将其从数据库中删除之后立即捕获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列。


3
在get_prep_value中,应该删除该值,以防该值有多个空格。
2015年

1
更新的答案在2016年与Django 1.10并使用EmailField一起使用时效果很好。
k0nG

4
如果CharField要将a 更新为a CharNullField,则需要分三步进行。首先,添加null=True到该字段并进行迁移。然后,进行数据迁移以更新任何空白值,使其为空。最后,将字段转换为CharNullField。如果在执行数据迁移之前转换字段,则数据迁移将不会执行任何操作。
mlissner'7

3
请注意,在更新的解决方案中,from_db_value()不应具有该额外contex参数。应该是def from_db_value(self, value, expression, connection):
Phil Gyford

1
@PhilGyford的注释自2.0起适用。
沙希德·哈克

16

因为我不是stackoverflow的新手,所以我尚未答复答案,但是我想指出,从哲学的角度来看,我不同意这个问题的最受欢迎答案。(凯伦·特雷西)

OP要求他的bar字段(如果有值)是唯一的,否则为null。然后必须是模型本身确保确实如此。它不能留给外部代码来检查,因为这意味着可以绕过它。(或者,如果您以后编写新视图,则可以忘记检查它)

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

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


13

快速解决方法是:

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

    if not self.bar:
        self.bar = None

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

2
请注意,使用MyModel.objects.bulk_create()会绕过此方法。
BenjaminGolder

从管理面板保存时,是否会调用此方法?我试过了,但是没有。
Kishan Mehta

1
不幸的是,@ Kishan django-admin面板将跳过这些钩子
Vincent Buscarello

@ e-satis您的逻辑是正确的,所以我实现了这一点,但是错误仍然是一个问题。我被告知null是重复的。
文森特·布斯卡洛

6

另一种可能的解决方案

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

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

这不是一个好的解决方案,因为您正在创建不必要的关系。
BurakÖzdemir19年


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

1

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

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)

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


0

无论好坏,Django都认为NULL等同于NULL唯一性检查。除非编写自己的唯一性检查实现,否则它实际上是没有办法的,NULL无论它在表中发生了多少次,该检查都被认为是唯一的。

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


6
这不是正确的答案。请参阅此答案以获取解释
卡尔·G

2
同意这是不正确的。我刚刚在Django 1.4中测试了IntegerField(blank = True,null = True,unique = True),它允许多行具有null值。
slacy 2012年

0

您可以添加UniqueConstraint条件,但nullable_field=null不将该字段包括在fields列表中。如果您还需要限制nullable_field值不是的约束,则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')
        ]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.