查询表达式

查询表达式描述了一个值或一个计算,它可以作为更新、创建、过滤、排序、注解或聚合的一部分。当一个表达式输出一个布尔值时,它可以直接用于过滤器中。有许多内置的表达式(在下面的文档中)可以用来帮助你编写查询。表达式可以组合,或者在某些情况下嵌套,以形成更复杂的计算。

支持的算术

Django 支持负、加、减、乘、除、模数运算,以及对查询表达式的幂运算符,使用 Python 常量、变量,甚至其他表达式。

输出字段

这一部分文档中许多表达式都支持一个可选的 output_field 参数。如果提供,Django 将在从数据库中检索该值后将其加载到该字段中。

output_field 接受一个模型字段实例,例如 IntegerField()BooleanField()。通常,该字段不需要任何参数,如 max_length,因为字段参数与数据验证相关,不会在表达式的输出值上执行。

只有在 Django 无法自动确定结果的字段类型时,才需要使用 output_field,例如混合字段类型的复杂表达式。例如,添加一个 DecimalField() 和一个 FloatField() 需要一个输出字段,如 output_field=FloatField()

一些例子

>>> from django.db.models import Count, F, Value
>>> from django.db.models.functions import Length, Upper
>>> from django.db.models.lookups import GreaterThan

# Find companies that have more employees than chairs.
>>> Company.objects.filter(num_employees__gt=F("num_chairs"))

# Find companies that have at least twice as many employees
# as chairs. Both the querysets below are equivalent.
>>> Company.objects.filter(num_employees__gt=F("num_chairs") * 2)
>>> Company.objects.filter(num_employees__gt=F("num_chairs") + F("num_chairs"))

# How many chairs are needed for each company to seat all employees?
>>> company = (
...     Company.objects.filter(num_employees__gt=F("num_chairs"))
...     .annotate(chairs_needed=F("num_employees") - F("num_chairs"))
...     .first()
... )
>>> company.num_employees
120
>>> company.num_chairs
50
>>> company.chairs_needed
70

# Create a new company using expressions.
>>> company = Company.objects.create(name="Google", ticker=Upper(Value("goog")))
# Be sure to refresh it if you need to access the field.
>>> company.refresh_from_db()
>>> company.ticker
'GOOG'

# Annotate models with an aggregated value. Both forms
# below are equivalent.
>>> Company.objects.annotate(num_products=Count("products"))
>>> Company.objects.annotate(num_products=Count(F("products")))

# Aggregates can contain complex computations also
>>> Company.objects.annotate(num_offerings=Count(F("products") + F("services")))

# Expressions can also be used in order_by(), either directly
>>> Company.objects.order_by(Length("name").asc())
>>> Company.objects.order_by(Length("name").desc())
# or using the double underscore lookup syntax.
>>> from django.db.models import CharField
>>> from django.db.models.functions import Length
>>> CharField.register_lookup(Length)
>>> Company.objects.order_by("name__length")

# Boolean expression can be used directly in filters.
>>> from django.db.models import Exists
>>> Company.objects.filter(
...     Exists(Employee.objects.filter(company=OuterRef("pk"), salary__gt=10))
... )

# Lookup expressions can also be used directly in filters
>>> Company.objects.filter(GreaterThan(F("num_employees"), F("num_chairs")))
# or annotations.
>>> Company.objects.annotate(
...     need_chairs=GreaterThan(F("num_employees"), F("num_chairs")),
... )

内置表达式

备注

这些表达式在 django.db.models.expressionsdjango.db.models.aggregates 中定义,但为了方便起见,通常从 django.db.models 中导入使用。

F() 表达式

class F

F() 对象表示模型字段的值、模型字段的转换值或注释列的值。它使得可以引用模型字段的值并对其执行数据库操作,而无需实际将它们从数据库提取到 Python 内存中。

取而代之的是,Django 使用 F() 对象来生成一个 SQL 表达式,在数据库层面描述所需的操作。

我们举个例子试试。通常情况下,我们可以这样做:

# Tintin filed a news story!
reporter = Reporters.objects.get(name="Tintin")
reporter.stories_filed += 1
reporter.save()

这里,我们从数据库中提取了 reporter.stories_filed 的值到内存中,并使用熟悉的 Python 操作符对其进行操作,然后将对象保存回数据库。但我们也可以这样做:

from django.db.models import F

reporter = Reporters.objects.get(name="Tintin")
reporter.stories_filed = F("stories_filed") + 1
reporter.save()

虽然 reporter.stories_filed = F('stories_filed') + 1 看起来像一个普通的 Python 赋值给一个实例属性,但实际上它是一个描述数据库操作的 SQL 结构。

当 Django 遇到 F() 的实例时,它会覆盖标准的 Python 运算符来创建一个封装的 SQL 表达式;在本例中,它指示数据库递增由 reporter.stories_filed 表示的数据库字段。

