如何撰寫自訂查詢

Django 提供多種 內建查詢 來進行篩選 (例如,exacticontains)。本文件說明如何撰寫自訂查詢以及如何變更現有查詢的運作方式。如需查詢的 API 參考,請參閱 查詢 API 參考

查詢範例

我們先從一個小的自訂查詢開始。我們將撰寫一個與 exact 相反的自訂查詢 neAuthor.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 檔案中,或在 AppConfigready() 方法中註冊查詢。

仔細研究實作,第一個必需的屬性是 lookup_name。這讓 ORM 能夠理解如何解讀 name__ne 並使用 NotEqual 來產生 SQL。依照慣例,這些名稱始終是僅包含字母的小寫字串,但唯一的要求是它不得包含字串 __

然後,我們需要定義 as_sql 方法。這會取得一個名為 compilerSQLCompiler 物件,以及作用中的資料庫連線。SQLCompiler 物件沒有文件記錄,但我們唯一需要知道的是它們有一個 compile() 方法,該方法會傳回一個包含 SQL 字串和要插入該字串的參數的元組。在大多數情況下,您不需要直接使用它,並且可以將其傳遞給 process_lhs()process_rhs()

Lookup 使用兩個值 lhsrhs,分別代表左手邊和右手邊。左手邊通常是欄位參考,但它可以是任何實作 查詢表達式 API 的東西。右手邊是使用者給定的值。在範例 Author.objects.filter(name__ne='Jack') 中,左手邊是對 Author 模型的 name 欄位的參考,而 'Jack' 是右手邊。

我們呼叫 process_lhsprocess_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 BYDISTINCT 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()。相反地,它跳過了 AbsoluteValuelhs 的轉換,並使用原始的 lhs。也就是說,我們希望取得 "experiments"."change",而不是 ABS("experiments"."change")。直接參照 self.lhs.lhs 是安全的,因為 AbsoluteValueLessThan 只能從 AbsoluteValue 查找中存取,也就是說 lhs 始終是 AbsoluteValue 的實例。

另請注意,由於查詢中兩側都多次使用,因此參數需要多次包含 lhs_paramsrhs_params

最終的查詢直接在資料庫中執行反轉(將 27 變成 -27)。這樣做的原因是,如果 self.rhs 不是一個純粹的整數值(例如 F() 參考),我們就無法在 Python 中執行轉換。

請注意

事實上,大多數帶有 __abs 的查找都可以像這樣實作為範圍查詢,並且在大多數資料庫後端上,這樣做可能更合理,因為你可以利用索引。但是,對於 PostgreSQL,你可能需要在 abs(change) 上新增索引,這將使這些查詢非常有效率。

雙邊轉換器的範例

我們之前討論的 AbsoluteValue 範例是一種應用於查找左側的轉換。在某些情況下,你可能希望將轉換同時應用於左側和右側。例如,如果你想根據左側和右側是否相等來篩選 queryset,而不區分某些 SQL 函數。

讓我們在這裡檢視不區分大小寫的轉換。這種轉換在實踐中不是很有用,因為 Django 已經內建了許多不區分大小寫的查找,但它將很好地示範以資料庫無關的方式進行雙邊轉換。

我們定義一個 UpperCase 轉換器,它使用 SQL 函數 UPPER() 來轉換比較前的值。我們定義 bilateral = True 以指示此轉換應同時應用於 lhsrhs

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。內建後端的供應商名稱為 sqlitepostgresqloraclemysql

Django 如何決定使用的查找和轉換

在某些情況下,你可能希望根據傳入的名稱動態變更傳回的 TransformLookup,而不是固定它。例如,你可以有一個欄位儲存座標或任意維度,並希望允許像 .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')

返回頂部