查詢表達式¶
查詢表達式描述一個值或一個計算,可以用於更新、建立、篩選、排序、註解或聚合的一部分。當表達式輸出布林值時,可以直接在篩選器中使用。有許多內建的表達式(如下所述),可以用來幫助您撰寫查詢。表達式可以組合,或在某些情況下可以巢狀,以形成更複雜的計算。
支援的算術¶
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.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 都永遠不會知道它 - 它完全由資料庫處理。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()
表達式¶
對於基於字串的欄位、基於文字的欄位和 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.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_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)[原始碼]¶
-
- 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()
方法應使用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
kwargs 是 key=value
對,可以內插到 template
屬性中。為了避免 SQL 注入漏洞,extra
不得包含不受信任的使用者輸入,因為這些值會內插到 SQL 字串中,而不是作為查詢參數傳遞,這樣資料庫驅動程式會對它們進行轉義。
可以使用 function
、template
和 arg_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)'
。
- 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()
表達式¶
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
是必要的。
條件表達式¶
條件表達式允許您在查詢中使用 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
在查詢集在 Subquery
中使用之前無法解析。)
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
子查詢中並不重要,並且只會降低效能,因此會自動移除排序。
您可以使用 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 表達式¶
有時資料庫表達式無法輕易表達複雜的 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,)))
視窗函數¶
視窗函數提供了一種在分割區上應用函數的方法。與針對 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,
... ),
... )
只要查找不是分離的(不使用 OR
或 XOR
作為連接詞)且針對執行聚合的 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 ROW
和UNBOUNDED 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
exclusion
引數允許在支援的資料庫上從視窗框架中排除列 (CURRENT_ROW
)、群組 (GROUP
) 和同分 (TIES
)。
%(frame_type)s BETWEEN %(start)s AND %(end)s EXCLUDE %(exclusion)s
框架縮小了用於計算結果的列。它們從某個起點移動到某個指定的終點。框架可以與分割一起使用,也可以不與分割一起使用,但通常最好指定視窗的排序,以確保結果具有確定性。在框架中,框架中的同級是指具有相同值的列,如果沒有排序子句,則指所有列。
框架的預設起點為 UNBOUNDED PRECEDING
,即分割區的第一列。終點始終明確地包含在 ORM 產生的 SQL 中,預設為 UNBOUNDED FOLLOWING
。預設框架包含從分割區到集合中最後一列的所有列。
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
模式下指定時,框架會以目前的列開始或結束。在 RANGE
模式下指定時,框架會根據排序子句在第一個或最後一個同級開始或結束。因此,RANGE CURRENT ROW
會針對具有排序所指定相同值的列評估表達式。由於範本同時包含 start
和 end
點,因此可以使用以下方式表示:
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),
... ),
... )
為 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_compatible
為False
的運算式必須只有一個來源運算式。預設為True
。
- contains_aggregate¶
告知 Django 這個運算式包含一個彙總 (aggregate),因此需要在查詢中加入
GROUP BY
子句。
- filterable¶
告知 Django 這個運算式可以在
QuerySet.filter()
中被引用。預設為True
。
- empty_result_set_value¶
告知 Django 當運算式被用於對空的結果集套用函數時,應該回傳哪個值。預設為
NotImplemented
,這會強制在資料庫上計算運算式。
- 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__()
方法來設定一些屬性開始
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()
方法中。