如何编写自定义查找

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

Field.register_lookup(NotEqual)

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

from django.db.models import Field


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

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

仔细看一下实现,第一个必需的属性是 lookup_name。这使 ORM 能够理解如何解释 name__ne 并使用 NotEqual 生成 SQL。按照惯例,这些名称始终是仅包含字母的小写字符串,但唯一严格的要求是它不能包含字符串 __

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

一个 Lookup 对两个值起作用,lhsrhs,分别代表左侧和右侧。左侧通常是字段引用,但它可以是任何实现了 查询表达式 API 的东西。右侧是用户提供的 value。在示例 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

在支持 distinct on 字段的数据库(如 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 并且 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()。相反,它跳过了 AbsoluteValuelhs 的转换,并使用原始的 lhs。也就是说,我们想要得到 "experiments"."change",而不是 ABS("experiments"."change")。直接引用 self.lhs.lhs 是安全的,因为 AbsoluteValueLessThan 只能从 AbsoluteValue 查找中访问,也就是说 lhs 始终是 AbsoluteValue 的实例。

还要注意,由于查询中两边都被多次使用,因此参数需要包含 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)

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

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

为现有查找编写替代实现

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

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

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.removeprefix("x"))
            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')
返回顶部