无论 reporter.stories_filed 上的值是多少,Python 永远不会知道它——它完全由数据库处理。通过 Django 的 F() 类,Python 所做的就是创建 SQL 语法来引用这个字段并描述操作。

要访问这样保存的新值,必须重新加载对象:

reporter = Reporters.objects.get(pk=reporter.pk)
# Or, more succinctly:
reporter.refresh_from_db()

F() 除了用于上述对单个实例的操作外,F() 还可以与 update() 一起用于对象实例的 QuerySets。这就把我们上面使用的两个查询——get()save() 减少到只有一个:

reporter = Reporters.objects.filter(name="Tintin")
reporter.update(stories_filed=F("stories_filed") + 1)

我们还可以使用 update() 来递增多个对象上的字段值——这可能比从数据库中把它们全部拉到 Python 中,在它们身上循环,递增每个对象的字段值,然后把每个对象保存回数据库要快得多:

Reporter.objects.update(stories_filed=F("stories_filed") + 1)

因此,F() 可以通过以下方式提供性能优势:

  • 让数据库,而不是 Python 来完成工作
  • 减少某些操作所需的查询次数

使用 F() 避免竞争条件

F() 的另一个有用的好处是,让数据库——而不是 Python——更新一个字段的值,避免了 竞争条件

如果两个 Python 线程执行上面第一个例子中的代码,一个线程可以在另一个线程从数据库中获取一个字段的值后,检索、递增并保存它。第二个线程保存的值将基于原始值,第一个线程的工作将丢失。

如果数据库负责更新字段,那么这个过程就比较稳健:它只会在执行 save()update() 时,根据数据库中字段的值来更新字段,而不是根据检索实例时的值来更新。

F() 赋值在 Model.save() 之后持续存在

F() 分配给模型字段的对象在保存模型实例后会持续存在,并将应用于每个 save()。例如:

reporter = Reporters.objects.get(name="Tintin")
reporter.stories_filed = F("stories_filed") + 1
reporter.save()

reporter.name = "Tintin Jr."
reporter.save()

在这种情况下,stories_filed 将被更新两次。如果最初是 1,最终值将是 3。这种持久性可以通过在保存模型对象后重新加载来避免,例如,使用 refresh_from_db()

在过滤器中使用 F()

F()QuerySet 过滤器中也非常有用,它们可以根据对象的字段值而不是 Python 值的标准来过滤一组对象。

这在 在查询中使用 F() 表达式 中有所记载。

与注解一起使用 F()

F() 可用于通过将不同的字段与算术相结合,在你的模型上创建动态字段:

company = Company.objects.annotate(chairs_needed=F("num_employees") - F("num_chairs"))

如果要组合的字段类型不同,您需要告诉 Django 返回什么类型的字段。大多数表达式都支持 output_field 用于此情况,但由于 F() 不支持,您需要使用 ExpressionWrapper 来包装表达式:

from django.db.models import DateTimeField, ExpressionWrapper, F

Ticket.objects.annotate(
    expires=ExpressionWrapper(
        F("active_at") + F("duration"), output_field=DateTimeField()
    )
)

在引用关系字段(如 ForeignKey)时,F() 返回主键值而不是模型实例:

>>> car = Company.objects.annotate(built_by=F("manufacturer"))[0]
>>> car.manufacturer
<Manufacturer: Toyota>
>>> car.built_by
3

使用 F() 对空值进行排序

使用 F()Expression.asc()esc() 的关键词参数 nulls_firstnulls_last 来控制字段的空值的排序。默认情况下,排序取决于你的数据库。

例如,在已经联系过的公司之后,对尚未联系过的公司进行排序(last_contacted 为空):

from django.db.models import F

Company.objects.order_by(F("last_contacted").desc(nulls_last=True))

使用 F() 进行逻辑操作

New in Django 4.2.

F() 表达式输出 BooleanField 的情况下,可以使用反转运算符 ~F() 进行逻辑否定。例如,要交换公司的激活状态:

from django.db.models import F

Company.objects.update(is_active=~F("is_active"))

Func() 表达式

Func() 表达式是所有涉及 COALESCELOWER 等数据库函数或 SUM 等集合的表达式的基本类型。它们可以直接使用:

from django.db.models import F, Func

queryset.annotate(field_lower=Func(F("field"), function="LOWER"))

或者可以用它们来建立一个数据库函数库:

class Lower(Func):
    function = "LOWER"


queryset.annotate(field_lower=Lower("field"))

但这两种情况都会产生一个查询集,其中每个模型都有一个额外的属性 field_lower 来注释,大致由以下 SQL 产生:

SELECT
    ...
    LOWER("db_table"."field") as "field_lower"

参见 数据库函数 获取内置的数据库函数列表。

Func API 如下:

