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;转载时应保留。











暂无评论内容