如何编写自定义的查询器

Django 提供了各种各样的 内置查询器 (例如, exacticontains )。本文档解释了如何编写自定义查询器以及如何更改已有查询器的工作方式。 有关 lookup 的 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 的任意字段。你需要确保注册行为发生在创建任意的 queryset 之前。你可以在 models.py 文件内设置它,或者在 AppConfigready() 方法中注册它。

仔细观察实现过程,第一个要求的属性是 lookup_name。它能让 ORM 理解如何编译 name_ne 并使用 NotEqual 来建立 SQL 语句。按照惯例,这些名字应该总是仅包含小写字母的字符串,但是绝对不能包含双下划线 __

之后我们需要定义 as_sql 方法。此方法需要一个 SQLCompiler 对象, 被叫做 compiler,和一个有效的数据库连接。SQLCompller 对象没有文档,我们只需要知道它有一个 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_lhs 返回 ('"%s"', ['Jack'])。在这个例子里面没有手边的参数,这需要看情况而定,所以我们仍需要在返回结果时包括这些参数。

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

一个转换器示例。

上面的自定义查询器没问题,但在某些情况下,您可能希望能够将一些查询器链接在一起。 例如,假设我们正在构建一个使用 abs() 运算的应用程序。我们有一个 Experiment 模型,它记录起始值,结束值和差值(起始 - 结束)。 我们想找到 change 属性等于某个数值的所有 Experiment 对象(Experiment.objects.filter(change__abs = 27)),change属性没有超过一定数量的 Experiment 对象(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"

下一步,让我们为其注册 IntrgerField:

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 的数据库中(比如 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 lookup 访问,即 lhs 总是 AbsoluteValue 的实例。

另请注意,由于在查询中多次使用两边,所以需要多次包含 lhs_paramsrhs_params 参数。

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

备注

实际上,大多数的利用 __abs 的查找都可以被转换为类似此的范围查找,且在大多数数据库后端来说,这样做能更好的利用索引。不过,对于 PostgreSQL,你可能会为 abs(change) 添加索引,这会使查找更加高效。

一个双向转换器示例

前文所述的 AbsoluteValue 例子实现了左侧查询。在某些场景下,你期望转换器同时作用于左侧和右侧。例如,如果你想在左侧基于等式进行过滤,而右侧对于某些 SQL 函数不敏感。

让我们在此测试这个大小写转换器。实际上这个转换器不是非常实用,因为 Django 已经内置了一系列大小写敏感相关的查询器,但它将是双向转换的一个很好的演示,且通过与数据库无关的方式来演示。

我们定义了一个 UpperCase 转换器,使用了 SQL 函数 UPPER(),在比较之前转换值。我们定义了:attr:bilateral = True <django.db.models.Transform.bilateral> 指明此转换应同时用于 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) 返回第七个值为 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,并附加 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')