自定义查询器

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

    SQL 会自动适配不同的后端,所以我们不需要为使用不同的数据库而担心。

    要让它生效需要两个步骤,首先我们需要实现该查询器,然后我们需要告诉 Django 有关它的信息。

    1. from django.db.models import Lookup
    2. class NotEqual(Lookup):
    3. lookup_name = 'ne'
    4. def as_sql(self, compiler, connection):
    5. lhs, lhs_params = self.process_lhs(compiler, connection)
    6. rhs, rhs_params = self.process_rhs(compiler, connection)
    7. params = lhs_params + rhs_params
    8. return '%s <> %s' % (lhs, rhs), params

    为了注册 NotEqual 查询器,我们需要在对应需要该查询器的字段类中调用 register_lookup 方法。在该情形下,该查询器作用在所有的 Field 子类,所以我们直接将它注册在 Field 中:

    1. from django.db.models import Field
    2. Field.register_lookup(NotEqual)

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

    1. from django.db.models import Field
    2. @Field.register_lookup
    3. class NotEqualLookup(Lookup):
    4. # ...

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

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

    之后我们需要定义 as_sql 方法。此方法需要一个 SQLCompiler 对象, 被叫做 compiler,和一个有效的数据库连接。SQLCompller 对象没有文档,我们只需要知道它有一个 compile() 方法可以返回一个包括 SQL 字符串的元组,和插入这个字符串的参数。大部分情况下,你不需要直接使用这个对象你可以把它传送给 process_lhs()process_rhs()

    Lookup 工作依靠两个值,lhsrhs,代表左右两边,左边是一个字段参考,它可以是任何实现了 的实例。右边是一个用户给定的数值。举个例子: Author.objects.filter(name__ne='Jack'),左边是 Author 模型的 name 字段,右边是 'Jack'

    我们利用 process_lhsprocess_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))。

    注解

    这个例子有点刻意,但它很好地演示了以数据库后端独立方式可能实现的功能范围,并且没有重复 Django 中的功能。

    1. from django.db.models import Transform
    2. class AbsoluteValue(Transform):
    3. function = 'ABS'

    下一步,让我们为其注册 IntrgerField:

    1. from django.db.models import IntegerField
    2. IntegerField.register_lookup(AbsoluteValue)

    现在可以运行我们先前已有的查询了。Experiment.objects.filter(change__abs=27) 将生成下面的 SQL 语句:

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

    1. SELECT ... WHERE ABS("experiments"."change") < 27

    请注意,如果没有指定其他查找定义,Django则会将 change__abs=27 解析为 change__abs__exact=27

    这也允许把结果用在 ORDER BYDISTINCT ON 子句中。例如 Experiment.objects.order_by('change__abs') 生成:

    1. SELECT ... ORDER BY ABS("experiments"."change") ASC

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

    1. SELECT ... DISTINCT ON ABS("experiments"."change")

    当我们在应用 Transform 之后查找允许执行哪些查找时,Django 使用 output_field 属性。 我们不需要在这里指定它,因为它没有改变,但假设我们将 AbsoluteValue 应用于某个字段,该字段表示更复杂的类型(例如,相对于原点的点或复数) 那么我们可能想要指定转换返回一个 FloatField 类型以进行进一步的查找。 这可以通过在变换中添加 output_field 属性来完成:

    1. from django.db.models import FloatField, Transform
    2. class AbsoluteValue(Transform):
    3. lookup_name = 'abs'
    4. function = 'ABS'
    5. @property
    6. def output_field(self):
    7. return FloatField()

    这确保了像 abs__lte 这样的进一步查找与对 FloatField 一致。

    当使用上面写的 abs 查找时,生成的 SQL 在某些情况下不会有效地使用索引。 特别是,当我们使用 change__abs__lt=27 时,这相当于 change__gt=-27change__lt=27。(对于 lte 情况,我们可以使用 SQL BETWEEN)。

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

    1. SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

    实现方式是:

    这里有几件值得注意的事情。首先,AbsoluteValueLessThan 没有调用 process_lhs()。 相反,它会跳过由 AbsoluteValue 完成的 lhs 的转换,并使用原始的 lhs。也就是说,我们希望得到 "experiments"."change",而不是 ABS("experiments"."change") 。直接引用 self.lhs.lhs 是安全的,因为 AbsoluteValueLessThan 只能从 AbsoluteValue lookup 访问,即 lhs 总是 AbsoluteValue 的实例。

    另请注意,由于在查询中多次使用两边,所以需要多次包含 lhs_paramsrhs_params 参数。

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

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

    一个双向转换器示例

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

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

    我们定义了一个 UpperCase 转换器,使用了 SQL 函数 UPPER(),在比较之前转换值。我们定义了:attr:bilateral = True <django.db.models.Transform.bilateral> 指明此转换应同时用于 lhsrhs:

    1. from django.db.models import Transform
    2. class UpperCase(Transform):
    3. function = 'UPPER'
    4. bilateral = True

    下一步, 让我们注册它:

    1. CharField.register_lookup(UpperCase)
    2. TextField.register_lookup(UpperCase)

    现在, Author.objects.filter(name__upper="doe") 将会产生一个不区分大小写的查询如下:

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

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

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

    1. class MySQLNotEqual(NotEqual):
    2. def as_mysql(self, compiler, connection, **extra_context):
    3. lhs, lhs_params = self.process_lhs(compiler, connection)
    4. rhs, rhs_params = self.process_rhs(compiler, connection)
    5. params = lhs_params + rhs_params
    6. return '%s != %s' % (lhs, rhs), params
    7. Field.register_lookup(MySQLNotEqual)

    接着,我们可以里利用 Field 注册它。它会替换之前的 NotEqual 类,因为拥有相同的 lookup_name

    编译查询指令是,Django 先查找 as_%s % connection.vendor 方法,其次 as_sql。内置后端的提供商名为 sqlitepostgresqloraclemysql

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

    某些场景下,你可能期望基于传入的名字动态地返回 TransformLookup,而不是指定。例如,有一个字段,存储了一些坐标或尺寸,期望使用以下语法 .filter(coords__x7=4) 返回第七个值为 4 的坐标。为此,你需要用以下内容重写 get_lookup:

    1. class CoordinatesField(Field):
    2. def get_lookup(self, lookup_name):
    3. if lookup_name.startswith('x'):
    4. try:
    5. dimension = int(lookup_name[1:])
    6. except ValueError:
    7. pass
    8. else:
    9. return get_coordinate_lookup(dimension)
    10. 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')