如何编写自定义查找

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

查找示例

让我们从一个小的自定义查找开始。我们将编写一个自定义查找 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 子类,因此我们将其注册到 Field 直接:

from django.db.models import Field

Field.register_lookup(NotEqual)

也可以使用decorator模式完成查找注册:

from django.db.models import Field


@Field.register_lookup
class NotEqualLookup(Lookup):
    ...

我们现在可以使用 foo__ne 对于任何领域 foo . 在尝试使用它创建任何查询集之前,您需要确保此注册发生。您可以将实现放在 models.py 文件或在中注册查找 ready() AN方法 AppConfig .

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

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

A Lookup 针对两个值工作, lhsrhs ,代表左侧和右侧。左侧通常是字段引用,但它可以是实现 query expression API . 右边是用户给定的值。在这个例子中 Author.objects.filter(name__ne='Jack') ,左侧是对 name 领域 Author 模型,以及 'Jack' 是右手边。

我们调用 process_lhsprocess_rhs 要将它们转换为SQL所需的值,请使用 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 .

这也允许将结果用于 ORDER BYDISTINCT 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")

当查找在 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 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)

有一些值得注意的事情正在发生。第一, AbsoluteValueLessThan 不是调用 process_lhs() . 相反,它跳过了 lhs 通过做 AbsoluteValue 使用原版 lhs . 也就是说,我们想要 "experiments"."change"ABS("experiments"."change") . 直接指 self.lhs.lhs 是安全的 AbsoluteValueLessThan 只能从 AbsoluteValue 查找,这是 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支持的所有官方数据库)。

我们可以通过创建 NotEqual 用一个 as_mysql 方法:

class MySQLNotEqual(NotEqual):
    def as_mysql(self, compiler, connection, **extra_context):
        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.removeprefix("x"))
            except ValueError:
                pass
            else:
                return get_coordinate_lookup(dimension)
        return super().get_lookup(lookup_name)

然后你会定义 get_coordinate_lookup 适当地返回 Lookup 处理相关值的子类 dimension .

有一个同名的方法 get_transform() . get_lookup() 应始终返回 Lookup 子类,以及 get_transform()Transform 子类。记住这点很重要 Transform 对象可以进一步筛选,并且 Lookup 对象不能。

筛选时,如果只剩下一个查找名称要解析,我们将查找 Lookup . 如果有多个名称,它将查找 Transform . 在只有一个名字和一个 Lookup 找不到,我们要找一个 Transform 然后 exact 查找 Transform . 所有调用序列始终以 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') .