如何撰寫自訂查詢¶
Django 提供多種 內建查詢 來進行篩選 (例如,exact
和 icontains
)。本文件說明如何撰寫自訂查詢以及如何變更現有查詢的運作方式。如需查詢的 API 參考,請參閱 查詢 API 參考。
查詢範例¶
我們先從一個小的自訂查詢開始。我們將撰寫一個與 exact
相反的自訂查詢 ne
。Author.objects.filter(name__ne='Jack')
會轉換為 SQL
"author"."name" <> 'Jack'
此 SQL 與後端無關,因此我們不需要擔心不同的資料庫。
要讓這個運作,需要兩個步驟。首先,我們需要實作查詢,然後我們需要告訴 Django 關於它的資訊
from django.db.models import Lookup
class NotEqual(Lookup):
lookup_name = "ne"
def as_sql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return "%s <> %s" % (lhs, rhs), params
為了註冊 NotEqual
查詢,我們需要在我們希望查詢可用的欄位類別上呼叫 register_lookup
。在這種情況下,查詢適用於所有 Field
子類別,因此我們直接向 Field
註冊
from django.db.models import Field
Field.register_lookup(NotEqual)
也可以使用裝飾器模式完成查詢註冊
from django.db.models import Field
@Field.register_lookup
class NotEqualLookup(Lookup): ...
現在,我們可以在任何欄位 foo
上使用 foo__ne
。您需要確保此註冊發生在您嘗試使用它建立任何查詢集之前。您可以將實作放在 models.py
檔案中,或在 AppConfig
的 ready()
方法中註冊查詢。
仔細研究實作,第一個必需的屬性是 lookup_name
。這讓 ORM 能夠理解如何解讀 name__ne
並使用 NotEqual
來產生 SQL。依照慣例,這些名稱始終是僅包含字母的小寫字串,但唯一的要求是它不得包含字串 __
。
然後,我們需要定義 as_sql
方法。這會取得一個名為 compiler
的 SQLCompiler
物件,以及作用中的資料庫連線。SQLCompiler
物件沒有文件記錄,但我們唯一需要知道的是它們有一個 compile()
方法,該方法會傳回一個包含 SQL 字串和要插入該字串的參數的元組。在大多數情況下,您不需要直接使用它,並且可以將其傳遞給 process_lhs()
和 process_rhs()
。
Lookup
使用兩個值 lhs
和 rhs
,分別代表左手邊和右手邊。左手邊通常是欄位參考,但它可以是任何實作 查詢表達式 API 的東西。右手邊是使用者給定的值。在範例 Author.objects.filter(name__ne='Jack')
中,左手邊是對 Author
模型的 name
欄位的參考,而 'Jack'
是右手邊。
我們呼叫 process_lhs
和 process_rhs
,使用之前描述的 compiler
物件將它們轉換為我們需要用於 SQL 的值。這些方法傳回包含一些 SQL 和要插入該 SQL 的參數的元組,就像我們需要從 as_sql
方法傳回的一樣。在上面的範例中,process_lhs
傳回 ('"author"."name"', [])
,而 process_rhs
傳回 ('"%s"', ['Jack'])
。在此範例中,左手邊沒有參數,但這取決於我們擁有的物件,因此我們仍然需要將它們包含在我們傳回的參數中。
最後,我們將這些部分與 <>
合併成 SQL 表達式,並提供查詢的所有參數。然後,我們傳回一個包含產生的 SQL 字串和參數的元組。
轉換器範例¶
上面的自訂查詢很棒,但在某些情況下,您可能希望能夠將查詢連結在一起。例如,假設我們正在建置一個應用程式,我們想要使用 abs()
運算子。我們有一個 Experiment
模型,該模型記錄開始值、結束值和變更 (開始 - 結束)。我們想要尋找所有變更等於特定金額的實驗 (Experiment.objects.filter(change__abs=27)
),或是不超過特定金額的實驗 (Experiment.objects.filter(change__abs__lt=27)
)。
請注意
此範例有些牽強,但它很好地示範了在資料庫後端獨立的方式中可能實現的功能範圍,且無需複製 Django 中已有的功能。
我們將從撰寫 AbsoluteValue
轉換器開始。這將使用 SQL 函數 ABS()
在比較之前轉換值
from django.db.models import Transform
class AbsoluteValue(Transform):
lookup_name = "abs"
function = "ABS"
接下來,讓我們為 IntegerField
註冊它
from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)
我們現在可以執行我們之前擁有的查詢。Experiment.objects.filter(change__abs=27)
將產生下列 SQL
SELECT ... WHERE ABS("experiments"."change") = 27
透過使用 Transform
而不是 Lookup
,表示我們可以稍後連結進一步的查詢。因此,Experiment.objects.filter(change__abs__lt=27)
將產生下列 SQL
SELECT ... WHERE ABS("experiments"."change") < 27
請注意,如果沒有指定其他查詢,Django 會將 change__abs=27
解釋為 change__abs__exact=27
。
這也允許結果在 ORDER BY
和 DISTINCT ON
子句中使用。例如,Experiment.objects.order_by('change__abs')
產生
SELECT ... ORDER BY ABS("experiments"."change") ASC
在支援欄位 DISTINCT ON 的資料庫 (例如 PostgreSQL) 上,Experiment.objects.distinct('change__abs')
產生
SELECT ... DISTINCT ON ABS("experiments"."change")
在尋找套用 Transform
後允許的查詢時,Django 使用 output_field
屬性。我們這裡不需要指定它,因為它沒有改變,但假設我們將 AbsoluteValue
套用至表示更複雜類型的某些欄位 (例如,相對於原點的點,或複數),那麼我們可能希望指定轉換傳回 FloatField
類型以進行進一步查詢。這可以透過將 output_field
屬性新增至轉換來完成
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = "abs"
function = "ABS"
@property
def output_field(self):
return FloatField()
這可確保像 abs__lte
這樣的進一步查詢,其行為與 FloatField
的行為相同。
撰寫有效的 abs__lt
查詢¶
當使用上述寫好的 abs
查找時,在某些情況下,產生的 SQL 不會有效率地使用索引。特別是,當我們使用 change__abs__lt=27
時,這等同於 change__gt=-27
AND change__lt=27
。(對於 lte
的情況,我們可以使用 SQL BETWEEN
)。
因此,我們希望 Experiment.objects.filter(change__abs__lt=27)
產生以下 SQL:
SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27
實作方式如下:
from django.db.models import Lookup
class AbsoluteValueLessThan(Lookup):
lookup_name = "lt"
def as_sql(self, compiler, connection):
lhs, lhs_params = compiler.compile(self.lhs.lhs)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params + lhs_params + rhs_params
return "%s < %s AND %s > -%s" % (lhs, rhs, lhs, rhs), params
AbsoluteValue.register_lookup(AbsoluteValueLessThan)
這裡有幾個值得注意的地方。首先,AbsoluteValueLessThan
沒有呼叫 process_lhs()
。相反地,它跳過了 AbsoluteValue
對 lhs
的轉換,並使用原始的 lhs
。也就是說,我們希望取得 "experiments"."change"
,而不是 ABS("experiments"."change")
。直接參照 self.lhs.lhs
是安全的,因為 AbsoluteValueLessThan
只能從 AbsoluteValue
查找中存取,也就是說 lhs
始終是 AbsoluteValue
的實例。
另請注意,由於查詢中兩側都多次使用,因此參數需要多次包含 lhs_params
和 rhs_params
。
最終的查詢直接在資料庫中執行反轉(將 27
變成 -27
)。這樣做的原因是,如果 self.rhs
不是一個純粹的整數值(例如 F()
參考),我們就無法在 Python 中執行轉換。
請注意
事實上,大多數帶有 __abs
的查找都可以像這樣實作為範圍查詢,並且在大多數資料庫後端上,這樣做可能更合理,因為你可以利用索引。但是,對於 PostgreSQL,你可能需要在 abs(change)
上新增索引,這將使這些查詢非常有效率。
雙邊轉換器的範例¶
我們之前討論的 AbsoluteValue
範例是一種應用於查找左側的轉換。在某些情況下,你可能希望將轉換同時應用於左側和右側。例如,如果你想根據左側和右側是否相等來篩選 queryset,而不區分某些 SQL 函數。
讓我們在這裡檢視不區分大小寫的轉換。這種轉換在實踐中不是很有用,因為 Django 已經內建了許多不區分大小寫的查找,但它將很好地示範以資料庫無關的方式進行雙邊轉換。
我們定義一個 UpperCase
轉換器,它使用 SQL 函數 UPPER()
來轉換比較前的值。我們定義 bilateral = True
以指示此轉換應同時應用於 lhs
和 rhs
。
from django.db.models import Transform
class UpperCase(Transform):
lookup_name = "upper"
function = "UPPER"
bilateral = True
接下來,讓我們註冊它
from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)
現在,queryset Author.objects.filter(name__upper="doe")
將產生類似這樣的不區分大小寫的查詢:
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
為現有的查找編寫替代實作方式¶
有時,不同的資料庫供應商對於相同的操作需要不同的 SQL。在此範例中,我們將為 MySQL 重寫 NotEqual 運算子的自訂實作。我們將使用 !=
運算子,而不是 <>
。(請注意,實際上幾乎所有資料庫都支援這兩者,包括 Django 支援的所有官方資料庫)。
我們可以透過建立具有 as_mysql
方法的 NotEqual
子類別來變更特定後端的行為。
class MySQLNotEqual(NotEqual):
def as_mysql(self, compiler, connection, **extra_context):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return "%s != %s" % (lhs, rhs), params
Field.register_lookup(MySQLNotEqual)
然後,我們可以將其與 Field
註冊。它會取代原始的 NotEqual
類別,因為它具有相同的 lookup_name
。
在編譯查詢時,Django 會先尋找 as_%s % connection.vendor
方法,然後回退到 as_sql
。內建後端的供應商名稱為 sqlite
、postgresql
、oracle
和 mysql
。
Django 如何決定使用的查找和轉換¶
在某些情況下,你可能希望根據傳入的名稱動態變更傳回的 Transform
或 Lookup
,而不是固定它。例如,你可以有一個欄位儲存座標或任意維度,並希望允許像 .filter(coords__x7=4)
這樣的語法,以傳回第 7 個座標值為 4 的物件。為了做到這一點,你需要使用類似以下的內容覆寫 get_lookup
:
class CoordinatesField(Field):
def get_lookup(self, lookup_name):
if lookup_name.startswith("x"):
try:
dimension = int(lookup_name.removeprefix("x"))
except ValueError:
pass
else:
return get_coordinate_lookup(dimension)
return super().get_lookup(lookup_name)
然後,你將適當地定義 get_coordinate_lookup
,以傳回處理 dimension
相關值的 Lookup
子類別。
有一個類似名稱的方法叫做 get_transform()
。get_lookup()
應始終傳回 Lookup
子類別,而 get_transform()
則傳回 Transform
子類別。請務必記住,Transform
物件可以進一步篩選,而 Lookup
物件則不行。
在篩選時,如果只剩下一個要解析的查找名稱,我們將尋找 Lookup
。如果有複數個名稱,則將尋找 Transform
。在只有一個名稱且未找到 Lookup
的情況下,我們會尋找 Transform
,然後再尋找該 Transform
上的 exact
查找。所有呼叫序列始終以 Lookup
結束。為了闡明:
.filter(myfield__mylookup)
將呼叫myfield.get_lookup('mylookup')
。.filter(myfield__mytransform__mylookup)
將呼叫myfield.get_transform('mytransform')
,然後呼叫mytransform.get_lookup('mylookup')
。.filter(myfield__mytransform)
將首先呼叫myfield.get_lookup('mytransform')
,如果失敗,它將回退到呼叫myfield.get_transform('mytransform')
,然後呼叫mytransform.get_lookup('exact')
。