查询表达式

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

支持的算术运算

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, OuterRef
>>> 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[source]

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 都不会知道 - 它完全由数据库处理。Python 通过 Django 的 F() 类所做的所有事情,只是创建引用该字段并描述操作的 SQL 语法。

要访问以这种方式保存的新值,必须重新加载对象

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

除了如上所述在单个实例上使用之外,F() 还可以与 QuerySets 对象实例一起使用,以及 update()。这减少了我们上面使用的两个查询 - 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() 表达式

Django 5.1 中的新功能。

对于基于字符串的字段、基于文本的字段和 ArrayField,您可以使用 Python 的数组切片语法。索引为 0 为基准,并且不支持 slicestep 参数。例如

>>> # Replacing a name with a substring of itself.
>>> writer = Writers.objects.get(name="Priyansh")
>>> writer.name = F("name")[1:5]
>>> writer.save()
>>> writer.refresh_from_db()
>>> writer.name
'riya'

使用 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()nulls_firstnulls_last关键字参数传递给Expression.asc()desc()来控制字段空值的排序。默认情况下,排序顺序取决于您的数据库。

例如,要将未联系过的公司(last_contacted为null)排序在已联系过的公司之后。

from django.db.models import F

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

使用带逻辑运算符的F()

输出BooleanFieldF()表达式可以用反转运算符~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)[source]
function

一个类属性,描述将生成的函数。具体来说,function将被插值到template中的function占位符。默认为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)[source]

生成数据库函数的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连接在一起,然后作为expressions占位符插值到template中。

位置参数可以是表达式或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)[source]
template

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

function

一个类属性,描述将生成的聚合函数。具体来说,function 将被插值为template中的function占位符。默认为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 kwargs 是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)[source]

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

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

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

如果没有指定output_field,它将根据许多常见类型的提供的value类型推断出来。例如,将datetime.datetime的实例作为value传递会将output_field默认为DateTimeField

ExpressionWrapper() 表达式

class ExpressionWrapper(expression, output_field)[source]

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

条件表达式

条件表达式允许您在查询中使用ifelifelse逻辑。Django 本机支持 SQL CASE表达式。有关更多详细信息,请参见条件表达式

Subquery() 表达式

class Subquery(queryset, output_field=None)[source]

您可以使用 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 执行子查询。在某些情况下,可能可以使用等效的 QuerySet 更清晰或更高效地执行相同的任务。

引用外部 QuerySet 中的列

class OuterRef(field)[source]

Subquery 中的 QuerySet 需要引用外部 QuerySet 或其转换中的字段时,使用 OuterRef。它类似于 F 表达式,但直到外部 QuerySet 解析后才会检查它是否引用了有效的字段。

OuterRef 的实例可以与嵌套的 Subquery 实例结合使用,以引用不是直接父级的包含 QuerySet。例如,此 QuerySet 需要在嵌套的 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() 仅返回一列:帖子的主键。

将子查询限制为单行

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

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

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

(使用 get() 而不是切片将失败,因为 OuterRef 无法在 QuerySet 用于 Subquery 中之前解析。)

Exists() 子查询

class Exists(queryset)[source]

ExistsSubquery 的一个子类,它使用 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 子查询中的排序并不重要,并且只会降低性能,因此会自动将其删除。

您可以使用 NOT EXISTS~Exists() 进行查询。

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

返回布尔值的 Subquery()Exists() 可用作 When 表达式中的 condition,或直接过滤 QuerySet

>>> 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 模型上的默认 ordering(如果有)。values('post')Post 对评论进行聚合。最后,annotate(...) 执行聚合。应用这些 QuerySet 方法的顺序很重要。在这种情况下,由于子查询必须限制为一列,因此需要 values('total')

这是在 Subquery 中执行聚合的唯一方法,因为使用 aggregate() 会尝试评估 QuerySet(如果存在 OuterRef,则无法解析)。

原始 SQL 表达式

class RawSQL(sql, params, output_field=None)[source]

有时数据库表达式无法轻松表达复杂的 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 注入保护 如何工作的信息。

窗口函数

窗口函数提供了一种在分区上应用函数的方法。与为 group by 定义的每个集合计算最终结果的普通聚合函数不同,窗口函数作用于 和分区,并为每一行计算结果。

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

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

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

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

expression 参数可以是 窗口函数聚合函数或在窗口子句中兼容的表达式。

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

output_field 作为参数或通过表达式指定。

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

frame 参数指定计算中应使用哪些其他行。有关详细信息,请参阅 框架

例如,要为每部电影添加同一制片厂、同一类型和同一上映年份的电影的平均评分的注释

