我有具有字段栏的模型 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 的唯一性,但仍在寻找更优雅的解决方案。有什么建议吗?
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=="": #if Django 尝试保存 '' 字符串,发送 db None (NULL) return None else: return value #otherwise, just传递价值
自从票证 #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
** edit 11/30/2015:在 python 3 中,模块全局 __metaclass__
变量是 no longer supported。另外,截至 Django 1.10
,SubfieldBase
类为 deprecated:
来自文档:django.db.models.fields.subclassing.SubfieldBase 已被弃用,将在 Django 1.10 中删除。从历史上看,它用于处理从数据库加载时需要类型转换的字段,但它没有用于 .values() 调用或聚合中。它已被替换为 from_db_value()。请注意,新方法不像 SubfieldBase 那样在赋值时调用 to_python() 方法。
因此,正如 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
我认为比在管理员中覆盖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
文件中,然后我可以在我的应用程序的 models.py
文件中from mysite.extras import CharNullField
。该字段的作用就像一个 CharField - 只需记住在声明该字段时设置 blank=True, null=True
,否则 Django 将抛出验证错误(需要字段)或创建一个不接受 NULL 的 db 列。
CharField
更新为 CharNullField
,则需要分三个步骤完成。首先,将 null=True
添加到该字段,然后迁移它。然后,进行数据迁移以更新任何空白值,使其为空值。最后,将该字段转换为 CharNullField。如果您在进行数据迁移之前转换了字段,那么您的数据迁移将不会执行任何操作。
from_db_value()
不应具有该额外的 contex
参数。它应该是 def from_db_value(self, value, expression, connection):
因为我是stackoverflow的新手,所以我还不允许回复答案,但我想指出,从哲学的角度来看,我不能同意这个问题最流行的答案。 (由凯伦特蕾西)
如果它有值,则 OP 要求他的 bar 字段是唯一的,否则为 null。那么一定是模型本身确保了这种情况。不能让外部代码检查这一点,因为这意味着它可以被绕过。 (或者以后写新视图可以忘记勾选)
因此,要使您的代码真正保持 OOP,您必须使用 Foo 模型的内部方法。修改 save() 方法或字段是不错的选择,但使用表单来执行此操作肯定不是。
我个人更喜欢使用建议的 CharNullField,以便移植到我将来可能定义的模型。
您可以添加条件为 nullable_field=null
的 UniqueConstraint
,并且不将该字段包含在 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')
]
快速解决方法是:
def save(self, *args, **kwargs):
if not self.bar:
self.bar = None
super(Foo, self).save(*args, **kwargs)
MyModel.objects.bulk_create()
会绕过此方法。
现在解决了 https://code.djangoproject.com/ticket/4136,此问题已得到解决。在 Django 1.11+ 中,您可以使用 models.CharField(unique=True, null=True, blank=True)
而无需手动将空白值转换为 None
。
CharField
但不适用于 TextField
- 约束失败,因为管理员表单仍在传递空字符串。
EmailField
不起作用
EmailField
。
另一种可能的解决方案
class Foo(models.Model):
value = models.CharField(max_length=255, unique=True)
class Bar(models.Model):
foo = models.OneToOneField(Foo, null=True)
我最近有同样的要求。我没有对不同的字段进行子类化,而是选择覆盖我的模型(下面命名为“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()
如果您有一个模型 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)
这样,该字段不能为空,只会是非空或空。空值与唯一性不矛盾
无论好坏,为了唯一性检查,Django 认为 NULL
等同于 NULL
。除了编写自己的唯一性检查实现之外,实际上没有其他办法,它认为 NULL
是唯一的,无论它在表中出现多少次。
(请记住,一些 DB 解决方案采用相同的 NULL
视图,因此依赖于一个 DB 关于 NULL
的想法的代码可能无法移植到其他数据库)
不定期副业成功案例分享
ModelForm
实例中的fields
或exclude
属性。您可以通过省略 ModelForm 中的Meta
内部类以在管理中使用来解决此问题。参考:docs.djangoproject.com/en/1.10/ref/contrib/admin/…