如何编写自定义查找

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__ne 来表示任何字段 foo。您需要确保此注册发生在您尝试使用它创建任何查询集之前。您可以将实现放在 models.py 文件中,或者在 AppConfigready() 方法中注册查找。

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

然后我们需要定义 as_sql 方法。这需要一个 SQLCompiler 对象(称为 compiler)和活动的数据库连接。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

在支持 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=-27change__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 始终是 AbsoluteValue 的实例。

还要注意,由于查询中多次使用了双方,因此 params 需要包含多次 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 来执行相同的操作。在此示例中,我们将为 MySQL 重写 NotEqual 运算符的自定义实现。我们将使用 != 运算符而不是 <>。(请注意,实际上几乎所有数据库都支持两者,包括 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.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')

返回顶部