我的模型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 唯一性,但仍在寻找更优雅的解决方案。有什么建议吗?
答案 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.10
,SubfieldBase
班级为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。空值不会与唯一性矛盾