查询表达式¶
查询表达式描述了一个值或计算过程,可用作更新、创建、过滤、排序、注释或聚合的一部分。当表达式输出布尔值时,可以直接用于过滤器中。Django 内置了许多表达式(详见下文),可用于帮助编写查询。表达式可以组合,在某些情况下还可以嵌套,以形成更复杂的计算。
支持的算术运算¶
Django 支持查询表达式的取反、加法、减法、乘法、除法、取模运算和幂运算,可以使用 Python 常量、变量甚至其他表达式。
输出字段¶
本节记录的许多表达式都支持一个可选的 output_field 参数。如果指定了该参数,Django 会在从数据库检索值后将其加载到相应的字段中。
output_field 接收一个模型字段实例,如 IntegerField() 或 BooleanField()。通常,该字段不需要任何参数(如 max_length),因为字段参数与数据验证有关,而数据验证不会在表达式的输出值上执行。
output_field 仅在 Django 无法自动确定结果的字段类型时才需要,例如混合了多种字段类型的复杂表达式。例如,将 DecimalField() 和 FloatField() 相加就需要一个输出字段,如 output_field=FloatField()。
output_field 还允许使用在特定模型字段上下文之外执行类型转换的自定义字段。例如,如果您经常需要对 timedelta 进行日期算术运算,可以创建一个处理此类转换的自定义字段,从而确保跨数据库的一致结果。请参阅 如何创建自定义模型字段。
一些示例¶
>>> 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")))
>>> 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.expressions 和 django.db.models.aggregates 中,但为了方便起见,它们通常可以从 django.db.models 中导入。
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 语法来引用该字段并描述操作。
除了像上面那样用于单个实例的操作外,F() 还可以与 update() 一起使用,对 QuerySet 执行批量更新。这会将我们上面使用的两个查询(即 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() 表达式进行切片¶
对于基于字符串的字段、基于文本的字段和 ArrayField,您可以使用 Python 的数组切片语法。索引从 0 开始。不支持 slice 的 step 参数以及负索引。例如:
>>> # Replacing a name with a substring of itself.
>>> writer = Writers.objects.get(name="Priyansh")
>>> writer.name = F("name")[1:5]
>>> writer.save()
>>> writer.name
'riya'
使用 F() 避免竞态条件¶
F() 的另一个有用好处是让数据库(而不是 Python)更新字段值可以避免*竞态条件*。
如果两个 Python 线程执行上述第一个示例中的代码,一个线程可能在另一个线程从数据库检索字段值之后,又检索、增加并保存了该字段的值。第二个线程保存的值将基于原始值;第一个线程的工作将丢失。
如果由数据库负责更新字段,则该过程更为稳健:它始终只根据执行 save() 或 update() 时数据库中字段的值来更新字段,而不是基于检索实例时的值。
F() 赋值在 Model.save() 后被刷新¶
分配给模型字段的 F() 对象会在执行 save() 时从数据库刷新(在支持的后端上,不会产生后续查询,如 SQLite、PostgreSQL 和 Oracle;在其他后端如 MySQL 或 MariaDB 上则会推迟)。例如:
>>> reporter = Reporters.objects.get(name="Tintin")
>>> reporter.stories_filed = F("stories_filed") + 1
>>> reporter.save()
>>> reporter.stories_filed # This triggers a refresh query on MySQL/MariaDB.
14 # Assuming the database value was 13 when the object was saved.
在 Django 的旧版本中,F() 对象在 save() 时不会从数据库刷新,这导致它们在每次保存实例时都会被求值并持久化。
在过滤器中使用 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() 或 desc() 的 nulls_first 或 nulls_last 关键字参数来控制字段空值的排序。默认情况下,排序取决于您的数据库。
例如,要将尚未联系的公司(last_contacted 为空)排在已联系的公司之后:
from django.db.models import F
Company.objects.order_by(F("last_contacted").desc(nulls_last=True))
将 F() 与逻辑运算一起使用¶
输出 BooleanField 的 F() 表达式可以使用反转运算符 ~F() 进行逻辑取反。例如,要切换公司的激活状态:
from django.db.models import F
Company.objects.update(is_active=~F("is_active"))
Func() 表达式¶
Func() 表达式是所有涉及数据库函数(如 COALESCE 和 LOWER)或聚合函数(如 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]¶
-
- 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()方法应使用function、template、arg_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 字符串中,而不是作为查询参数传递。
function、template 和 arg_joiner 关键字可用于替换同名属性,而无需定义自己的类。output_field 可用于定义预期的返回类型。
清理用于配置查询表达式的输入
内置数据库函数(如 Cast)在参数(如 output_field)是位置参数还是仅为关键字参数方面有所不同。对于 output_field 和其他几种情况,输入最终以关键字参数的形式到达 Func(),因此避免从不可信用户输入构建关键字参数的建议,同样适用于这些参数,就像它适用于 **extra 一样。
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, order_by=None, **extra)[source]¶
- template¶
一个作为格式字符串的类属性,描述为该聚合生成的 SQL。默认为
'%(function)s(%(distinct)s%(expressions)s)'。
- allow_distinct¶
一个决定此聚合函数是否允许传递
distinct关键字参数的类属性。如果设置为False(默认值),传递distinct=True将引发TypeError。
- allow_order_by¶
- Django 6.0 中的新功能。
一个决定此聚合函数是否允许传递
order_by关键字参数的类属性。如果设置为False(默认值),传递None以外的值给order_by将引发TypeError。
- empty_result_set_value¶
默认为
None,因为大多数聚合函数在应用于空结果集时会返回NULL。
expressions 位置参数可以包含表达式、模型字段的转换或模型字段名称。它们将被转换为字符串,并用作 template 中的 expressions 占位符。
distinct 参数决定聚合函数是否应针对 expressions 的每个不同值(或对于多个 expressions 的值集)进行调用。该参数仅在 allow_distinct 设置为 True 的聚合函数上受支持。
filter 参数接收一个用于过滤聚合行的 Q 对象。有关示例用法,请参阅 条件聚合 和 对注释进行过滤。
order_by 参数的行为类似于 order_by() 函数的 field_names 输入,接收一个字段名(带有可选的 "-" 前缀表示降序)或一个表达式(或字符串和/或表达式的元组或列表),用于指定结果中元素的排序。
default 参数接收一个值,该值将与聚合一起传递给 Coalesce。这对于在查询集(或分组)不包含任何条目时指定返回除 None 以外的值非常有用。
**extra 关键字参数是 key=value 对,可以插值到 template 属性中。
添加了 order_by 参数。
创建您自己的聚合函数¶
您也可以创建自己的聚合函数。至少,您需要定义 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
arity = 1
def __init__(self, expression, all_values=False, **extra):
super().__init__(expression, all_values="ALL " if all_values else "", **extra)
Value() 表达式¶
Value() 对象表示表达式中最小的组成部分:简单值。当您需要在表达式中表示整数、布尔值或字符串的值时,可以将该值包装在 Value() 中。
您很少需要直接使用 Value()。当您编写表达式 F('field') + 1 时,Django 会隐式地将 1 包装在 Value() 中,从而允许在更复杂的表达式中使用简单值。当您想要将字符串传递给表达式时,就需要使用 Value()。大多数表达式将字符串参数解释为字段名称,如 Lower('name')。
value 参数描述了包含在表达式中的值,如 1、True 或 None。Django 知道如何将这些 Python 值转换为相应的数据库类型。
如果未指定 output_field,对于许多常见类型,它将根据所提供 value 的类型进行推断。例如,传递 datetime.datetime 的实例作为 value,会将 output_field 默认为 DateTimeField。
ExpressionWrapper() 表达式¶
ExpressionWrapper 包裹另一个表达式并提供对属性(如 output_field)的访问,这些属性在其他表达式中可能不可用。当按照 在注释中使用 F() 中所述,对不同类型的 F() 表达式进行算术运算时,需要使用 ExpressionWrapper。
未执行数据库强制转换
ExpressionWrapper 仅为 ORM 设置输出字段,不会执行任何数据库层面的强制转换。要确保从数据库返回特定类型,请改用 Cast。
条件表达式¶
条件表达式允许您在查询中使用 if … elif … else 逻辑。Django 原生支持 SQL CASE 表达式。更多详情,请参阅 条件表达式。
Subquery() 表达式¶
您可以使用 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 执行子查询。在某些情况下,可能能够编写一个等效的查询集,以更清晰或更高效的方式执行相同的任务。
引用外部查询集中的列¶
当 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() 而不是切片将会失败,因为在子查询中使用查询集之前,OuterRef 无法被解析。)
Exists() 子查询¶
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 模型上的默认 ordering(如果有)。values('post') 按 Post 对评论进行分组。最后,annotate(...) 执行聚合。应用这些查询集方法的顺序非常重要。在这种情况下,由于子查询必须限制为单列,因此需要使用 values('total')。
这是在 Subquery 中执行聚合的唯一方法,因为使用 aggregate() 会尝试求值查询集(如果存在 OuterRef,则无法解析)。
原始 SQL 表达式¶
有时数据库表达式无法轻松表达复杂的 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,)))
窗口函数¶
窗口函数提供了一种在分区上应用函数的方法。与为分组定义的每组计算最终结果的普通聚合函数不同,窗口函数在 帧 (frames) 和分区上操作,并为每一行计算结果。
您可以在同一个查询中指定多个窗口,这在 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 参数指定计算中应使用哪些其他行。详见 帧 (Frames)。
例如,为每部电影注释上相同工作室、相同类型和发行年份的电影的平均评分:
>>> 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,
... ),
... )
只要查找不是析取的(不使用 OR 或 XOR 作为连接符)并且针对执行聚合的查询集,则支持针对窗口函数进行过滤。
例如,不支持依赖聚合且针对窗口函数和字段具有 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() 中的选项可能不受支持。请根据需要查阅您的数据库文档。
帧 (Frames)¶
对于窗口帧,您可以选择基于范围的行序列或普通的行序列。
- class ValueRange(start=None, end=None, exclusion=None)[source]¶
- frame_type¶
此属性设置为
'RANGE'。
PostgreSQL 对
ValueRange的支持有限,仅支持使用标准的开始和结束点,如CURRENT ROW和UNBOUNDED FOLLOWING。
这两个类都返回带有模板的 SQL:
%(frame_type)s BETWEEN %(start)s AND %(end)s
exclusion 参数允许在支持的数据库上从窗口帧中排除行(CURRENT_ROW)、组(GROUP)和平局(TIES)。
%(frame_type)s BETWEEN %(start)s AND %(end)s EXCLUDE %(exclusion)s
Frame(框架)用于缩小计算结果时所使用的行范围。它们从某个起点移动到某个指定的终点。Frame 可以在有或没有分区(partition)的情况下使用,但为了确保结果的确定性,通常建议指定窗口的排序方式。在 Frame 中,Frame 内的“同级(peer)”是指具有等值的一行,如果不存在排序子句,则指所有行。
Frame 的默认起点是 UNBOUNDED PRECEDING,即分区的起始行。终点在 ORM 生成的 SQL 中总是被显式包含,默认情况下是 UNBOUNDED FOLLOWING。默认的 Frame 包含从分区开始到集合中最后一行所有的行。
start 和 end 参数的可接受值为 None、整数或零。start 取负整数会得到 N PRECEDING,而 None 则会产生 UNBOUNDED PRECEDING。在 ROWS 模式下,start 可以使用正整数,从而得到 N FOLLOWING。end 接受正整数,结果为 N FOLLOWING。在 ROWS 模式下,end 可以使用负整数,结果为 N PRECEDING。对于 start 和 end,零都将返回 CURRENT ROW。
CURRENT ROW 的含义存在差异。当在 ROWS 模式下指定时,Frame 以当前行开始或结束。当在 RANGE 模式下指定时,Frame 根据排序子句在第一个或最后一个同级行处开始或结束。因此,RANGE CURRENT ROW 会针对所有具有与排序所指定值相同值的行计算表达式。由于模板同时包含了 start 和 end 点,这可以通过以下方式表达:
ValueRange(start=0, end=0)
如果某部电影的“同级(peers)”被描述为同年、同类型、由同一制片厂发行的电影,那么这个 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[source]¶
- allowed_default¶
告知 Django 此表达式可用于
Field.db_default。默认为False。
- constraint_validation_compatible¶
告知 Django 此表达式可以在约束验证期间使用。将
constraint_validation_compatible设置为False的表达式必须仅有一个源表达式。默认为True。
- contains_aggregate¶
告知 Django 此表达式包含聚合操作,并且查询中需要添加
GROUP BY子句。
- filterable¶
告知 Django 此表达式可以在
QuerySet.filter()中引用。默认为True。
- empty_result_set_value¶
告知 Django 当表达式用于在空结果集上应用函数时应返回什么值。默认为
NotImplemented,这会强制在数据库上进行计算。
- set_returning¶
- Django 5.2 新增。
告知 Django 此表达式包含集合返回函数,从而强制进行子查询评估。它用于允许某些 Postgres 集合返回函数(例如
JSONB_PATH_QUERY、UNNEST等)跳过优化,并在标注本身生成行时被正确评估。默认为False。
- resolve_expression(query=None, allow_joins=True, reuse=None, summarize=False, for_save=False)¶
提供在将表达式添加到查询之前进行任何预处理或验证的机会。也必须在任何嵌套表达式上调用
resolve_expression()。应返回self的一个copy(),包含任何必要的转换。query是后端查询的实现。allow_joins是一个布尔值,用于允许或拒绝在查询中使用连接(join)。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强制转换为更合适类型的钩子(hook)。expression与self相同。
- get_group_by_cols()¶
负责返回此表达式引用的列列表。应在任何嵌套表达式上调用
get_group_by_cols()。特别是F()对象,它们持有对列的引用。
- asc(nulls_first=None, nulls_last=None)¶
返回准备按升序排序的表达式。
nulls_first和nulls_last定义了空值如何排序。有关示例用法,请参见 使用 F() 排序空值。
- desc(nulls_first=None, nulls_last=None)¶
返回准备按降序排序的表达式。
nulls_first和nulls_last定义了空值如何排序。有关示例用法,请参见 使用 F() 排序空值。
编写自己的查询表达式¶
您可以编写自己的查询表达式类,它们可以使用并能够与其他查询表达式集成。让我们通过编写一个 COALESCE SQL 函数的实现来逐步了解,且不使用内置的 Func() 表达式。
COALESCE SQL 函数被定义为接收列或值的列表。它将返回第一个非 NULL 的列或值。
我们将首先定义用于 SQL 生成的模板,并定义一个 __init__() 方法来设置一些属性:
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, tuple(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_vendorname() 方法内变异 self,使用 as_sql() 关键字参数进行自定义更为可取,因为后者在不同的数据库后端运行会导致错误。如果您的类依赖类属性来定义数据,请考虑允许在 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 语法,您可以通过给该函数的类添加一个新方法(monkey patching)来增加支持。
假设我们正在为 Microsoft SQL Server 编写后端,它使用 SQL LEN 而不是 LENGTH 来实现 Length 函数。我们将给 Length 类添加一个名为 as_sqlserver() 的新方法:
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() 方法中。