class Func(*expressions, **extra)
function

描述将要生成的函数的类属性。具体来说,function 将作为 function 占位符插在 template 中。默认值为 None

template

一个类属性,作为格式字符串,描述为该函数生成的SQL。默认值为 '%(function)s(%(expressions)s)'

如果你正在构造像 strftime('%W', 'date') 这样的 SQL,并且需要在查询中使用 % 字符,那么在 template 属性中把它四倍(%%%%),因为这个字符串会被插值两次:一次是在 as_sql() 中的模板插值中,另一次是在数据库游标中的查询参数的 SQL 插值中。

arg_joiner

一个类属性,表示用于将 expressions 列表连接在一起的字符。默认值为 ', '

arity

一个类属性,表示函数接受的参数数。如果设置了这个属性,并且用不同数量的表达式调用函数,将引发 TypeError。默认值为 None

as_sql(compiler, connection, function=None, template=None, arg_joiner=None, **extra_context)

生成数据库函数的 SQL 片段。返回一个元组 (sql, params),其中 sql 是 SQL 字符串,params 是查询参数的列表或元组。

as_vendor() 方法应该使用 functiontemplatearg_joiner 以及其他 **extra_context 参数来根据需要定制 SQL。例如:

django/db/models/functions.py
class ConcatPair(Func):
    ...
    function = "CONCAT"
    ...

    def as_mysql(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler,
            connection,
            function="CONCAT_WS",
            template="%(function)s('', %(expressions)s)",
            **extra_context
        )

为了避免 SQL 注入漏洞,extra_context 不得包含不受信任的用户输入,因为这些值会被内插到 SQL 字符串中,而不是作为查询参数传递,数据库驱动对其进行转义。

*expressions 参数是一个位置表达式的列表,该函数将应用于此。表达式将被转换为字符串,用 arg_joiner 连接在一起,然后插入到 template 中作为 expressions 占位符。

位置参数可以是表达式或 Python 值。字符串被认为是列引用,将被包装在 F() 表达式中,而其他值将被包装在 Value() 表达式中。

**extra 关键字是 key=value 对,可以内插到 template 属性中。为了避免 SQL 注入漏洞,extra 不能包含不受信任的用户输入,因为这些值会被内插到 SQL 字符串中,而不是作为查询参数传递,数据库驱动程序对其进行转义。

functiontemplatearg_joiner 关键字可用于替换具有相同名称的属性,而无需定义自己的类。output_field 可用于定义预期的返回类型。

Aggregate() 表达式

聚合表达式是 Func() 表达式 的一个特例,它通知查询需要一个 GROUP BY 子句。所有的 聚合函数,如 Sum()Count(),都继承自 Aggregate()

由于 Aggregate 是表达式和封装表达式,你可以表示一些复杂的计算:

from django.db.models import Count

Company.objects.annotate(
    managers_required=(Count("num_employees") / 4) + Count("num_managers")
)

Aggregate 的 API 如下:

class Aggregate(*expressions, output_field=None, distinct=False, filter=None, default=None, **extra)
template

作为格式字符串的类属性,描述为该集合生成的 SQL。默认值为 '%(function)s(%(distinct)s%(expressions)s)'

function

描述将生成的集合函数的类属性。具体来说,function 将作为 function 占位符插值在 template 中。默认值为 None

window_compatible

默认为 True,因为大多数聚合函数可以作为 Window 中的源表达式。

allow_distinct

一个类属性,决定该聚合函数是否允许传递 distinct 关键字参数。如果设置为 False (默认),如果传递了 distinct=True,会引发 TypeError

empty_result_set_value

默认为 None,因为大多数聚合函数在应用于空结果集时会产生 NULL

expressions 的位置参数可以包括表达式、模型字段的转换或模型字段的名称。它们将被转换为字符串,并用作 template 中的 expressions 占位符。

distinct 参数决定是否应该为 expressions 的每一个不同的值(或一组值,对于多个 expressions)调用聚合函数。该参数仅在 allow_distinct 设置为 True 的聚合函数中得到支持。

filter 参数取一个 Q 对象,用于过滤被聚合的行。参见 条件聚合过滤注解 的用法示例。

default 参数接受一个值,该值将与聚合一起传递给 Coalesce。这对于指定在查询集(或分组)不包含条目时要返回的值非常有用,而不是返回 None

**extra 关键字是 key=value 对,可以内插到 template 属性中。

创建你自己的聚合函数

你也可以创建自己的聚合函数。至少,你需要定义 function,但你也可以完全自定义生成的 SQL。下面是一个简单的例子:

from django.db.models import Aggregate


class Sum(Aggregate):
    # Supports SUM(ALL field).
    function = "SUM"
    template = "%(function)s(%(all_values)s%(expressions)s)"
    allow_distinct = False

    def __init__(self, expression, all_values=False, **extra):
        super().__init__(expression, all_values="ALL " if all_values else "", **extra)

