【问题标题】:Unique model field in Django and case sensitivity (postgres)Django 中的唯一模型字段和区分大小写(postgres)
【发布时间】:2023-03-27 13:48:01
【问题描述】:

考虑以下情况:-

假设我的应用允许用户在他们的 国家。为清楚起见,我们只考虑 ASCII 字符 在这里。

在美国,用户可以创建名为“德克萨斯”的州。如果这个应用程序 正在内部使用,假设用户不在乎它是否是 拼写为“texas”或“Texas”或“teXas”

但重要的是,如果出现以下情况,系统应该阻止创建“texas” “德克萨斯”已经在数据库中。

如果模型如下:

class State(models.Model):
    name = models.CharField(max_length=50, unique=True)

postgres 中的唯一性是区分大小写的;也就是postgres 将允许用户按原样创建“texas”和“Texas” 被认为是独一无二的。

在这种情况下可以做些什么来防止这种行为。如何 使用 Django 提供 case-insenstitive 唯一性 Postgres

现在我正在执行以下操作以防止创建案例- 不敏感的重复。

class CreateStateForm(forms.ModelForm):
    def clean_name(self):
        name = self.cleaned_data['name']
        try:
            State.objects.get(name__iexact=name)
        except ObjectDoesNotExist:
            return name
        raise forms.ValidationError('State already exists.')

    class Meta:
        model = State

在许多情况下,我必须进行此检查,但我并不热衷于在各处编写类似的 iexact 检查。

只是想知道是否有内置或 更好的方法?也许 db_type 会有所帮助?也许还有其他解决方案?

【问题讨论】:

  • 我认为你已经做对了。至少我是这样处理标签的,所以我的标签云不会以“标签”、“标签”和“标签”结尾。
  • 是的,但是在这里 Django 的内置 unique 并没有多大帮助,是吗?毕竟我必须自己到处做独特的检查。
  • 我会对应用程序进行编码以确保字符串仅以小写形式进入数据库。然后检查一下。
  • 我宁愿让用户保留他想保留的任何案例。

标签: django postgresql django-models unique


【解决方案1】:

您可以定义从models.CharField 派生的自定义模型字段。 该字段可以检查重复值,忽略大小写。

自定义字段文档在这里http://docs.djangoproject.com/en/dev/howto/custom-model-fields/

查看http://code.djangoproject.com/browser/django/trunk/django/db/models/fields/files.py 的示例,了解如何通过子类化现有字段来创建自定义字段。

你可以使用 PostgreSQL 的 citext 模块https://www.postgresql.org/docs/current/static/citext.html

如果您使用此模块,自定义字段可以将“db_type”定义为 PostgreSQL 数据库的 CITEXT。

这会导致自定义字段中唯一值的比较不区分大小写。

【讨论】:

  • 这是一个有趣的解决方案,而且似乎比这里提到的其他解决方案更具 Django 风格。
  • 看我的回答下面的明确步骤
  • 从 Django 1.11 开始,CICharField 现在是 Django 的一部分。见stackoverflow.com/a/43812440/1089026
【解决方案2】:

或者,您可以更改默认查询集管理器以在字段上执行不区分大小写的查找。在尝试解决类似的问题时,我遇到了:

http://djangosnippets.org/snippets/305/

为方便起见,此处粘贴代码:

from django.db.models import Manager
from django.db.models.query import QuerySet

class CaseInsensitiveQuerySet(QuerySet):
    def _filter_or_exclude(self, mapper, *args, **kwargs):
        # 'name' is a field in your Model whose lookups you want case-insensitive by default
        if 'name' in kwargs:
            kwargs['name__iexact'] = kwargs['name']
            del kwargs['name']
        return super(CaseInsensitiveQuerySet, self)._filter_or_exclude(mapper, *args, **kwargs)

# custom manager that overrides the initial query set
class TagManager(Manager):
    def get_query_set(self):
        return CaseInsensitiveQuerySet(self.model)

# and the model itself
class Tag(models.Model):
    name = models.CharField(maxlength=50, unique=True, db_index=True)

    objects = TagManager()

    def __str__(self):
        return self.name

【讨论】:

  • 这种方法不适用于像 name__in=[] 或 related__name= 这样的复合查询。
【解决方案3】:

Mayuresh 回答的明确步骤:

  1. 在 postgres 中执行:CREATE EXTENSION citext;

  2. 在你的 models.py 中添加:

    from django.db.models import fields
    
    class CaseInsensitiveTextField(fields.TextField):
        def db_type(self, connection):
            return "citext"
    

    参考:https://github.com/zacharyvoase/django-postgres/blob/master/django_postgres/citext.py

  3. 在您的模型中使用:name = CaseInsensitiveTextField(unique=True)

