首页 文章

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

提问于
浏览
101

我有模型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模型的错误 . 有任何想法吗?

EDIT

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

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

7 回答

  • 2

    快速解决方法是:

    def save(self, *args, **kwargs):
    
        if not self.bar:
            self.bar = None
    
        super(Foo, self).save(*args, **kwargs)
    
  • 10

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

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

    这里的问题是,表单CharField的规范化“空白”值是空字符串,而不是None . 因此,如果将该字段留空,则会在DB中存储一个空字符串,而不是NULL . 在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
    
  • 122

    ** edit 11/30/2015 :在python 3中,模块全局 __metaclass__ 变量是no longer supported . 另外,截至 Django 1.10SubfieldBase 类是deprecated

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

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

    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子类化 - 这种方式无论以何种形式访问该字段,它都会"just work."你可以在它被发送到数据库之前捕获 '' ,并捕获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 文件中,然后我可以在我的应用程序的 models.py 文件中 from mysite.extras import CharNullField . 该字段就像CharField一样 - 只需记住在声明字段时设置 blank=True, null=True ,否则Django将抛出验证错误(需要字段)或创建不接受NULL的db列 .

  • 1

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

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

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

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

  • 4

    另一种可能的解决

    class Foo(models.Model):
        value = models.CharField(max_length=255, unique=True)
    
    class Bar(models.Model):
        foo = models.OneToOneField(Foo, null=True)
    
  • 54

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

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

  • 12

    我最近有同样的要求 . 我选择覆盖模型上的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()
    

相关问题