自定义查找¶
Django提供了多种用于过滤的 内置查找 (例如,exact
和 icontains
)。本文档介绍了如何编写自定义查找以及如何更改现有查找的工作。有关查找的API参考,请参见 查找API参考。
一个简单的查找示例¶
让我们从一个简单的自定义查找开始。我们将编写一个与 exact
相反的自定义查找 ne
。 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.fields import Field
Field.register_lookup(NotEqual)
查找注册也可以使用装饰器模式完成:
from django.db.models.fields 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中的参数,就像我们需要从我们的 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
。
当查找在应用 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
是安全的,因为只能从 AbsoluteValue
查找访问 AbsoluteValueLessThan
,也就是说 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支持的所有官方数据库)。
我们可以通过使用 as_mysql
方法创建 NotEqual
的子类来更改特定后端的行为:
class MySQLNotEqual(NotEqual):
def as_mysql(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
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[1:])
except ValueError:
pass
else:
return get_coordinate_lookup(dimension)
return super(CoordinatesField, self).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')
。