ChatGPT解决这个技术问题 Extra ChatGPT

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

我有具有字段栏的模型 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 的唯一性,但仍在寻找更优雅的解决方案。有什么建议吗?

我还不能发表评论,所以在这里对mightyhal 做一点补充:从Django 1.4 开始,您需要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传递价值
我为此开了一张 Django 票。添加您的支持。 code.djangoproject.com/ticket/30210#ticket

x
xblitz

自从票证 #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

如果 bar 为空白,则在 pre_save 方法中将其替换为 None 。我想代码会更干。
此答案仅有助于基于表单的数据输入,但对实际保护数据完整性没有任何帮助。可以通过导入脚本、从 shell、通过 API 或任何其他方式输入数据。重写 save() 方法比为每个可能接触数据的表单创建自定义案例要好得多。
Django 1.9+ 需要 ModelForm 实例中的 fieldsexclude 属性。您可以通过省略 ModelForm 中的 Meta 内部类以在管理中使用来解决此问题。参考:docs.djangoproject.com/en/1.10/ref/contrib/admin/…
这个答案不是最理想的,你可以从@tBuLi 看到答案,了解原因。
m
mlissner

** 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() 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 列。


在 get_prep_value 中,您应该删除该值,以防该值有多个空格。
此处更新的答案在 2016 年使用 Django 1.10 并使用 EmailField 效果很好。
如果您要将 CharField 更新为 CharNullField,则需要分三个步骤完成。首先,将 null=True 添加到该字段,然后迁移它。然后,进行数据迁移以更新任何空白值,使其为空值。最后,将该字段转换为 CharNullField。如果您在进行数据迁移之前转换了字段,那么您的数据迁移将不会执行任何操作。
请注意,在更新的解决方案中,from_db_value() 不应具有该额外的 contex 参数。它应该是 def from_db_value(self, value, expression, connection):
@PhilGyford 的评论自 2.0 起适用。
t
tBuLi

因为我是stackoverflow的新手,所以我还不允许回复答案,但我想指出,从哲学的角度来看,我不能同意这个问题最流行的答案。 (由凯伦特蕾西)

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

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

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


А
Андрей Лебедев

您可以添加条件为 nullable_field=nullUniqueConstraint,并且不将该字段包含在 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')
        ]

这样可行!但我得到一个 IntegrityError 异常而不是表单验证错误。你怎么处理?抓住它并在创建+更新视图中引发 ValidationError?
上帝,我为什么要为此一直向下滚动?谢谢你救了我。
如果 bar 为空,您希望再次插入相同的名称。我认为这会阻止这样做。即您希望能够插入 ("name", null) 两次。
e
e-satis

快速解决方法是:

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

    if not self.bar:
        self.bar = None

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

请注意,使用 MyModel.objects.bulk_create() 会绕过此方法。
当我们从管理面板保存时,是否会调用此方法?我试过了,但没有。
不幸的是,@Kishan django-admin 面板将跳过这些钩子
@e-satis 你的逻辑是合理的,所以我实现了这个,但错误仍然是一个问题。我被告知 null 是重复的。
p
praseodym

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


这对我在 Django 3.1 上使用 CharField 但不适用于 TextField - 约束失败,因为管理员表单仍在传递空字符串。
django Django 3.1 上的 EmailField 不起作用
@Aditya 刚刚在 Django 3.1.5 上尝试过,它适用于 EmailField
外键呢?例如 2 个字段是 FK,但一个可以为空。 (在 DB 中,它是一个多列唯一索引
R
Radagast

另一种可能的解决方案

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

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

这不是一个好的解决方案,因为您正在创建不必要的关系。
c
captnswing

我最近有同样的要求。我没有对不同的字段进行子类化,而是选择覆盖我的模型(下面命名为“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()    

J
Joseph Bani

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

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


J
James Bennett

无论好坏,为了唯一性检查,Django 认为 NULL 等同于 NULL。除了编写自己的唯一性检查实现之外,实际上没有其他办法,它认为 NULL 是唯一的,无论它在表中出现多少次。

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


这不是正确的答案。请参阅this answer for explanation
同意这是不正确的。我刚刚在 Django 1.4 中测试了 IntegerField(blank=True, null=True, unique=True) ,它允许多行具有空值。