---
title: "如何编写自定义的查询器"
version: 6.1
locale: zh-hans
source: https://docs.djangoproject.com/zh-hans/6.1/howto/custom-lookups/
canonical: https://djangodocs.dev/zh-hans/6.1/howto/custom-lookups/
---
# 如何编写自定义的查询器

Django 提供了各种各样的 [内置查询器](/zh-hans/6.1/ref/models/querysets/#field-lookups) （例如， `exact` 和 `icontains` ）。本文档解释了如何编写自定义查询器以及如何更改已有查询器的工作方式。 有关 lookup 的 API 参考，请参阅 [查找 API 参考](/zh-hans/6.1/ref/models/lookups/)。

## 一个查询器示例。

让我们以一个小巧的自定义查询器为例。我们将书写一个名为 `ne` 的自定义查询器，它的效果与 `exact` 相反。语句 `Author.objects.filter(name__ne='Jack')`  将会翻译为下面的 SQL 语句：

```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 import Field

Field.register_lookup(NotEqual)
```

查询器注册也可以用修饰模式来完成:

```
from django.db.models import Field

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

现在我们可以用 `foo__ne` 来代表 `foo` 的任意字段。你需要确保注册行为发生在创建任意的 queryset 之前。你可以在 `models.py` 文件内设置它，或者在 `AppConfig` 的 `ready()` 方法中注册它。

仔细观察实现过程，第一个要求的属性是 `lookup_name`。它能让 ORM 理解如何编译 `name_ne` 并使用 `NotEqual` 来建立 SQL 语句。按照惯例，这些名字应该总是仅包含小写字母的字符串，但是绝对不能包含双下划线 `__`。

We then need to define the `as_sql` method. This takes a `SQLCompiler`
object, called `compiler`, and the active database connection.
`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. In most cases, you don't need to use it directly and can pass it on to
`process_lhs()` and `process_rhs()`.

`Lookup` 工作依靠两个值，`lhs` 和 `rhs`，代表左右两边，左边是一个字段参考，它可以是任何实现了 [查询表达式 API](/zh-hans/6.1/ref/models/lookups/#query-expression) 的实例。右边是一个用户给定的数值。举个例子： `Author.objects.filter(name__ne='Jack')`，左边是 `Author` 模型的 `name` 字段，右边是 `'Jack'`。

我们利用 `process_lhs` 和 `process_rhs` 将他们转换为我们期望值，用于之前介绍的 `compiler` 对象执行 SQL。这俩方法返回一个元组，包含一些 SQL 语句和插入 SQL 语句一些参数，就像是 `as_sql` 方法需要返回的。前文所述的例子中，`process_lhs` 返回 `('"author"."name"', [])`，`process_lhs` 返回 `('"%s"', ['Jack'])`。在这个例子里面没有手边的参数，这需要看情况而定，所以我们仍需要在返回结果时包括这些参数。

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

## 一个转换器示例。

上面的自定义查询器没问题，但在某些情况下，您可能希望能够将一些查询器链接在一起。 例如，假设我们正在构建一个使用 `abs()` 运算的应用程序。我们有一个 `Experiment` 模型，它记录起始值，结束值和差值（起始 - 结束）。 我们想找到 change 属性等于某个数值的所有 Experiment 对象(`Experiment.objects.filter(change__abs = 27)`)，change属性没有超过一定数量的 Experiment 对象(`Experiment.objects.filter(change__abs__lt= 27)`)。

> **Note**
>
> 这个例子有点刻意，但它很好地演示了以数据库后端独立方式可能实现的功能范围，并且没有重复 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 语句:

```sql
SELECT ... WHERE ABS("experiments"."change") = 27
```

使用 `Transform` 代替 `Lookup` 意味着我们可以在后面联锁更多的查询，所以 `Experiment.objects.filter(change__abs__lt=27)` 将会生成下面的 SQL：

```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')` 生成:

```sql
SELECT ... ORDER BY ABS("experiments"."change") ASC
```

并且在支持对字段使用 distinct 的数据库中（比如 PostgreSQL），`Experiment.objects.distinct('change__abs')` 会产生：

```sql
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` 和 `change__lt=27`。（对于 `lte` 情况，我们可以使用 SQL `BETWEEN`）。

所以我们期望 `Experiment.objects.filter(change__abs__lt=27)`  会生成下列 SQL 语句

```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` lookup 访问，即 `lhs` 总是 `AbsoluteValue` 的实例。

另请注意，由于在查询中多次使用两边，所以需要多次包含 `lhs_params` 和 `rhs_params` 参数。

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

> **Note**
>
> 实际上，大多数的利用 `__abs` 的查找都可以被转换为类似此的范围查找，且在大多数数据库后端来说，这样做能更好的利用索引。不过，对于 PostgreSQL，你可能会为 `abs(change)` 添加索引，这会使查找更加高效。

## 一个双向转换器示例

前文所述的 `AbsoluteValue` 例子实现了左侧查询。在某些场景下，你期望转换器同时作用于左侧和右侧。例如，如果你想在左侧基于等式进行过滤，而右侧对于某些 SQL 函数不敏感。

让我们在此测试这个大小写转换器。实际上这个转换器不是非常实用，因为 Django 已经内置了一系列大小写敏感相关的查询器，但它将是双向转换的一个很好的演示，且通过与数据库无关的方式来演示。

我们定义了一个 `UpperCase` 转换器，使用了 SQL 函数 `UPPER()`，在比较之前转换值。我们定义了:attr:bilateral = True \<django.db.models.Transform.bilateral\> 指明此转换应同时用于 `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")` 将会产生一个不区分大小写的查询如下：

```sql
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
```

## 为现有的查找编写代替实现

有时候，不同的数据库提供商对相同的操作要求不同的 SQL 语句。针对此例子，我们会为 MySQL 重写 NotEqual 操作符。使用 `!=` 操作符替代 `<>`。（注意，实际上几乎所有的数据两者都支持，包括 Django 支持的所有正式数据库）。

我们可以通过使用 `as_mysql` 方法创建 `NotEqual` 的子类来更改特定后端的行为:

```
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`。内置后端的提供商名为 `sqlite`，`postgresql`，`oracle` 和 `mysql`。

## Django 是如何取舍查询器和转换器的

某些场景下，你可能期望基于传入的名字动态地返回 `Transform` 或 `Lookup`，而不是指定。例如，有一个字段，存储了一些坐标或尺寸，期望使用以下语法 `.filter(coords__x7=4)` 返回第七个值为 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` 查询器。所以的系列调用都以一个 `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')`。
