自定义查找

Django提供了多种用于过滤的built-in lookups(例如,exacticontains)。 本文档解释了如何编写自定义查找以及如何改变现有查找的工作。 有关查找的API参考,请参阅Lookup API reference

一个简单的查找示例

我们从一个简单的自定义查找开始。 我们将编写一个定制的查找ne,它与exact相反。 Author.objects.filter(name__ne='Jack') will translate to the 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文件中,或者将查找注册到AppConfigready()方法中。

仔细看看实现,第一个必需的属性是lookup_name 这允许ORM理解如何解释name__ne并使用NotEqual来生成SQL。 按照惯例,这些名称总是只包含字母的小写字符串,但唯一的要求是它不能包含字符串__

然后我们需要定义as_sql方法。 这需要一个名为compilerSQLCompiler对象和活动的数据库连接。 SQLCompiler objects are not documented, but the only thing we need to know about them is that they have a compile() method which returns a tuple containing an SQL string, and the parameters to be interpolated into that string. 在大多数情况下,您不需要直接使用它,可以将它传递给process_lhs()process_rhs()

一个Lookup是针对左侧和右侧两个值,lhsrhs 左侧通常是字段引用,但可以是实现query expression API的任何内容。 右边是用户给出的值。 In the example Author.objects.filter(name__ne='Jack'), the left-hand side is a reference to the name field of the Author model, and 'Jack' is the right-hand side.

我们使用前面描述的compiler对象,将process_lhsprocess_rhs转换为我们所需的SQL值。 这些方法返回包含一些SQL的元组和要插入到SQL中的参数,就像我们需要从我们的as_sql方法返回一样。 In the above example, process_lhs returns ('"author"."name"', []) and process_rhs returns ('"%s"', ['Jack']). 在这个例子中,左侧没有参数,但这取决于我们所拥有的对象,所以我们仍然需要将它们包含在我们返回的参数中。

最后,我们将这些部分与<>组合成一个SQL表达式,并提供查询的所有参数。 然后我们返回一个包含生成的SQL字符串和参数的元组。

一个简单的变压器例子

上面的自定义查询是好的,但是在某些情况下,您可能希望能够将查询链接在一起。 例如,假设我们正在构建一个应用程序,以便使用abs()运算符。 我们有一个Experiment模型,它记录了一个开始值,结束值和变化(开始 - 结束)。 We would like to find all experiments where the change was equal to a certain amount (Experiment.objects.filter(change__abs=27)), or where it did not exceed a certain amount (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) will generate the following 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类型来进一步查找。 这可以通过向transform添加一个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)

有一些值得注意的事情正在进行。 First, AbsoluteValueLessThan isn’t calling process_lhs(). 相反,它会跳过由AbsoluteValue完成的lhs的转换,并使用原始的lhs 也就是说,我们想得到"experiments"."change"不是ABS("experiments"."change") Referring directly to self.lhs.lhs is safe as AbsoluteValueLessThan can be accessed only from the AbsoluteValue lookup, that is the lhs is always an instance of AbsoluteValue.

还要注意,由于双方在查询中多次使用,参数需要多次包含lhs_paramsrhs_params

最后一个查询直接在数据库中进行反转(27-27)。 这样做的原因是,如果self.rhs不是普通整数值(例如F()引用),我们不能进行转换在Python中。

注意

实际上,大多数使用__abs的查找都可以像这样的范围查询来实现,而在大多数数据库后端,这样做可能更明智,因为您可以使用这些索引。 但是对于PostgreSQL,您可能需要在abs(change)上添加一个索引,这样可以使这些查询非常高效。

双向变压器示例

我们之前讨论的AbsoluteValue示例是一个适用于查找左侧的转换。 在某些情况下,您希望将转换应用于左侧和右侧。 例如,如果你想过滤一个基于左侧和右侧不相等的查询集,对一些SQL函数不敏感。

我们来看一下这里不区分大小写转换的一个简单例子。 这个转换在实践中并不是非常有用,因为Django已经有了一大堆内置的不区分大小写的查找,但是它将以数据库不可知的方式很好地演示双向转换。

We define an UpperCase transformer which uses the SQL function UPPER() to transform the values before comparison. 我们定义bilateral = True来表示这个转换应该适用于lhsrhs

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)

现在,queryset 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 as_sql 内置后端的供应商名称是sqlitepostgresqloraclemysql

Django如何确定使用的查找和变换

在某些情况下,您可能希望根据传入的名称来动态更改其中的TransformLookup,而不是修复它。 作为一个例子,可以有存储坐标或任意的尺寸的字段,并希望允许像.filter(coords__x7=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() should always return a Lookup subclass, and get_transform() a Transform subclass. 记住Transform对象可以被进一步过滤,而Lookup对象不能。

过滤时,如果只有一个查找名称需要解析,我们将查找Lookup 如果有多个名称,则会查找Transform In the situation where there is only one name and a Lookup is not found, we look for a Transform and then the exact lookup on that Transform. 所有的呼叫序列总是以Lookup结尾。 澄清:

  • .filter(myfield__mylookup)会调用myfield.get_lookup('mylookup')
  • .filter(myfield__mytransform__mylookup)会调用myfield.get_transform('mytransform'),然后是mytransform.get_lookup('mylookup')
  • .filter(myfield__mytransform) will first call myfield.get_lookup('mytransform'), which will fail, so it will fall back to calling myfield.get_transform('mytransform') and then mytransform.get_lookup('exact').