1. 程式人生 > >Django 2.1.3 模型層 自定義查詢

Django 2.1.3 模型層 自定義查詢

自定義查詢

Django提供了各種各樣的用於過濾的內建查詢(例如,exacticontains)。 本文件解釋瞭如何編寫自定義查詢以及如何更改已有查詢的工作方式。 請參閱有關lookup的API參考

1.一個簡單的查詢示例

讓我們從一個簡單的自定義查詢開始。我們編寫一個自定義查詢 ne

,它與 exact 相反。 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.fields import Field
Field.register_lookup(NotEqual)

查詢註冊也可以用修飾模式來完成

from django.db.models.fields import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...

現在我們可以用foo__ne來代表foo的任意欄位。你需要確保在建立任意的queryset之前使用它。
(1) 你可以在models.py檔案內設定它
(2) 或者在“AppConfig”內使用ready()方法註冊它。

1.1 具體步驟

(1)定義 lookup_name 屬性
仔細觀察實現過程,最開始我們需要“lookup_name”這個屬性。這個可以保證ORM理解如何編譯“name__ne”和使用“NotEqual”來建立結構化查詢語言SQL。按照慣例,這些名字“name_ne”是小寫字母字串,但是很麻煩的是必須有“__”字串
(2)定義as_sql 方法
之後我們需要定義一個“as_sql”方法。這方法需要一個“SQLCompiler” 物件, 被叫做編譯器,和一個有效的資料庫連線。“SQLCompller”物件沒有文件,我們只需要知道它有一個compile()方法可以返回一個元組包括SQL字串,和插入這個字串的引數。大部分情況下,你不需要直接使用這個物件你可以把它傳送給“process_lhs()”和“process_rhs()

“Lookup”工作依靠兩個值, “lhs”和“rhs”,代表左右兩邊,左邊是一個欄位參考,但它可以是實現了query expression API的任何東西。右邊是一個使用者給的數值。舉個例子:Author.objects.filter(name__ne='Jack'),左邊是一個引用Author模型的name欄位的東西,“Jack”是右邊。

我們呼叫“process_lhs”和“process_rhs”轉化他們成為我們想要的用來檢索的值通過之前我們提到的“編譯器”。這個方法返回一個元組包含SQL資料庫和插入SQL資料庫一些引數,剛好就是我們‘as_sql’需要返回的。使用前面的例子,“process_lhs”返回('"author"."name"', []) ,“process_rhs”返回('"%s"', ['Jack']).在這個例子裡面沒有左手邊的引數,但是這需要看情況而定,我們還需要包括這些引數當我們返回的時候。

最後,我們將這些部分組合成一個帶有<>的SQL表示式,並提供查詢的所有引數。 然後我們返回一個包含生成的SQL字串和引數的元組。

效果圖
(1)資料庫:
在這裡插入圖片描述
(2)自定義Lookup 之後
在這裡插入圖片描述

2.簡單的轉換器示例

上面的自定義查詢沒問題,但在某些情況下,您可能希望能夠將一些查詢連結在一起。 例如,假設我們正在構建一個我們想要製作一個帶有abs()運算子的應用程式。 我們有一個Experiment模型,它記錄start值,end值和change值(start - end)。 我們想找到所有在Experiment模型中change屬性等於一定數量的(Experiment.objects.filter(change__abs=27)),或者在Experiment模型中change屬性沒有超過一定數量的(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'

下一步, 讓我們為其註冊 IntrgerField:

from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)

我們現在可以執行之前的查詢。 Experiment.objects.filter(change__abs = 27)將生成以下SQL

SELECT ... WHERE ABS("experiments"."change") = 27

譯者注,效果圖如下:
(1)資料庫
在這裡插入圖片描述
(2)呼叫Transform
在這裡插入圖片描述


通過使用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

在支援欄位去重的資料庫(例如PostgreSQL)上,語句Experiment.objects.distinct('change__abs')會生成:

SELECT ... DISTINCT ON ABS("experiments"."change")

Django 2.1中的更改:
上兩段所提到的排序與去重的支援被加入了。↑

當我們在應用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一致。

3.編寫一個高效的 abs__lt 查詢

當使用上面寫的abs查詢時,生成的SQL在某些情況下不會有效地使用索引。 特別是,當我們使用change__abs__lt = 27時,這相當於change__gt = -27change__lt = 27。 (對於lte情況,我們可以使用SQLBETWEEN)。

因此, 我們希望 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),以使這些查詢非常高效。↑

4.Transformer 雙向示例

我們之前討論的AbsoluteValue示例是一個適用於查詢左側的轉換。在某些情況下,您可能希望將轉換應用於左側和右側。例如,如果要根據左側和右側的相等性對某個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)

現在,這個Author.objects.filter(name__upper =“doe”)查詢集會生成一個像這樣的不區分大小寫的查詢:

SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

5.為現有查詢的關係編寫一個代替實現

有時,不同的資料庫供應商對同一操作需要不同的SQL。對於此示例,我們將為NotEqual運算子重寫MySQL的自定義實現。我們將使用!= 運算子而不是<>。(請注意,實際上幾乎所有資料庫都支援這兩種運算子,包括Django支援的所有官方資料庫)。

我們可以通過NotEqual使用as_mysql方法建立子類來更改特定後端的行為 :

class MySQLNotEqual(NotEqual):
    def as_mysql(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

Field.register_lookup(MySQLNotEqual)

然後我們可以註冊它Field。它取代了原始 NotEqual類,因為它具有相同的功能lookup_name。

在編譯查詢時,Django首先查詢as_%s % connection.vendor方法,然後再回到as_sql。對於內建後端的vendor名稱有sqlite,postgresql,mysql和oracle。

6.Django如何確定使用Lookup還是Transforms

在某些情況下,您可能希望根據傳入的名稱動態更改哪個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[1:])
            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’)。