Value() 表达式

class Value(value, output_field=None)

Value() 对象表示一个表达式中最小的成分:一个简单的值。当你需要在一个表达式中表示一个整数、布尔值或字符串的值时,你可以将该值包裹在一个 Value() 中。

你很少会需要直接使用 Value()。当你写下表达式 F('field') + 1 时,Django 会隐式地将 1 包裹在 Value() 中,允许简单的值被用于更复杂的表达式中。当你想把一个字符串传递给一个表达式时,你需要使用 Value()。大多数表达式将字符串参数解释为字段的名称,如 Lower('name')

value 参数描述了要包含在表达式中的值,比如 1TrueNone。Django 知道如何将这些 Python 值转换为相应的数据库类型。

如果没有指定 output_field,则对于许多常见类型,它将从提供的 value 的类型推断出来。例如,将 datetime.datetime 的实例作为 value 传递将默认 output_fieldDateTimeField

ExpressionWrapper() 表达式

class ExpressionWrapper(expression, output_field)

ExpressionWrapper 包围另一个表达式,并提供了访问属性的功能,例如 output_field,这些属性可能在其他表达式上不可用。在使用描述中不同类型的 F() 表达式进行算术运算时,必须使用 ExpressionWrapper,如 与注解一起使用 F() 中所述。

条件表达式

条件表达式允许你在查询中使用 if ... elif ... else 逻辑。Django 原生支持 SQL CASE 表达式。更多细节请参见 条件表达式

Subquery() 表达式

class Subquery(queryset, output_field=None)

你可以使用 Subquery 表达式向 QuerySet 添加一个显式子查询。

例如,要使用最新评论的作者的电子邮件地址来注释每篇帖子:

>>> from django.db.models import OuterRef, Subquery
>>> newest = Comment.objects.filter(post=OuterRef("pk")).order_by("-created_at")
>>> Post.objects.annotate(newest_commenter_email=Subquery(newest.values("email")[:1]))

在 PostgreSQL 上,SQL 看起来像:

SELECT "post"."id", (
    SELECT U0."email"
    FROM "comment" U0
    WHERE U0."post_id" = ("post"."id")
    ORDER BY U0."created_at" DESC LIMIT 1
) AS "newest_commenter_email" FROM "post"

备注

本节中的例子是为了展示如何强制 Django 执行一个子查询。在某些情况下,可以写一个等价的查询集,更清楚或更有效地执行同样的任务。

从外部查询集中引用列

class OuterRef(field)

Subquery 中的查询集需要引用外部查询或其转换的字段时,请使用 OuterRef。它的行为类似于 F 表达式,不同之处在于检查是否引用有效字段的操作直到外部查询被解析时才会执行。

OuterRef 的实例可以与嵌套的 Subquery 的实例结合使用,以引用不是直接父级的包含查询集。例如,为了正确解析此查询集,它需要在嵌套的 Subquery 实例内部:

>>> Book.objects.filter(author=OuterRef(OuterRef("pk")))

将子查询限制为单列

有时需要从 Subquery 返回单个列,例如,将 Subquery 用作 __in 查找的目标时。要返回在最近一天内发布的所有评论:

>>> from datetime import timedelta
>>> from django.utils import timezone
>>> one_day_ago = timezone.now() - timedelta(days=1)
>>> posts = Post.objects.filter(published_at__gte=one_day_ago)
>>> Comment.objects.filter(post__in=Subquery(posts.values("pk")))

在这种情况下,子查询必须使用 values() 只返回一列:该文章的主键。

将子查询限制为单行

为了防止子查询返回多行,使用了查询集的切片([:1]):

>>> subquery = Subquery(newest.values("email")[:1])
>>> Post.objects.annotate(newest_commenter_email=subquery)

在这种情况下,子查询必须只返回一列 一行:最近创建的评论的电子邮件地址。

(使用 get() 而不是分片会导致失败,因为 OuterRefSubquery 中使用查询集之前无法解析。)

Exists() 子查询

class Exists(queryset)

Exists 是一个 Subquery 子类,它使用 SQL EXISTS 语句。在许多情况下,它的性能比子查询更好,因为当找到第一条匹配的记录时,数据库能够停止对子查询的执行。

例如,要为每篇帖子注释是否有来自最近一天内的评论:

>>> from django.db.models import Exists, OuterRef
>>> from datetime import timedelta
>>> from django.utils import timezone
>>> one_day_ago = timezone.now() - timedelta(days=1)
>>> recent_comments = Comment.objects.filter(
...     post=OuterRef("pk"),
...     created_at__gte=one_day_ago,
... )
>>> Post.objects.annotate(recent_comment=Exists(recent_comments))

在 PostgreSQL 上,SQL 看起来像:

