天天看点

Django 2.1.3 模型层 自定义查询1.一个简单的查询示例2.简单的转换器示例3.编写一个高效的 abs__lt 查找4.Transformer 双向示例5.为现有查找的关系编写一个代替实现6.Django如何确定使用Lookup还是Transforms

自定义查询

  • 1.一个简单的查询示例
    • 1.1 具体步骤
  • 2.简单的转换器示例
  • 3.编写一个高效的 abs__lt 查找
  • 4.Transformer 双向示例
  • 5.为现有查找的关系编写一个代替实现
  • 6.Django如何确定使用Lookup还是Transforms

Django提供了各种各样的用于过滤的内置查询(例如,

exact

icontains

)。 本文档解释了如何编写自定义查找以及如何更改已有查找的工作方式。 请参阅有关lookup的API参考。

1.一个简单的查询示例

让我们从一个简单的自定义查找开始。我们编写一个自定义查找 ne ,它与 exact 相反。 Author.objects.filter(name__ne=‘Jack’) 将会转换成 SQL语句:

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)数据库:

Django 2.1.3 模型层 自定义查询1.一个简单的查询示例2.简单的转换器示例3.编写一个高效的 abs__lt 查找4.Transformer 双向示例5.为现有查找的关系编写一个代替实现6.Django如何确定使用Lookup还是Transforms

(2)自定义Lookup 之后

Django 2.1.3 模型层 自定义查询1.一个简单的查询示例2.简单的转换器示例3.编写一个高效的 abs__lt 查找4.Transformer 双向示例5.为现有查找的关系编写一个代替实现6.Django如何确定使用Lookup还是Transforms

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

译者注,效果图如下:

(1)数据库

Django 2.1.3 模型层 自定义查询1.一个简单的查询示例2.简单的转换器示例3.编写一个高效的 abs__lt 查找4.Transformer 双向示例5.为现有查找的关系编写一个代替实现6.Django如何确定使用Lookup还是Transforms

(2)调用Transform

Django 2.1.3 模型层 自定义查询1.一个简单的查询示例2.简单的转换器示例3.编写一个高效的 abs__lt 查找4.Transformer 双向示例5.为现有查找的关系编写一个代替实现6.Django如何确定使用Lookup还是Transforms

通过使用

Transform

而不是

Lookup

,这意味着我们可以在之后链接进一步的查找。 所以

Experiment.objects.filter(change__abs__lt = 27)

将生成以下SQL

请注意,如果没有指定其他查找定义,Django则会将

change__abs = 27

解析为

change__abs__exact = 27

这也允许结果用于

ORDER BY

DISTINCT ON

子句。 例如

Experiment.objects.order_by('change__abs')

会生成:

在支持字段去重的数据库(例如PostgreSQL)上,语句

Experiment.objects.distinct('change__abs')

会生成:

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

情况,我们可以使用SQL

BETWEEN

)。

因此, 我们希望

Experiment.objects.filter(change__abs__lt=27)

能生成以下 SQL:

实现方式是:

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中进行转换。

效果图:

Django 2.1.3 模型层 自定义查询1.一个简单的查询示例2.简单的转换器示例3.编写一个高效的 abs__lt 查找4.Transformer 双向示例5.为现有查找的关系编写一个代替实现6.Django如何确定使用Lookup还是Transforms

注解

事实上,大多数查找可以实现为像

__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”)

查询集会生成一个像这样的不区分大小写的查询:

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’)。