>>> 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 作为连接符)并且针对执行聚合的 QuerySet,就可以针对窗口函数进行过滤。

例如,依赖于聚合并且对窗口函数和字段使用 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.

在 Django 的内置数据库后端中,MySQL、PostgreSQL 和 Oracle 支持窗口表达式。不同数据库对不同窗口表达式功能的支持有所不同。例如,asc()desc() 中的选项可能不受支持。根据需要查阅数据库文档。

框架

对于窗口框架,您可以选择基于范围的行序列或普通行序列。

class ValueRange(start=None, end=None, exclusion=None)[source]
frame_type

此属性设置为 'RANGE'

PostgreSQL 对 ValueRange 的支持有限,仅支持使用标准起点和终点,例如 CURRENT ROWUNBOUNDED FOLLOWING

Django 5.1 中的更改

添加了 exclusion 参数。

class RowRange(start=None, end=None, exclusion=None)[source]
frame_type

此属性设置为 'ROWS'

Django 5.1 中的更改

添加了 exclusion 参数。

这两个类都返回带有以下模板的 SQL

%(frame_type)s BETWEEN %(start)s AND %(end)s
class WindowFrameExclusion[source]
Django 5.1 中的新功能。
CURRENT_ROW
GROUP
TIES
NO_OTHERS

exclusion 参数允许从支持的数据库上的窗口框架中排除行 (CURRENT_ROW)、组 (GROUP) 和关联 (TIES)

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

框架缩小了用于计算结果的行范围。它们从某个起点转移到某个指定的终点。框架可以与分区一起使用,也可以不与分区一起使用,但通常最好指定窗口的排序以确保确定性结果。在框架中,框架中的同级是具有等效值的行,或者如果没有排序子句,则为所有行。

框架的默认起点是 UNBOUNDED PRECEDING,它是分区的首行。终点始终在 ORM 生成的 SQL 中明确包含,并且默认为 UNBOUNDED FOLLOWING。默认框架包含从分区到集合中最后一行中的所有行。

startend 参数的接受值为 None、整数或零。start 的负整数会导致 N PRECEDING,而 None 会产生 UNBOUNDED PRECEDING。在 ROWS 模式下,可以为 start 使用正整数,从而产生 N FOLLOWINGend 接受正整数,并产生 N FOLLOWING。在 ROWS 模式下,可以为 end 使用负整数,从而产生 N PRECEDING。对于 startend,零将返回 CURRENT ROW

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 示例使用每部电影前后 12 个月内发行的同类电影的平均评分来注释每部电影。

>>> 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),
...     ),
... )
Django 5.1 中的更改

已为 RowRange 添加了对正整数 start 和负整数 end 的支持。

技术信息

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

表达式 API

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

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

class Expression[source]
allowed_default
Django 5.0 中的新增功能。

告诉 Django 此表达式可以在 Field.db_default 中使用。默认为 False

constraint_validation_compatible
Django 5.1 中的新功能。

告诉 Django 此表达式可以在约束验证期间使用。将 constraint_validation_compatible 设置为 False 的表达式只能有一个源表达式。默认为 True

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() 对象尤其会保存对列的引用。

asc(nulls_first=None, nulls_last=None)

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

nulls_firstnulls_last 定义如何对空值进行排序。有关示例用法,请参阅 使用 F() 对空值进行排序

desc(nulls_first=None, nulls_last=None)

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

nulls_firstnulls_last 定义如何对空值进行排序。有关示例用法,请参阅 使用 F() 对空值进行排序

reverse_ordering()

返回 self,并在 order_by 调用中进行必要的修改以反转排序顺序。例如,实现 NULLS LAST 的表达式会将其值更改为 NULLS FIRST。仅需要对实现排序顺序(如 OrderBy)的表达式进行修改。当在查询集中调用 reverse() 时,会调用此方法。

编写您自己的查询表达式

您可以编写自己的查询表达式类,这些类使用并可以与其他查询表达式集成。让我们通过编写 COALESCE SQL 函数的实现(不使用内置的 Func() 表达式)来逐步了解一个示例。

SQL 函数 COALESCE 定义为接受列或值的列表。它将返回第一个非 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 语法,则可以通过将新的方法修补到函数的类上来添加对它的支持。

假设我们正在为 Microsoft 的 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 文件中或顶级 expressions.py 文件(或包)中注册其函数,该文件(或包)从顶级 __init__.py 中导入。

对于希望修补其正在使用的后端的用户项目,此代码应位于 AppConfig.ready() 方法中。

返回顶部