Django 6.1:如何编写自定义查询器

Django 提供了 exact、icontains 等内置查询器。本文介绍自定义查询器以及替换现有查询器的实现;相关接口见查询 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

在需要支持它的字段类上调用 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。注册必须发生在创建使用它的 QuerySet 之前,例如在 models.py 中,或在 AppConfig.ready() 中进行。

lookup_name 告诉 ORM 如何解释 name__ne 并用 NotEqual 生成 SQL。按惯例,名字只包含小写字母,绝不能包含双下划线 __。

接着定义 as_sql()。它接收名为 compiler 的 SQLCompiler 对象和当前数据库连接。虽然 SQLCompiler 没有公开文档,这里只需知道其 compile() 返回二元组:SQL 字符串和待绑定参数。多数时候无需直接调用,可以把编译器传给 process_lhs()、process_rhs()。

Lookup 有左右两侧 lhs 与 rhs。左侧是字段引用,也可以是任何实现查询表达式 API 的对象;右侧是用户提供的值。在上述查询中,左侧是 Author.name,右侧是字符串 "Jack"。

两个 process_*() 方法把两侧转换为 SQL 和参数,返回形式与 as_sql() 一样。此例的左侧结果为 ('"author"."name"', []),右侧为 ('%s', ['Jack'])。虽然这里左侧没有参数,其他表达式可能有,所以仍须合并两边参数。最后拼接 <> 条件,返回 SQL 与完整参数列表。

一个转换器示例

有时需要把多个操作串联起来。假设 Experiment 保存起始值、结束值和差值 change(起始值减结束值)。我们希望查询绝对值等于 27 的实验,以及绝对值小于 27 的实验:change__abs=27、change__abs__lt=27。

这个例子有些刻意,但能展示如何以不依赖数据库后端的方式扩展 ORM,而不重复 Django 的现成功能。先用 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) 会生成:

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

使用 Transform 而非 Lookup,意味着后面仍能串联查询器。Experiment.objects.filter(change__abs__lt=27) 会生成:

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

若没有显式指定最后的查询器,change__abs=27 被解释为 change__abs__exact=27。转换结果也可以用于排序和 DISTINCT ON:

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

这是 Experiment.objects.order_by("change__abs") 的效果。对支持按字段去重的后端,例如 PostgreSQL,Experiment.objects.distinct("change__abs") 对应:

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

Django 根据转换器的 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 查询

前面的 SQL 在某些后端不能高效使用索引。change__abs__lt=27 等价于同时满足 change__gt=-27 和 change__lt=27;对于 lte 可以使用 SQL BETWEEN。我们希望生成:

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)

这里没有调用 process_lhs(),而是绕过 AbsoluteValue 转换,直接编译原始左侧。我们要的是 "experiments"."change",而非 ABS("experiments"."change")。直接访问 self.lhs.lhs 是安全的,因为这个查询器只注册在 AbsoluteValue 上,左侧总是它的实例。

左右两侧在 SQL 中各出现两次,参数也必须重复两次。负号由数据库处理,而不是先在 Python 中把 27 改为 −27,因为右侧也可能是 F() 引用等表达式。

大多数针对绝对值的比较都可以转换成类似范围查询,在多数后端有利于利用索引;PostgreSQL 也可以为 abs(change) 建立表达式索引。

双向转换器

前面的转换只处理左侧。有时需要同时转换两侧,例如在比较前对两侧应用同一个 SQL 函数。大小写转换并非实用的新功能——Django 已有相应查询器——但适合演示双向转换。

from django.db.models import Transform


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

bilateral = True 表示同时处理 lhs 和 rhs。注册:

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 的 != 替换 <>;实际上,Django 支持的正式数据库基本都支持两者,这只是演示后端定制。

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)

因为继承了相同的 lookup_name,注册会替换先前的 NotEqual。编译时 Django 先寻找 as_{connection.vendor},没有才调用 as_sql()。内置后端的 vendor 名为 sqlite、postgresql、oracle、mysql。

Django 如何选择查询器和转换器

也可以根据名字动态生成类。例如 CoordinatesField 保存坐标,.filter(coords__x7=4) 要查询第七个坐标等于 4:

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(dimension),返回处理相应维度的 Lookup 子类。类似的 get_transform() 必须返回 Transform 子类。转换器后面还能继续筛选,查询器则是操作链的终点。

只有一个名字时,Django 先查找查询器;有多个名字时,先查找转换器。如果唯一名字不是查询器,却能找到转换器,就在后面补 exact。所有操作链都以查询器结束:

  • .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")。

原文:如何编写自定义的查询器(Django 6.1)。© Django Software Foundation 及独立贡献者;中文整理与补译。Django 文档随项目采用 BSD 3-Clause 许可证。版权声明、许可条件及免责声明见随附 licenses/Django-BSD-3-Clause.txt;转载时应保留。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容