查詢表達式

查詢表達式描述一個值或一個計算,可以用於更新、建立、篩選、排序、註解或聚合的一部分。當表達式輸出布林值時,可以直接在篩選器中使用。有許多內建的表達式(如下所述),可以用來幫助您撰寫查詢。表達式可以組合,或在某些情況下可以巢狀,以形成更複雜的計算。

支援的算術

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[來源]

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()Expression.asc()desc()nulls_firstnulls_last 關鍵字引數,來控制欄位空值的排序。預設情況下,排序取決於您的資料庫。

例如,將尚未聯絡的公司(last_contacted 為空)排在已聯絡的公司之後

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)[原始碼]
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)[原始碼]

產生資料庫函數的 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 kwargs 是 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 將作為 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)[原始碼]

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() 表達式上使用具有不同類型的算術運算時,如 使用 F() 進行註解 中所述,ExpressionWrapper 是必要的。

條件表達式

條件表達式允許您在查詢中使用 ifelifelse 邏輯。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() 而不是切片會失敗,因為 OuterRef 在查詢集在 Subquery 中使用之前無法解析。)

Exists() 子查詢

class Exists(queryset)[原始碼]

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,或直接篩選查詢集

>>> 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 表達式

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 注入保護 如何運作的資訊。

視窗函數

視窗函數提供了一種在分割區上應用函數的方法。與針對 group by 定義的每個集合計算最終結果的普通聚合函數不同,視窗函數在 框架 和分割區上運作,並為每一行計算結果。

您可以在同一個查詢中指定多個視窗,這在 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 參數指定計算中應使用的其他列。有關詳細資訊,請參閱 框架

例如,要註解每部電影,說明同一製片廠在同一類型和發行年份的電影的平均評分:

>>> 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)[原始碼]
frame_type

此屬性設定為 'RANGE'

PostgreSQL 對 ValueRange 的支援有限,僅支援使用標準的起點和終點,例如 CURRENT ROWUNBOUNDED FOLLOWING

在 Django 5.1 中變更

新增了 exclusion 引數。

class RowRange(start=None, end=None, exclusion=None)[原始碼]
frame_type

此屬性設定為 'ROWS'

在 Django 5.1 中變更

新增了 exclusion 引數。

這兩個類別都回傳具有範本的 SQL

%(frame_type)s BETWEEN %(start)s AND %(end)s
class WindowFrameExclusion[原始碼]
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 FOLLOWING。正整數被接受用於 end,並產生 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[原始碼]
allowed_default
在 Django 5.0 中新增。

告訴 Django 這個表達式可以用在 Field.db_default 中。預設值為 False

constraint_validation_compatible
Django 5.1 中的新功能。

告知 Django 這個運算式可以用於約束驗證。設定 constraint_validation_compatibleFalse 的運算式必須只有一個來源運算式。預設為 True

contains_aggregate

告知 Django 這個運算式包含一個彙總 (aggregate),因此需要在查詢中加入 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 是一個布林值,允許或拒絕在查詢中使用聯結 (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)。

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)的運算式才需要修改。當在 queryset 上呼叫 reverse() 時,會呼叫此方法。

撰寫自己的查詢運算式

您可以撰寫自己的查詢運算式類別,這些類別可以使用其他查詢運算式,並且可以與之整合。讓我們逐步執行一個範例,藉由撰寫一個 COALESCE SQL 函數的實作,而不使用內建的 Func() 運算式

COALESCE SQL 函數定義為採用欄位或值清單。它將回傳第一個不是 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 函式。我們將在 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() 方法中。

返回頂部