【讨论】:

    【解决方案4】:

    在 Postgres 方面,功能唯一索引可以让您强制执行不区分大小写的唯一值。还提到了 citext,但这将适用于旧版本的 PostgreSQL,并且通常是一种有用的技术。

    例子:

    # create table foo(bar text);
    CREATE TABLE
    # create unique index foo_bar on foo(lower(bar));
    CREATE INDEX
    # insert into foo values ('Texas');
    INSERT 0 1
    # insert into foo values ('texas');
    ERROR:  duplicate key value violates unique constraint "foo_bar"
    

    【讨论】:

    • 我已经尝试过了,可以确认它有效。但是 Mayuresh 的回答让我可以在 Django 中生活。
    • 好吧,您也应该始终在数据库中强制执行您的约束。
    • 在 Django 创建模型后,在数据库级别“自动”执行此操作的好方法是什么。我要求一种脚本方式来执行此操作,而不是在 django 的 syncdb 之后手动编辑 db
    【解决方案5】:

    一个非常简单的解决方案:

    class State(models.Model):
        name = models.CharField(max_length=50, unique=True)
    
        def clean(self):
            self.name = self.name.capitalize()
    

    【讨论】:

      【解决方案6】:

      除了已经提到的覆盖保存选项之外,您可以简单地将所有文本以小写形式存储在数据库中并在显示时将它们大写。

      class State(models.Model):
          name = models.CharField(max_length=50, unique=True)
      
          def save(self, force_insert=False, force_update=False):
              self.name = self.name.lower()
              super(State, self).save(force_insert, force_update)
      

      【讨论】:

      • 我现在收到一个错误,当我有相同的文本时,我怎样才能在页面上显示错误,而不是得到 "IntegrityError at /add_category/ UNIQUE constraint failed: theblog_category .name”?
      【解决方案7】:

      您可以在序列化程序的 UniqueValidator 中使用 lookup='iexact',如下所示:

      class StateSerializer(serializers.ModelSerializer): 
          name = serializers.CharField(validators=[
          UniqueValidator(
              queryset=models.State.objects.all(),lookup='iexact'
          )]
      

      django 版本:1.11.6

      【讨论】:

        【解决方案8】:

        如果您不想使用特定于 postgres 的解决方案,您可以使用 upper() 在字段上创建唯一索引以在数据库级别强制唯一性,然后创建覆盖 @987654324 的自定义 Field mixin @ 将区分大小写的查找转换为其不区分大小写的版本。 mixin 看起来像这样:

        class CaseInsensitiveFieldMixin:
            """
            Field mixin that uses case-insensitive lookup alternatives if they exist.
            """
        
            LOOKUP_CONVERSIONS = {
                'exact': 'iexact',
                'contains': 'icontains',
                'startswith': 'istartswith',
                'endswith': 'iendswith',
                'regex': 'iregex',
            }
        
            def get_lookup(self, lookup_name):
                converted = self.LOOKUP_CONVERSIONS.get(lookup_name, lookup_name)
                return super().get_lookup(converted)
        

        你可以这样使用它:

        from django.db import models
        
        
        class CICharField(CaseInsensitiveFieldMixin, models.CharField):
            pass
        
        
        class CIEmailField(CaseInsensitiveFieldMixin, models.EmailField):
            pass
        
        
        class TestModel(models.Model):
            name = CICharField(unique=True, max_length=20)
            email = CIEmailField(unique=True)
        

        您可以阅读有关此方法的更多信息here

        【讨论】:

          【解决方案9】:

          您可以通过覆盖模型的保存方法来做到这一点 - 请参阅docs。你基本上会做这样的事情:

          class State(models.Model):
              name = models.CharField(max_length=50, unique=True)
          
              def save(self, force_insert=False, force_update=False):
                  if State.objects.get(name__iexact = self.name):
                      return
                  else:
                      super(State, self).save(force_insert, force_update)
          

          此外,我可能对此有误,但即将推出的模型验证 SoC 分支将使我们能够更轻松地做到这一点。

          【讨论】:

          • 这与我已经在做的基本相同。事实上,我现在这样做的方式比在保存中处理它要好。
          • 再读一遍,你是对的。 AFAIK,使用表单的验证将是最好的方法(截至目前)-除非数据不是通过表单插入的:)。
          • 完全有可能不通过表单插入数据!特别是如果您使用的是第三方应用程序。
          【解决方案10】:

          来自 suhail 的解决方案为我工作,无需启用 citext,非常简单的解决方案只有一个干净的功能,而不是大写我使用 upper()。 Mayuresh 的解决方案也有效,但将字段从 CharField 更改为 TextField

          class State(models.Model):
          
              name = models.CharField(max_length=50, unique=True)
          
              def clean(self):
                  self.name = self.name.upper()
          

          【讨论】:

            猜你喜欢
            • 2011-12-08
            • 2017-04-15
            • 2016-02-04
            • 2020-08-29
            • 2012-12-09
            • 1970-01-01
            • 2021-08-02
            • 2015-10-21
            相关资源
            最近更新 更多