如何编写自定义查找¶
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__ne
来表示任何字段 foo
。您需要确保此注册发生在您尝试使用它创建任何查询集之前。您可以将实现放在 models.py
文件中,或者在 AppConfig
的 ready()
方法中注册查找。
仔细查看实现,第一个必需属性是 lookup_name
。这允许 ORM 理解如何解释 name__ne
并使用 NotEqual
生成 SQL。按照惯例,这些名称始终是仅包含字母的小写字符串,但唯一严格的要求是它不能包含字符串 __
。
然后我们需要定义 as_sql
方法。这需要一个 SQLCompiler
对象(称为 compiler
)和活动的数据库连接。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
在支持 distinct on 字段的数据库(例如 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
的实例。
还要注意,由于查询中多次使用了双方,因此 params 需要包含多次 lhs_params
和 rhs_params
。
最终查询直接在数据库中执行反转(27
到 -27
)。这样做的原因是,如果 self.rhs
不是简单的整数值(例如 F()
引用),我们无法在 Python 中进行转换。
注意
事实上,大多数使用 __abs
的查找都可以这样实现为范围查询,并且在大多数数据库后端上,这样做更有意义,因为您可以利用索引。但是对于 PostgreSQL,您可能需要在 abs(change)
上添加索引,这将使这些查询非常高效。
双向转换器示例¶
我们之前讨论的 AbsoluteValue
示例是一种应用于查找左侧的转换。在某些情况下,您可能希望将转换应用于左侧和右侧。例如,如果您想根据左侧和右侧在某种 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)
现在,查询集 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
来返回一个 Lookup
子类,该子类处理 dimension
的相关值。
有一个类似名称的方法称为 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')
。