SELECT "post"."id", "post"."published_at", EXISTS(
    SELECT (1) as "a"
    FROM "comment" U0
    WHERE (
        U0."created_at" >= YYYY-MM-DD HH:MM:SS AND
        U0."post_id" = "post"."id"
    )
    LIMIT 1
) AS "recent_comment" FROM "post"

没有必要强制 Exists 指向单一列,因为列会被丢弃,并返回一个布尔结果。同样,由于在 SQL EXISTS 子查询中,排序并不重要,只会降低性能,所以会自动删除。

可以用 `~Exists() 来查询 NOT EXISTS

Subquery()Exists() 表达式进行过滤。

返回布尔值的 Subquery()Exists() 可以用作 When 表达式中的 condition,或者直接过滤查询集:

>>> recent_comments = Comment.objects.filter(...)  # From above
>>> Post.objects.filter(Exists(recent_comments))

这将确保子查询不会被添加到 SELECT 列中,这可能会带来更好的性能。

Subquery 表达式中使用集合。

聚合可以在 Subquery 中使用,但需要将 filter()values()annotate() 进行特定的组合,才能使子查询分组正确。

假设两个模型都有一个 length 字段,要查找帖子长度大于所有组合评论的总长度的帖子:

>>> from django.db.models import OuterRef, Subquery, Sum
>>> comments = Comment.objects.filter(post=OuterRef("pk")).order_by().values("post")
>>> total_comments = comments.annotate(total=Sum("length")).values("total")
>>> Post.objects.filter(length__gt=Subquery(total_comments))

初始的 filter(...) 将子查询限制在相关参数上。order_by() 删除 Comment 模型上的默认 Order (如果有的话)。values('post')Post 聚合评论。最后,annotate(...) 执行聚合。这些查询集方法的应用顺序很重要。在这种情况下,由于子查询必须限于一列,所以需要使用 values('total')

这是在 Subquery 内进行聚合的唯一方法,因为使用 aggregary() 试图执行查询集(如果有 OuterRef,将无法解决)。

原始 SQL 表达式

class RawSQL(sql, params, output_field=None)

有时候数据库表达式不能轻松表示复杂的 WHERE 子句。在这些特殊情况下,可以使用 RawSQL 表达式。例如:

>>> from django.db.models.expressions import RawSQL
>>> queryset.annotate(val=RawSQL("select col from sometable where othercol = %s", (param,)))

这些额外的查找可能无法移植到不同的数据库引擎中(因为你是显式地编写 SQL 代码),并且违反了 DRY 原则,所以你应该尽可能地避免它们。

RawSQL 表达式也可以用作 __in 过滤器的目标:

>>> queryset.filter(id__in=RawSQL("select id from sometable where col = %s", (param,)))

警告

为了防止 SQL 注入攻击 ,你必须使用 params 来转义任何用户可以控制的参数。params 是一个必要的参数,以迫使你承认你没有用用户提供的数据来插值你的 SQL。

你也不能在 SQL 字符串中引用占位符。这个例子因为在 %s 周围的引号而容易受到 SQL 注入的影响。

RawSQL("select col from sometable where othercol = '%s'")  # unsafe!

你可以阅读更多关于 Django 的 SQL 注入保护 的工作原理。

窗口函数

窗口函数提供了一种在分区上应用函数的方法。与一般的聚合函数不同,窗口函数对 和分区进行操作,并计算每行的结果。

你可以在同一个查询中指定多个窗口,这在 Django ORM 中相当于在一个 QuerySet.annotate() 调用中包含多个表达式。ORM 并没有利用命名窗口,而是将其作为所选列的一部分。

class Window(expression, partition_by=None, order_by=None, frame=None, output_field=None)
template

默认为 %(expression)s OVER (%(window)s)。如果只提供了 expression 参数,则窗口子句将为空。

Window 类是 OVER 子句的主要表达式。

expression 参数是一个 窗口函数,一个 聚合函数,或者一个与窗口子句兼容的表达式。

partition_by 参数接受一个表达式或一个表达式序列(列名应包装在 F 对象中),用于控制行的分区。分区将缩小用于计算结果集的行范围。

output_field 要么作为参数指定,要么由表达式指定。

order_by 参数接受一个表达式,您可以在该表达式上调用 asc()desc(),一个字段名称的字符串(可以有可选的 "-" 前缀,表示降序),或一个字符串和/或表达式的元组或列表。排序控制表达式应用的顺序。例如,如果在一个分区内对行进行求和,第一个结果是第一行的值,第二个结果是第一行和第二行的和。

frame 参数指定在计算中应该使用哪些其他行。详见

Changed in Django 4.1:

添加了对按字段名称引用的 order_by 的支持。

例如,要为每部电影注释相同工作室、相同类型和发布年份的电影的平均评分:

>>> from django.db.models import Avg, F, Window
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         partition_by=[F("studio"), F("genre")],
...         order_by="released__year",
...     ),
... )

这可以让你检查一部电影的评分是好是坏,与它的同行相比。

您可能希望在相同的窗口上应用多个表达式,即相同的分区和帧。例如,您可以修改上一个示例,以在同一查询中使用三个窗口函数来包括每部电影组中(相同工作室、类型和发布年份)的最佳和最差评级。上一个示例中的分区和排序被提取到一个字典中,以减少重复:

>>> from django.db.models import Avg, F, Max, Min, Window
>>> window = {
...     "partition_by": [F("studio"), F("genre")],
...     "order_by": "released__year",
... }
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         **window,
...     ),
...     best=Window(
...         expression=Max("rating"),
...         **window,
...     ),
...     worst=Window(
...         expression=Min("rating"),
...         **window,
...     ),
... )

对窗口函数进行过滤是支持的,只要查找不是不连续的(不使用 ORXOR 作为连接器)并且对执行聚合的查询集进行过滤。

例如,依赖于聚合并具有针对窗口函数和字段的 OR 过滤的查询是不受支持的。在聚合后应用组合谓词可能会导致通常会从组中排除的行被包括进来:

>>> qs = Movie.objects.annotate(
...     category_rank=Window(Rank(), partition_by="category", order_by="-rating"),
...     scenes_count=Count("actors"),
... ).filter(Q(category_rank__lte=3) | Q(title__contains="Batman"))
>>> list(qs)
NotImplementedError: Heterogeneous disjunctive predicates against window functions
are not implemented when performing conditional aggregation.
Changed in Django 4.2:

添加了针对窗口函数的过滤支持。

在 Django 内置的数据库后端中,MySQL 8.0.2+、PostgreSQL、Oracle 都支持窗口表达式。不同的数据库对不同窗口表达式功能的支持也不同。例如, asc()esc() 中的选项可能不被支持。根据需要,请查阅你的数据库的文档。

对于一个窗口帧,你可以选择基于范围的行序列或普通的行序列。

class ValueRange(start=None, end=None)
frame_type

该属性被设置为 'RANGE'

PostgreSQL 对 ValueRange 的支持有限,只支持使用标准的开始和结束点,如 CURRENT ROWUNBOUNDED FOLLOWING

class RowRange(start=None, end=None)
frame_type

该属性被设置为 'ROWS'

这两个类都返回具有模板的 SQL:

%(frame_type)s BETWEEN %(start)s AND %(end)s

帧缩小了用于计算结果的行。它们从某个起点移动到某个指定的终点。帧可以不分区使用,但通常情况下,指定窗口的排序以确保结果的确定性是个好主意。在帧中,帧中的对等值是指具有等值的行,如果不存在排序子句,则是指所有行。

一个帧的默认起点是 UNBOUNDED PRECEDING,即分区的第一行。终点总是显式地包含在 ORM 生成的 SQL 中,默认为 UNBOUNDED FOLLOWING。默认帧包括从分区到集合中最后一行的所有行。

startend 参数的可接受值是 None、一个整数或零。start 的负整数会导致 N preceding,而 None 会产生 UNBOUNDED PRECEDING。对于 startend,0 将返回 CURRENT ROWend 接受正整数。

CURRENT ROW 包括的内容有区别。当在 ROWS 模式下指定时,帧以当前行开始或结束。当在 RANGE 模式下指定时,根据排序子句,帧以第一个或最后一个对等值开始或结束。因此,RANGE CURRENT ROW 对具有由排序指定的相同值的行执行表达式。因为模板包括 startend 点,所以可以用:

ValueRange(start=0, end=0)

如果一部电影的“同行”被描述为由同一工作室在同一类型和同一年份发布的电影,那么这个 RowRange 示例将为每部电影注释它的两个前辈和两个后辈的平均评分:

>>> from django.db.models import Avg, F, RowRange, Window
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         partition_by=[F("studio"), F("genre")],
...         order_by="released__year",
...         frame=RowRange(start=-2, end=2),
...     ),
... )

如果数据库支持,您可以根据分区中表达式的值指定起始点和结束点。如果 Movie 模型的 released 字段存储了每部电影的发布月份,那么这个 ValueRange 示例将为每部电影注释它的同行之间的平均评分,这些同行是在每部电影之前十二个月和之后十二个月内发布的电影:

>>> from django.db.models import Avg, F, ValueRange, Window
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         partition_by=[F("studio"), F("genre")],
...         order_by="released__year",
...         frame=ValueRange(start=-12, end=12),
...     ),
... )

技术信息

下面你会发现对库作者可能有用的技术实现细节。下面的技术 API 和示例将有助于创建通用的查询表达式,可以扩展 Django 提供的内置功能。

表达式 API

查询表达式实现了 查询表达式 API,但也暴露了下面列出的一些额外的方法和属性。所有查询表达式必须继承于 Expression() 或相关子类。

当一个查询表达式包装另一个表达式时,它负责在被包装的表达式上调用相应的方法。

class Expression
contains_aggregate

告诉 Django 这个表达式包含一个集合,需要在查询中添加一个 GROUP BY 子句。

contains_over_clause

告诉 Django 这个表达式包含一个 Window 表达式。例如,它用于在修改数据的查询中不允许使用窗口函数表达式。

filterable

告诉 Django 这个表达式可以在 QuerySet.filter() 中引用。默认值为 True

window_compatible

告诉 Django 这个表达式可以作为 Window 的源表达式。默认值为 False

empty_result_set_value

告诉 Django 在将表达式用于空结果集时应返回哪个值。默认值为 NotImplemented,它强制在数据库上计算表达式。

resolve_expression(query=None, allow_joins=True, reuse=None, summarize=False, for_save=False)

提供在将表达式添加到查询之前进行任何预处理或验证的机会。还必须在任何嵌套表达式上调用 resolve_expression()。应返回 selfcopy(),并进行任何必要的转换。

query 是后端查询的实现。

allow_joins 是一个允许或拒绝在查询中使用连接的布尔值。

reuse 是一组可重用的多连接方案的连接。

summarize 是一个布尔值,当 True 时,表示正在计算的查询是一个终端聚合查询。

for_save 是一个布尔值,当 True 时,表示正在执行的查询正在进行创建或更新。

get_source_expressions()

返回一个有序的内部表达式列表。例如:

>>> Sum(F("foo")).get_source_expressions()
[F('foo')]
set_source_expressions(expressions)

获取一个表达式列表,并将其存储起来,使 get_source_expressions() 能够返回它们。

relabeled_clone(change_map)

返回 self` 的克隆(副本),并重新标明所有列别名。当创建子查询时,列别名会被重新命名。relabeled_clone() 也应该对任何嵌套的表达式进行调用并分配给克隆。

