postgresql Django?中不区分大小写的唯一模型字段

slhcrj9b  于 2022-12-18  发布在  PostgreSQL
关注(0)|答案(9)|浏览(198)

我基本上有一个用户名是唯一的(大小写不敏感),但大小写问题时,显示由用户提供。
本人有以下要求:

  • 字段与CharField兼容
  • 字段唯一,但不区分大小写
  • 字段需要可搜索,忽略大小写(避免使用iexact,容易忘记)
  • 字段保存时 shell 完好无损
  • 优选地在数据库级上实施
  • 优选地避免存储额外的字段

这在 Django 有可能吗?
我想到的唯一解决方案是“以某种方式”覆盖模型管理器,使用一个额外的字段,或者在搜索中总是使用“iexact”。
我使用的是Django 1.3和PostgreSQL 8.4.2。

iibxawm4

iibxawm41#

从Django 1.11开始,你可以使用CITextField,一个Postgress特有的字段,用于citext类型支持的不区分大小写的文本。

from django.db import models
from django.contrib.postgres.fields import CITextField

class Something(models.Model):
    foo = CITextField()

Django还提供了CIEmailFieldCICharField,它们是EmailFieldCharField的不区分大小写的版本。

toe95027

toe950272#

将原始的大小写混合字符串存储在纯文本列中。使用数据类型**text**或不带长度修饰符的varchar,而不是varchar(n)。它们本质上是相同的,但使用varchar(n)时,必须设置任意长度限制,如果以后要更改,这可能会很麻烦。请阅读有关in the manual或此related answer by Peter Eisentraut @serverfault.SE的更多信息。
lower(string)上创建一个functional unique index,这是这里的要点:

CREATE UNIQUE INDEX my_idx ON mytbl(lower(name));

如果你试图INSERT一个混合大小写的名字,而这个名字已经是小写的了,你会得到一个唯一的键冲突错误。
对于快速相等搜索,请使用如下查询:

SELECT * FROM mytbl WHERE lower(name) = 'foo' --'foo' is lower case, of course.

使用与索引中相同的表达式(以便查询计划器识别兼容性),这样会非常快。
顺便说一句:你可能想升级到PostgreSQL的最新版本。已经有很多important fixes since 8.4.2了。更多关于official Postgres versioning site的信息。

lhcgjxsq

lhcgjxsq3#

覆盖模型管理器,你有两个选择,第一个是创建一个新的查找方法:

class MyModelManager(models.Manager):
   def get_by_username(self, username):
       return self.get(username__iexact=username)

class MyModel(models.Model):
   ...
   objects = MyModelManager()

然后,您使用get_by_username('blah')而不是get(username='blah'),并且不必担心忘记iexact,当然,这要求您记住使用get_by_username
第二种选择更加复杂,我甚至不愿提出,但为了完整起见,我将:覆盖filterget,这样如果您在按用户名查询时忘记了iexact,它将为您添加它。

class MyModelManager(models.Manager):
    def filter(self, **kwargs):
        if 'username' in kwargs:
            kwargs['username__iexact'] = kwargs['username']
            del kwargs['username']
        return super(MyModelManager, self).filter(**kwargs)

    def get(self, **kwargs):
        if 'username' in kwargs:
            kwargs['username__iexact'] = kwargs['username']
            del kwargs['username']
        return super(MyModelManager, self).get(**kwargs)

class MyModel(models.Model):
   ...
   objects = MyModelManager()
z8dt9xmd

z8dt9xmd4#

截至2021年12月,在Django 4.0 UniqueConstraint表达式的帮助下,您可以像这样向模型中添加 meta类:

class Meta:
    constraints = [
        models.UniqueConstraint(
            Lower('<field name>'),
            name='<constraint name>'
        ),
    ]

我不是一个专业的Django开发人员,我不知道这个解决方案的技术考虑,如性能问题。希望其他人对此发表评论。

yhxst69z

yhxst69z5#

由于用户名总是小写的,建议在Django中使用自定义的小写model字段。为了方便访问和代码整洁,在app文件夹中创建一个新文件fields.py

from django.db import models
from django.utils.six import with_metaclass

# Custom lowecase CharField

class LowerCharField(with_metaclass(models.SubfieldBase, models.CharField)):
    def __init__(self, *args, **kwargs):
        self.is_lowercase = kwargs.pop('lowercase', False)
        super(LowerCharField, self).__init__(*args, **kwargs)

    def get_prep_value(self, value):
        value = super(LowerCharField, self).get_prep_value(value)
        if self.is_lowercase:
            return value.lower()
        return value

用法(以models.py为单位)

from django.db import models
from your_app_name.fields import LowerCharField

class TheUser(models.Model):
    username = LowerCharField(max_length=128, lowercase=True, null=False, unique=True)

结束注解:您可以使用此方法在数据库中存储小写值,而不必担心__iexact

hpxqektj

hpxqektj6#

你可以使用citextpostgres类型来代替,而不用再为任何类型的不精确而烦恼。只需在模型中注明底层字段是不区分大小写的。解决起来容易得多。

mmvthczy

mmvthczy7#

您可以在序列化器的UniqueValidator中使用lookup='iexact',如下所示:Django中唯一的模型字段和大小写敏感性(postgres)

zour9fqk

zour9fqk8#

我喜欢Chris Pratt的答案,但它对我不起作用,因为models.Manager-类没有get(...)filter(...)方法,我不得不通过自定义QuerySet采取额外的步骤:

from django.contrib.auth.base_user import BaseUserManager
from django.db.models import QuerySet

class CustomUserManager(BaseUserManager):

    # Use the custom QuerySet where get and filter will change 'email'
    def get_queryset(self):
        return UserQuerySet(self.model, using=self._db)

    def create_user(self, email, password, **extra_fields):
        ...

    def create_superuser(self, email, password, **extra_fields):
        ...

class UserQuerySet(QuerySet):

    def filter(self, *args, **kwargs):
        if 'email' in kwargs:
            # Probably also have to replace...
            #   email_contains -> email_icontains,
            #   email_exact -> email_iexact,
            #   etc.
            kwargs['email__iexact'] = kwargs['email']
            del kwargs['email']
        return super().filter(*args, **kwargs)

    def get(self, *args, **kwargs):
        if 'email' in kwargs:
            kwargs['email__iexact'] = kwargs['email']
            del kwargs['email']
        return super().get(*args, **kwargs)

这在一个非常简单的情况下对我起作用,但到目前为止效果相当不错。

w9apscun

w9apscun9#

您还可以覆盖get_prep_value()并通过继承重用它。

class LowerCaseField:
    def get_prep_value(self, value):
        value = super().get_prep_value(value)
        if value:
            value = value.strip().lower()
        return value

class LowerSlugField(LowerCaseField, models.SlugField):
    pass

class LowerEmailField(LowerCaseField, models.EmailField):
    pass

class MyModel(models.Model):
    email = LowerEmailField(max_length=255, unique=True)

这样,如果您想在另一个模型中重用这个字段,您可以使用相同的一致策略。
来自Django文档:

一个月一个月一个月二个月
value是模型属性的当前值,并且该方法应该以已准备好用作查询中的参数的格式返回数据。

有关用法,请参见将Python对象转换为查询值。

相关问题