如何编写自定义查找¶
Django 为过滤提供了多种 内置查找(例如 exact 和 icontains)。本文档介绍了如何编写自定义查找以及如何修改现有查找的工作方式。有关查找的 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 使用 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.objects.filter(name__ne='Jack') 中,左侧是对 Author 模型 name 字段的引用,而 'Jack' 是右侧。
我们调用 process_lhs 和 process_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 BY 和 DISTINCT ON 子句。例如 Experiment.objects.order_by('change__abs') 会生成。
SELECT ... ORDER BY ABS("experiments"."change") ASC
在支持按字段区分的数据库(例如 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 查找中访问,即 lhs 始终是 AbsoluteValue 的一个实例。
还要注意,由于两侧在查询中都被多次使用,参数需要多次包含 lhs_params 和 rhs_params。
最终查询直接在数据库中执行了反转(27 到 -27)。这样做的原因是,如果 self.rhs 是除了纯整数值以外的其他东西(例如 F() 引用),我们无法在 Python 中进行转换。
注意
事实上,大多数带有 __abs 的查找都可以像这样实现为范围查询,在大多数数据库后端上,这样做可能更合理,因为你可以利用索引。然而,对于 PostgreSQL,你可能需要添加一个对 abs(change) 的索引,这将使这些查询非常高效。
双边转换器示例¶
我们之前讨论的 AbsoluteValue 示例是一种应用于查找左侧的转换。有些情况下,你可能希望将转换应用于左侧和右侧。例如,如果你想基于左侧和右侧的相等性来过滤 QuerySet,而不受某种 SQL 函数的影响。
让我们在这里检查一下不区分大小写的转换。这种转换在实践中并不是很有用,因为 Django 已经带有一堆内置的不区分大小写的查找,但它将是一个以数据库无关方式进行双边转换的好例子。
我们定义了一个 UpperCase 转换器,它在比较之前使用 SQL 函数 UPPER() 来转换值。我们定义 bilateral = True 来指示此转换应应用于 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)
现在,QuerySet 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) 的语法,以返回第 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 以返回处理 dimension 相关值的 Lookup 子类。
还有一个名称类似的方法叫 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')。