change_map 是一个将旧别名映射到新别名的字典。

举例:

def relabeled_clone(self, change_map):
    clone = copy.copy(self)
    clone.expression = self.expression.relabeled_clone(change_map)
    return clone
convert_value(value, expression, connection)

一个钩子,允许表达式将 value` 强制转换成一个更合适的类型。

expressionself 相同。

get_group_by_cols()

负责返回由此表达式引用的列的列表。应在任何嵌套表达式上调用 get_group_by_cols()。特别是,F() 对象保存对列的引用。

Changed in Django 4.2:

移除了 alias=None 关键字参数。

asc(nulls_first=None, nulls_last=None)

返回准备按升序排序的表达式。

nulls_firstnulls_last 定义了如何对空值进行排序。参见 使用 F() 对空值进行排序 的用法示例。

Changed in Django 4.1:

在旧版本中,nulls_firstnulls_last 的默认值为 False

4.1 版后已移除: nulls_first=Falsenulls_last=False 传递给 asc() 已被弃用。请改用 None

desc(nulls_first=None, nulls_last=None)

返回准备好降序排序的表达式。

nulls_firstnulls_last 定义了如何对空值进行排序。参见 使用 F() 对空值进行排序 的用法示例。

Changed in Django 4.1:

在旧版本中,nulls_firstnulls_last 的默认值为 False

4.1 版后已移除: nulls_first=Falsenulls_last=False 传递给 desc() 已被弃用。请改用 None

reverse_ordering()

返回 self,包括在 order_by 中对排序顺序进行的任何修改。例如,一个实现 NULLS LAST 的表达式将把它的值改为 NULLS FIRST。只有实现排序顺序的表达式,如 OrderBy 才需要修改。当 reverse() 在一个查询集上被调用时,会调用这个方法。

编写自己的查询表达式

你可以编写你自己的查询表达式类,这些类使用并可以与其他查询表达式集成。让我们通过一个例子,在不使用内置的 Func() 表达式 的情况下,编写一个 COALESCE SQL 函数的实现。

COALESCE SQL 函数被定义为接收一个列或值的列表,它将返回第一个不是 NULL 的列或值。它将返回第一个不是 NULL 的列或值。

我们将首先定义用于生成 SQL 的模板和一个 __init__() 方法来设置一些属性:

import copy
from django.db.models import Expression


class Coalesce(Expression):
    template = "COALESCE( %(expressions)s )"

    def __init__(self, expressions, output_field):
        super().__init__(output_field=output_field)
        if len(expressions) < 2:
            raise ValueError("expressions must have at least 2 elements")
        for expression in expressions:
            if not hasattr(expression, "resolve_expression"):
                raise TypeError("%r is not an Expression" % expression)
        self.expressions = expressions

我们对参数进行一些基本的验证,包括要求至少有 2 个列或值,并确保它们是表达式。我们在这里要求 output_field,以便 Django 知道应该将最终的结果分配给哪种模型字段。

现在我们实现预处理和验证。由于在这一点上我们没有自己的验证,我们委托给嵌套的表达式:

def resolve_expression(
    self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
):
    c = self.copy()
    c.is_summary = summarize
    for pos, expression in enumerate(self.expressions):
        c.expressions[pos] = expression.resolve_expression(
            query, allow_joins, reuse, summarize, for_save
        )
    return c

接下来,我们编写负责生成 SQL 的方法:

def as_sql(self, compiler, connection, template=None):
    sql_expressions, sql_params = [], []
    for expression in self.expressions:
        sql, params = compiler.compile(expression)
        sql_expressions.append(sql)
        sql_params.extend(params)
    template = template or self.template
    data = {"expressions": ",".join(sql_expressions)}
    return template % data, sql_params


def as_oracle(self, compiler, connection):
    """
    Example of vendor specific handling (Oracle in this case).
    Let's make the function name lowercase.
    """
    return self.as_sql(compiler, connection, template="coalesce( %(expressions)s )")

as_sql() 方法可以支持自定义关键字参数,允许 as_vendorname() 方法覆盖用于生成 SQL 字符串的数据。使用 as_sql() 关键字参数进行自定义最好是在 as_vendorname() 方法中突变 self,因为后者在不同的数据库后端运行时可能会导致错误。如果你的类依赖于类属性来定义数据,可以考虑在你的 as_sql() 方法中允许覆盖。

我们使用 compiler.compile() 方法为每个 expressions 生成 SQL,并将结果用逗号连接起来。然后在模板中填入我们的数据,并返回 SQL 和参数。

我们还定义了一个专门针对 Oracle 后端的自定义实现。如果使用 Oracle 后端,将调用 as_oracle() 函数,而不是 as_sql()

最后,我们实现了其余的方法,使我们的查询表达式能够与其他查询表达式很好地配合:

def get_source_expressions(self):
    return self.expressions


def set_source_expressions(self, expressions):
    self.expressions = expressions

让我们看看它是如何工作的:

>>> from django.db.models import F, Value, CharField
>>> qs = Company.objects.annotate(
...     tagline=Coalesce(
...         [F("motto"), F("ticker_name"), F("description"), Value("No Tagline")],
...         output_field=CharField(),
...     )
... )
>>> for c in qs:
...     print("%s: %s" % (c.name, c.tagline))
...
Google: Do No Evil
Apple: AAPL
Yahoo: Internet Company
Django Software Foundation: No Tagline

避免 SQL 注入

由于 Func 的关键字参数 __init__()**extra)和 as_sql()**extra_context)的参数是内插到 SQL 字符串中,而不是作为查询参数传递的(数据库驱动程序会对其进行转义处理),因此它们不能包含不受信任的用户输入。

例如,如果 substring 是用户提供的,这个函数就容易被 SQL 注入:

from django.db.models import Func


class Position(Func):
    function = "POSITION"
    template = "%(function)s('%(substring)s' in %(expressions)s)"

    def __init__(self, expression, substring):
        # substring=substring is an SQL injection vulnerability!
        super().__init__(expression, substring=substring)

这个函数在没有任何参数的情况下生成一个 SQL 字符串。由于 substring 是作为关键字参数传递给 super().__init__() 的,所以在将查询发送到数据库之前,它就被插入到 SQL 字符串中。

下面是更正后的改写:

class Position(Func):
    function = "POSITION"
    arg_joiner = " IN "

    def __init__(self, expression, substring):
        super().__init__(substring, expression)

substring 代替作为位置参数传递,它将作为数据库查询的参数传递。

在第三方数据库后端增加支持

如果你使用的数据库后端对某个函数使用了不同的 SQL 语法,你可以通过在函数的类上打一个新的方法来增加对它的支持。

比方说,我们正在为微软的 SQL Server 编写一个后端,它使用 SQL 的 LEN 而不是 LENGTH 来实现 Length 函数。我们将把一个名为 as_sqlserver() 的新方法移植到 Length 类上:

from django.db.models.functions import Length


def sqlserver_length(self, compiler, connection):
    return self.as_sql(compiler, connection, function="LEN")


Length.as_sqlserver = sqlserver_length

你也可以使用 as_sql()template 参数自定义 SQL。

我们使用 as_sqlserver(),因为 django.db.connection.vendor 返回 sqlserver 作为后端。

第三方后端可以在后端包的顶层 __init__.py 文件或从顶层 __init__.py 导入的顶层 expressions.py 文件(或包)中注册它们的函数。

对于希望给自己正在使用的后端打补丁的用户项目来说,这段代码应该存在于 AppConfig.ready() 方法中。

Back to Top