【问题标题】:Unique fields that allow nulls in Django在 Django 中允许空值的唯一字段
【发布时间】:2010-10-02 01:13:51
【问题描述】:

我有具有字段栏的模型 Foo。 bar 字段应该是唯一的,但允许其中包含 null,这意味着如果 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 为 null 的 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 属性中删除了唯一说明符并处理了应用程序中的 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

标签: django orm django-models


【解决方案1】:

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

(请记住,某些 DB 解决方案与 NULL 的观点相同,因此依赖于一个 DB 关于 NULL 的想法的代码可能无法移植到其他人)

【讨论】:

  • 这不是正确的答案。见this answer for explanation
  • 同意这是不正确的。我刚刚在 Django 1.4 中测试了 IntegerField(blank=True, null=True, unique=True) ,它允许多行具有空值。
【解决方案2】:

自从票证 #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/…
【解决方案3】:

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

来自docs

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 起适用。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    快速解决方法是:

    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 是重复的。
    【解决方案6】:

    另一种可能的解决方案

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

    【讨论】:

    • 这不是一个好的解决方案,因为您正在创建不必要的关系。
    【解决方案7】:

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

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

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

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

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

        【解决方案9】:

        现在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 中它是一个多列唯一索引
        【解决方案10】:

        您可以在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')
                ]
        

        【讨论】:

        • 行得通!但我得到一个 IntegrityError 异常而不是表单验证错误。你怎么处理?抓住它并在创建+更新视图中引发 ValidationError?
        • 天哪,我为什么要为此一直向下滚动?谢谢你救了我。
        • 如果 bar 为空,您希望再次插入同名。我认为这会阻止这样做。即您希望能够插入 ("name", null) 两次。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-23
        • 1970-01-01
        • 2013-12-07
        • 2021-05-09
        • 2019-08-20
        • 1970-01-01
        相关资源
        最近更新 更多