允许Django中的空值的唯一字段

时间:2009-01-18 01:13:40

标签: django orm django-models

我的模型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为空的foo对象时,它会给我一个错误:“Foo with this Bar已经存在。”

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

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

这很好用,它允许我插入超过1条记录,条形为空,所以数据库允许我做我想要的,这只是Django模型的错误。有什么想法吗?

修改

就DB而言,解决方案的可移植性不是问题,我们对Postgres感到满意。 我已经尝试设置一个可调用的唯一,这是我的函数返回True / False为 bar 的特定值,它没有给出任何错误,但是如果它没有任何影响那么缝合。< / p>

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

10 个答案:

答案 0 :(得分:132)

由于票证#9039已修复,Django并未将NULL视为等于NULL以进行唯一性检查,请参阅:

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

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

您可以通过为foo提供自己的自定义模型表单来强制管理接口为空字符串存储NULL,并使用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

答案 1 :(得分:56)

** 编辑2015年11月30日:在python 3中,模块全局__metaclass__变量为no longer supported。 另外,截至Django 1.10SubfieldBase班级为deprecated

  来自docs:的

     

django.db.models.fields.subclassing.SubfieldBase已被弃用,将在Django 1.10中删除。   从历史上看,它用于处理从数据库加载时需要进行类型转换的字段,   但它没有在.values()调用或聚合中使用。它已被from_db_value()取代。   请注意,与to_python()的情况一样,新方法在分配时不会调用SubfieldBase方法

因此,正如from_db_value() documentation和此example所建议的那样,此解决方案必须更改为:

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中的cleaning_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列。

答案 2 :(得分:12)

快速解决方法是:

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

    if not self.bar:
        self.bar = None

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

答案 3 :(得分:12)

因为我是stackoverflow的新手,我还不能回答答案,但我想指出,从哲学的角度来看,我不能同意这个问题最受欢迎的答案。 (作者:Karen Tracey)

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

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

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

答案 4 :(得分:6)

另一种可能的解决方案

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

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

答案 5 :(得分:6)

此问题已解决,https://code.djangoproject.com/ticket/4136已解决。在Django 1.11+中,您可以使用class CreateWarehouseProductTable extends Migration { public function up() { Schema::create('warehouse_products', function (Blueprint $table) { $this->setWarehouseProductColumns($table); }); Schema::create('warehouse_product_backups', function (Blueprint $table) { $this->setWarehouseProductColumns($table); $table->date('date_of_backup')->nullable(); }); } public function setWarehouseProductColumns(Blueprint $table) { $table->bigIncrements('id'); $table->integer('product_id')->default(0); $table->integer('warehouse_id'); $table->integer('free_amount')->default(0); $table->integer('booked_amount')->default(0); // ... $table->timestamps(); } } ,而不必将空白值手动转换为models.CharField(unique=True, null=True, blank=True)

答案 6 :(得分:3)

您可以添加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')
        ]

答案 7 :(得分:2)

无论好坏,Django认为NULL等同于NULL以进行唯一性检查。除了编写自己的唯一性检查实现之外,没有任何方法可以将NULL视为唯一性,无论它在表中出现多少次。

(请注意,某些数据库解决方案采用NULL的相同视图,因此依赖于一个数据库关于NULL的想法的代码可能无法移植到其他人那里)

答案 8 :(得分:1)

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

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

答案 9 :(得分:0)

如果您有模型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)

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