How to write custom lookups
Django 提供了各种各样的 内置查询器 (例如, exact
和 icontains
)。本文档解释了如何编写自定义查询器以及如何更改已有查询器的工作方式。 有关 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
文件内设置它,或者在 AppConfig
的 ready()
方法中注册它。
仔细观察实现过程,第一个要求的属性是 lookup_name
。它能让 ORM 理解如何编译 name_ne
并使用 NotEqual
来建立 SQL 语句。按照惯例,这些名字应该总是仅包含小写字母的字符串,但是绝对不能包含双下划线 __
。
之后我们需要定义 as_sql
方法。此方法需要一个 SQLCompiler
对象, 被叫做 compiler
,和一个有效的数据库连接。SQLCompller
对象没有文档,我们只需要知道它有一个 compile()
方法可以返回一个包括 SQL 字符串的元组,和插入这个字符串的参数。大部分情况下,你不需要直接使用这个对象你可以把它传送给 process_lhs()
和 process_rhs()
。
Lookup
工作依靠两个值,lhs
和 rhs
,代表左右两边,左边是一个字段参考,它可以是任何实现了 查询表达式 API 的实例。右边是一个用户给定的数值。举个例子: Author.objects.filter(name__ne='Jack')
,左边是 Author
模型的 name
字段,右边是 'Jack'
。
我们利用 process_lhs
和 process_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
By using Transform
instead of Lookup
it means we are able to chain further lookups afterward. So Experiment.objects.filter(change__abs__lt=27)
will generate the following SQL:
SELECT ... WHERE ABS("experiments"."change") < 27
请注意,如果没有指定其他查找定义,Django则会将 change__abs=27
解析为 change__abs__exact=27
。
这也允许把结果用在 ORDER BY
和 DISTINCT 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=-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()
。 相反,它会跳过由 AbsoluteValue
完成的 lhs
的转换,并使用原始的 lhs
。也就是说,我们希望得到 "experiments"."change"
,而不是 ABS("experiments"."change")
。直接引用 self.lhs.lhs
是安全的,因为 AbsoluteValueLessThan
只能从 AbsoluteValue
lookup 访问,即 lhs
总是 AbsoluteValue
的实例。
另请注意,由于在查询中多次使用两边,所以需要多次包含 lhs_params
和 rhs_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> 指明此转换应同时用于 lhs
和 rhs
:
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
。内置后端的提供商名为 sqlite
,postgresql
,oracle
和 mysql
。
Django 是如何取舍查询器和转换器的
某些场景下,你可能期望基于传入的名字动态地返回 Transform
或 Lookup
,而不是指定。例如,有一个字段,存储了一些坐标或尺寸,期望使用以下语法 .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[1:])
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')
。