Django提供多种 built-in lookups 用于过滤(例如, 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
子类,因此我们将其注册到 Field
直接:
from django.db.models import Field
Field.register_lookup(NotEqual)
也可以使用decorator模式完成查找注册:
from django.db.models import Field
@Field.register_lookup
class NotEqualLookup(Lookup):
...
我们现在可以使用 foo__ne
对于任何领域 foo
. 在尝试使用它创建任何查询集之前,您需要确保此注册发生。您可以将实现放在 models.py
文件或在中注册查找 ready()
AN方法 AppConfig
.
仔细观察实现,第一个必需的属性是 lookup_name
. 这使ORM能够理解如何解释 name__ne
使用 NotEqual
生成SQL。按照惯例,这些名称总是只包含字母的小写字符串,但唯一困难的要求是它不能包含字符串。 __
.
然后我们需要定义 as_sql
方法。这需要一个 SQLCompiler
对象,称为 compiler
以及活动的数据库连接。 SQLCompiler
对象没有文档记录,但我们唯一需要知道的是它们有一个 compile()
方法,返回包含SQL字符串的元组以及要插入该字符串的参数。在大多数情况下,您不需要直接使用它,可以将它传递给 process_lhs()
和 process_rhs()
.
A Lookup
针对两个值工作, lhs
和 rhs
,代表左侧和右侧。左侧通常是字段引用,但它可以是实现 query expression API . 右边是用户给定的值。在这个例子中 Author.objects.filter(name__ne='Jack')
,左侧是对 name
领域 Author
模型,以及 'Jack'
是右手边。
我们调用 process_lhs
和 process_rhs
要将它们转换为SQL所需的值,请使用 compiler
之前描述的对象。这些方法返回包含一些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
AND 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()
. 相反,它跳过了 lhs
通过做 AbsoluteValue
使用原版 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
我们前面讨论的示例是应用于查找左侧的转换。在某些情况下,您可能希望将转换同时应用于左侧和右侧。例如,如果要根据左右两侧的相等性筛选查询集,则对某些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。对于这个例子,我们将为notequal操作符重写mysql的自定义实现。而不是 <>
我们将使用 !=
操作员。(请注意,实际上几乎所有数据库都支持这两种功能,包括Django支持的所有官方数据库)。
我们可以通过创建 NotEqual
用一个 as_mysql
方法:
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
.
在某些情况下,您可能希望动态更改 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
然后 exact
查找 Transform
. 所有调用序列始终以 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')
.
12月 18, 2023