自定义查找 — Django 文档

来自菜鸟教程
Django/docs/3.0.x/howto/custom-lookups
跳转至:导航、​搜索

自定义查找

Django 提供了各种各样的 内置查找 用于过滤(例如,exacticontains)。 本文档解释了如何编写自定义查找以及如何更改现有查找的工作方式。 有关查找的 API 参考,请参阅 Lookup API 参考

查找示例

让我们从一个小的自定义查找开始。 我们将编写一个自定义查找 ne,其作用与 exact 相反。 Author.objects.filter(name__ne='Jack') 将转换为 SQL:

"author"."name" <> 'Jack'

这个 SQL 是后端独立的,所以我们不需要担心不同的数据库。

完成这项工作有两个步骤。 首先我们需要实现查找,然后我们需要告诉 Django:

from django.db.models import Lookup

class NotEqual(Lookup):
    lookup_name = 'ne'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s <> %s' % (lhs, rhs), params

要注册 NotEqual 查找,我们需要在我们希望查找可用的字段类上调用 register_lookup。 在这种情况下,查找对所有 Field 子类都有意义,所以我们直接用 Field 注册它:

from django.db.models.fields import Field
Field.register_lookup(NotEqual)

查找注册也可以使用装饰器模式完成:

from django.db.models.fields import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...

我们现在可以将 foo__ne 用于任何字段 foo。 在尝试使用它创建任何查询集之前,您需要确保此注册发生。 您可以将实现放在 models.py 文件中,或在 AppConfigready() 方法中注册查找。

仔细查看实现,第一个必需的属性是 lookup_name。 这允许 ORM 了解如何解释 name__ne 并使用 NotEqual 生成 SQL。 按照惯例,这些名称总是只包含字母的小写字符串,但唯一的硬性要求是它不能包含字符串 __

然后我们需要定义 as_sql 方法。 这需要一个名为 compilerSQLCompiler 对象和活动的数据库连接。 SQLCompiler 对象没有记录,但我们唯一需要知道的是它们有一个 compile() 方法,该方法返回一个包含 SQL 字符串的元组,以及要插入其中的参数细绳。 在大多数情况下,您不需要直接使用它,可以将其传递给 process_lhs()process_rhs()

Lookup 对两个值起作用,lhsrhs,分别代表左侧和右侧。 左侧通常是字段引用,但它可以是实现 查询表达式 API 的任何内容。 右边是用户给出的值。 在示例Author.objects.filter(name__ne='Jack')中,左侧是对Author型号的name字段的引用,而'Jack'是右侧.

我们调用 process_lhsprocess_rhs 将它们转换为我们使用前面描述的 compiler 对象的 SQL 所需的值。 这些方法返回包含一些 SQL 和要插入到该 SQL 中的参数的元组,就像我们需要从 as_sql 方法返回一样。 在上面的例子中,process_lhs 返回 ('"author"."name"', [])process_rhs 返回 ('"%s"', ['Jack'])。 在这个例子中,左侧没有参数,但这取决于我们拥有的对象,所以我们仍然需要将它们包含在我们返回的参数中。

最后,我们将这些部分组合成一个带有 <> 的 SQL 表达式,并提供查询的所有参数。 然后我们返回一个包含生成的 SQL 字符串和参数的元组。


变压器示例

上面的自定义查找很棒,但在某些情况下,您可能希望能够将查找链接在一起。 例如,假设我们正在构建一个要使用 abs() 运算符的应用程序。 我们有一个 Experiment 模型,它记录开始值、结束值和变化(开始 - 结束)。 我们想找出变化等于一定量 (Experiment.objects.filter(change__abs=27)) 或不超过一定量 (Experiment.objects.filter(change__abs__lt=27)) 的所有实验。

笔记

这个例子有点人为,但它很好地展示了以数据库后端独立方式可能的功能范围,并且没有复制 Django 中已有的功能。


我们将首先编写一个 AbsoluteValue 转换器。 这将使用 SQL 函数 ABS() 在比较之前转换值:

from django.db.models import Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

接下来,让我们将其注册为 IntegerField

from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)

我们现在可以运行之前的查询。 Experiment.objects.filter(change__abs=27) 将生成以下 SQL:

SELECT ... WHERE ABS("experiments"."change") = 27

通过使用 Transform 而不是 Lookup,这意味着我们可以在之后链接进一步的查找。 所以 Experiment.objects.filter(change__abs__lt=27) 会生成如下 SQL:

SELECT ... WHERE ABS("experiments"."change") < 27

请注意,如果没有指定其他查找,Django 会将 change__abs=27 解释为 change__abs__exact=27

这也允许在 ORDER BYDISTINCT ON 子句中使用结果。 例如 Experiment.objects.order_by('change__abs') 生成:

SELECT ... ORDER BY ABS("experiments"."change") ASC

在支持不同字段的数据库(如 PostgreSQL)上,Experiment.objects.distinct('change__abs') 生成:

SELECT ... DISTINCT ON ABS("experiments"."change")

在应用 Transform 后查找允许的查找时,Django 使用 output_field 属性。 我们不需要在这里指定它,因为它没有改变,但假设我们将 AbsoluteValue 应用于表示更复杂类型的某个字段(例如相对于原点的点,或复数) 那么我们可能想要指定转换返回 FloatField 类型以供进一步查找。 这可以通过向转换添加 output_field 属性来完成:

from django.db.models import FloatField, Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

    @property
    def output_field(self):
        return FloatField()

这确保了 abs__lte 等进一步查找的行为与 FloatField 的行为相同。


