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

我有福模型,它有字段栏。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个 Foo 对象,其中 Bar 为 null 时,它会给我一个错误: “ Foo with this Bar already been。”

但是,当我插入数据库(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,它没有给出任何错误,不管它看起来有多么接近,都没有任何效果。

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

54996 次浏览

无论是好是坏,Django 都认为 NULL等价于 NULL,以便进行唯一性检查。除非编写自己的唯一性检查实现,否则真的没有其他办法可以绕过这个问题,因为无论 NULL在一个表中出现多少次,它都认为 NULL是唯一的。

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

Django 没有考虑到 NULL等于 NULL的唯一性检查,因为票号9039是固定的,见:

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

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

通过使用 clean_bar方法为 Foo提供自定义的模型表单,可以强制管理界面为空字符串存储 NULLclean_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

* * 2015年11月30日: 在 python3中,模块-全局 __metaclass__变量是 不再支持。 此外,从 Django 1.10开始,SubfieldBase的课程是 不赞成:

来自 医生:

django.db.models.fields.subclassing.SubfieldBase已被弃用,将在 Django 1.10中删除。 过去,它用于处理从数据库加载时需要进行类型转换的字段, 但是它并没有用在 .values()呼叫或者聚合中,而是被 from_db_value()所取代。 注意,新方法不像 SubfieldBase那样在赋值时调用 to_python()方法

因此,根据 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

我认为比在管理中覆盖 clean _ 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文件,生活在我的网站的根,然后我可以只在我的应用程序的 models.py文件的 from mysite.extras import CharNullField。字段的作用就像 CharField 一样——只要记住在声明字段时设置 blank=True, null=True,否则 Django 将抛出验证错误(字段需要)或创建不接受 NULL 的 db 列。

我最近也有同样的要求。我没有子类化不同的字段,而是选择覆盖模型上的 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()

快速解决办法是:

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


if not self.bar:
self.bar = None


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

另一个可能的解决办法

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


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

因为我是堆栈溢出的新手,我还不能回答这个问题,但是我想指出,从哲学的角度来看,我不能同意这个问题最流行的答案。(by Karen Tracey)

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

因此,要使代码真正面向对象,必须使用 Foo 模型的内部方法。修改 save ()方法或字段是不错的选择,但是使用表单进行此操作肯定不是。

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

如果您有一个模型 MyModel,并希望 my _ field 为 Null 或惟一,那么您可以重写 model 的 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)

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

这个问题现在已经解决了,因为解析了 https://code.djangoproject.com/ticket/4136。在 Django 1.11 + 中,您可以使用 models.CharField(unique=True, null=True, blank=True),而不必手动将空白值转换为 None

您可以添加条件为 nullable_field=nullUniqueConstraint,并且在 fields列表中不包含此字段。 如果还需要约束,且 nullable_field的值不是 null,则可以添加另一个约束。

注意: UniqueConstraint 是从 django 2.2开始添加的

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