Skip to main content

自定义查找

Django提供了多种用于过滤的 内置查找 (例如,exacticontains)。本文档介绍了如何编写自定义查找以及如何更改现有查找的工作。有关查找的API参考,请参见 查找API参考

一个简单的查找示例

让我们从一个简单的自定义查找开始。我们将编写一个与 exact 相反的自定义查找 neAuthor.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 文件中,或者在 AppConfigready() 方法中注册查找。

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

然后我们需要定义 as_sql 方法。这需要一个 SQLCompiler 对象(称为 compiler)和活动数据库连接。 SQLCompiler 对象没有记录,但我们需要知道的唯一的事情是他们有一个 compile() 方法,它返回一个包含SQL字符串的元组,以及要内插到该字符串中的参数。在大多数情况下,您不需要直接使用它,可以传递给 process_lhs()process_rhs()

Lookup 对抗两个值,lhsrhs,代表左手边和右手边。左侧通常是字段引用,但它可以是实现 查询表达式API 的任何内容。右边是用户给出的值。在示例 Author.objects.filter(name__ne='Jack') 中,左手侧是对 Author 模型的 name 字段的参考,并且 'Jack' 是右手侧。

我们调用 process_lhsprocess_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=-27change__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_paramsrhs_params

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

注解

事实上,使用 __abs 的大多数查找都可以实现为这样的范围查询,并且在大多数数据库后端,这样做可能更有意义,因为您可以使用索引。然而,使用PostgreSQL,你可能想在 abs(change) 上添加一个索引,这将允许这些查询非常有效。

双边变压器示例

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

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

我们定义了一个 UpperCase 变换器,它使用SQL函数 UPPER() 在比较之前对值进行变换。我们定义 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)

现在,查询集 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。内置后端的供应商名称是 sqlitepostgresqloraclemysql

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

在某些情况下,您可能希望根据传入的名称动态更改返回的 TransformLookup,而不是修复它。例如,您可以有一个存储坐标或任意维度的字段,并希望允许像 .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')