编写高效的 abs__lt 查找

当使用上面写的 abs 查找时,生成的 SQL 在某些情况下不会有效地使用索引。 特别是,当我们使用 change__abs__lt=27 时,这相当于 change__gt=-27 AND change__lt=27。 (对于 lte 的情况,我们可以使用 SQL BETWEEN)。

所以我们希望 Experiment.objects.filter(change__abs__lt=27) 生成以下 SQL:

SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

实现是:

from django.db.models import Lookup

class AbsoluteValueLessThan(Lookup):
    lookup_name = 'lt'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = compiler.compile(self.lhs.lhs)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params + lhs_params + rhs_params
        return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params

AbsoluteValue.register_lookup(AbsoluteValueLessThan)

有几件值得注意的事情正在发生。 首先,AbsoluteValueLessThan 没有调用 process_lhs()。 相反,它跳过了由 AbsoluteValue 完成的 lhs 的转换,并使用原始的 lhs。 也就是说,我们想要得到 "experiments"."change" 而不是 ABS("experiments"."change")。 直接引用 self.lhs.lhs 是安全的,因为 AbsoluteValueLessThan 只能从 AbsoluteValue 查找中访问,即 lhs 始终是 [ X155X]。

另请注意,由于在查询中多次使用双方,因此参数需要多次包含 lhs_paramsrhs_params

最后的查询直接在数据库中进行反演(27-27)。 这样做的原因是,如果 self.rhs 不是一个普通的整数值(例如 F() 引用),我们就不能在 Python 中进行转换。

笔记

事实上,大多数使用 __abs 的查找都可以作为这样的范围查询来实现,并且在大多数数据库后端,这样做可能更明智,因为您可以使用索引。 但是,对于 PostgreSQL,您可能希望在 abs(change) 上添加索引,这将使这些查询非常有效。


双边变压器示例

我们之前讨论的 AbsoluteValue 示例是应用于查找左侧的转换。 在某些情况下,您可能希望将转换应用于左侧和右侧。 例如,如果您想根据左侧和右侧的相等性对某个 SQL 函数不敏感地过滤查询集。

让我们在这里检查不区分大小写的转换。 这种转换在实践中并不是很有用,因为 Django 已经附带了一堆内置的不区分大小写的查找,但它将以与数据库无关的方式很好地演示双边转换。

我们定义了一个 UpperCase 转换器,它使用 SQL 函数 UPPER() 在比较之前转换值。 我们定义 bilateral = True 来表明这个变换应该同时应用于 lhsrhs

from django.db.models import Transform

class UpperCase(Transform):
    lookup_name = 'upper'
    function = 'UPPER'
    bilateral = True

接下来,让我们注册它:

from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)

现在,查询集 Author.objects.filter(name__upper="doe") 将生成一个不区分大小写的查询,如下所示:

SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

为现有查找编写替代实现

有时,不同的数据库供应商对同一操作需要不同的 SQL。 对于此示例,我们将为 NotEqual 运算符重写 MySQL 的自定义实现。 我们将使用 != 运算符而不是 <>。 (注意,实际上几乎所有的数据库都支持两者,包括 Django 支持的所有官方数据库)。

我们可以通过使用 as_mysql 方法创建 NotEqual 的子类来更改特定后端的行为:

class MySQLNotEqual(NotEqual):
    def as_mysql(self, compiler, connection, **extra_context):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s != %s' % (lhs, rhs), params

Field.register_lookup(MySQLNotEqual)

然后我们可以用 Field 注册它。 它取代了原来的 NotEqual 类,因为它具有相同的 lookup_name

在编译查询时,Django 首先查找 as_%s % connection.vendor 方法,然后回退到 as_sql。 内置后端的供应商名称为 sqlitepostgresqloraclemysql


Django 如何确定使用的查找和转换

在某些情况下,您可能希望根据传入的名称动态更改返回的 TransformLookup,而不是修复它。 例如,您可以有一个存储坐标或任意维度的字段,并希望允许像 .filter(coords__x7=4) 这样的语法返回第 7 个坐标值为 4 的对象。 为此,您可以使用以下内容覆盖 get_lookup

class CoordinatesField(Field):
    def get_lookup(self, lookup_name):
        if lookup_name.startswith('x'):
            try:
                dimension = int(lookup_name[1:])
            except ValueError:
                pass
            else:
                return get_coordinate_lookup(dimension)
        return super().get_lookup(lookup_name)

然后,您将适当地定义 get_coordinate_lookup 以返回一个 Lookup 子类,该子类处理 dimension 的相关值。

有一个名称类似的方法,称为 get_transform()get_lookup() 应该总是返回一个 Lookup 子类,而 get_transform() 一个 Transform 子类。 重要的是要记住 Transform 对象可以进一步过滤,而 Lookup 对象不能。

过滤时,如果只剩下一个查找名称需要解析,我们将查找一个Lookup。 如果有多个名称,它将查找 Transform。 在只有一个名称且未找到 Lookup 的情况下,我们查找 Transform,然后在该 Transform 上查找 exact。 所有调用序列总是以 Lookup 结尾。 澄清:

  • .filter(myfield__mylookup) 将调用 myfield.get_lookup('mylookup')
  • .filter(myfield__mytransform__mylookup) 将调用 myfield.get_transform('mytransform'),然后是 mytransform.get_lookup('mylookup')
  • .filter(myfield__mytransform)会先调用myfield.get_lookup('mytransform'),会失败,所以回退到调用myfield.get_transform('mytransform'),然后再调用mytransform.get_lookup('exact')