自定义查询
Django提供了各种各样的用于过滤的内置查询(例如,exact
和icontains
)。 本文档解释了如何编写自定义查找以及如何更改已有查找的工作方式。 请参阅有关lookup的API参考。
1.一个简单的查询示例
让我们从一个简单的自定义查找开始。我们编写一个自定义查找 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.fields import Field
Field.register_lookup(NotEqual)
查找注册也可以用修饰模式来完成
from django.db.models.fields import Field
@Field.register_lookup
class NotEqualLookup(Lookup):
# ...
现在我们可以用foo__ne
来代表foo的任意字段。你需要确保在创建任意的queryset之前使用它。
(1) 你可以在models.py文件内设置它
(2) 或者在“AppConfig”内使用ready()
方法注册它。
1.1 具体步骤
(1)定义 lookup_name
属性
仔细观察实现过程,最开始我们需要“lookup_name
”这个属性。这个可以保证ORM理解如何编译“name__ne
”和使用“NotEqual”来建立结构化查询语言SQL。按照惯例,这些名字“name_ne”是小写字母字符串,但是很麻烦的是必须有“__”字符串
(2)定义as_sql
方法
之后我们需要定义一个“as_sql
”方法。这方法需要一个“SQLCompiler” 对象, 被叫做编译器,和一个有效的数据库连接。“SQLCompller”对象没有文档,我们只需要知道它有一个compile()方法可以返回一个元组包括SQL字符串,和插入这个字符串的参数。大部分情况下,你不需要直接使用这个对象你可以把它传送给“process_lhs()
”和“process_rhs()
”
“Lookup”工作依靠两个值, “lhs
”和“rhs
”,代表左右两边,左边是一个字段参考,但它可以是实现了query expression API的任何东西。右边是一个用户给的数值。举个例子:Author.objects.filter(name__ne='Jack')
,左边是一个引用Author模型的name字段的东西,“Jack”是右边。
我们调用“process_lhs”和“process_rhs”转化他们成为我们想要的用来检索的值通过之前我们提到的“编译器”。这个方法返回一个元组包含SQL数据库和插入SQL数据库一些参数,刚好就是我们‘as_sql’需要返回的。使用前面的例子,“process_lhs”返回('"author"."name"', [])
,“process_rhs”返回('"%s"', ['Jack'])
.在这个例子里面没有左手边的参数,但是这需要看情况而定,我们还需要包括这些参数当我们返回的时候。
最后,我们将这些部分组合成一个带有<>
的SQL表达式,并提供查询的所有参数。 然后我们返回一个包含生成的SQL字符串和参数的元组。
效果图:
(1)数据库:
(2)自定义Lookup 之后
2.简单的转换器示例
上面的自定义查找没问题,但在某些情况下,您可能希望能够将一些查找链接在一起。 例如,假设我们正在构建一个我们想要制作一个带有abs()
运算符的应用程序。 我们有一个Experiment
模型,它记录start值,end值和change值(start - end)。 我们想找到所有在Experiment模型中change属性等于一定数量的(Experiment.objects.filter(change__abs=27)
),或者在Experiment模型中change属性没有超过一定数量的(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
译者注,效果图如下:
(1)数据库
(2)调用Transform
通过使用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")
Django 2.1中的更改:
上两段所提到的排序与去重的支持被加入了。↑
当我们在应用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
一致。
3.编写一个高效的 abs__lt 查找
当使用上面写的abs
查找时,生成的SQL在某些情况下不会有效地使用索引。 特别是,当我们使用change__abs__lt = 27
时,这相当于change__gt = -27
和change__lt = 27
。 (对于lte
情况,我们可以使用SQLBETWEEN
)。
因此, 我们希望 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)
,以使这些查询非常高效。↑
4.Transformer 双向示例
我们之前讨论的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')
5.为现有查找的关系编写一个代替实现
有时,不同的数据库供应商对同一操作需要不同的SQL。对于此示例,我们将为NotEqual运算符重写MySQL的自定义实现。我们将使用!=
运算符而不是<>
。(请注意,实际上几乎所有数据库都支持这两种运算符,包括Django支持的所有官方数据库)。
我们可以通过NotEqual使用as_mysql方法创建子类来更改特定后端的行为 :
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。对于内置后端的vendor名称有sqlite,postgresql,mysql和oracle。
6.Django如何确定使用Lookup还是Transforms
在某些情况下,您可能希望根据传入的名称动态更改哪个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().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’)。