1. 程式人生 > >django 1.8 官方文件翻譯: 2-5-7 自定義查詢

django 1.8 官方文件翻譯: 2-5-7 自定義查詢

自定義查詢

New in Django 1.7.

Django為過濾提供了大量的內建的查詢(例如,exacticontains)。這篇文件闡述瞭如何編寫自定義查詢,以及如何修改現存查詢的功能。關於查詢的API參考,詳見查詢API參考。

一個簡單的查詢示例

讓我們從一個簡單的自定義查詢開始。我們會編寫一個自定義查詢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

我們只需要在我們想讓查詢應用的欄位上呼叫register_lookup,來註冊NotEqual查詢。這種情況下,查詢在所有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):
# ...
Changed in Django 1.8:

新增了使用裝飾器模式的能力。

我們現在可以為任何foo欄位使用 foo__ne。你需要確保在你嘗試建立使用它的任何查詢集之前完成註冊。你應該把實現放在models.py檔案中,或者在AppConfigready()方法中註冊查詢。

現在讓我們深入觀察這個實現,首先需要的屬性是lookup_name。這需要讓ORM理解如何去解釋name__ne,以及如何使用NotEqual來生成SQL。按照慣例,這些名字一般是隻包含字母的小寫字串,但是唯一硬性的要求是不能夠包含字串__

然後我們需要定義as_sql方法。這個方法需要傳入一個SQLCompiler物件,叫做 compiler,以及活動的資料庫連線。SQLCompiler物件並沒有記錄,但是我們需要知道的唯一一件事就是他們擁有compile()方法,這個方法返回一個元組,含有SQL字串和要向字串插入的引數。在多數情況下,你並不需要世界使用它,並且可以把它傳遞給process_lhs()process_rhs()

Lookup作用於兩個值,lhs和rhs,分別是左邊和右邊。左邊的值一般是個欄位的引用,但是它可以是任何實現了查詢表示式API的物件。右邊的值由使用者提供。在例子Author.objects.filter(name__ne='Jack')中,左邊的值是Author模型的name 欄位的引用,右邊的值是'Jack'

我們可以呼叫 process_lhsprocess_rhs 來將它們轉換為我們需要的SQL值,使用之前我們描述的compiler 物件。

最後我們用<>將這些部分組合成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'

    def as_sql(self, compiler, connection):
        lhs, params = compiler.compile(self.lhs)
        return "ABS(%s)" % lhs, params

接下來,為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

當尋找在 Transform之後,哪個查詢可以使用的時候,Django使用output_field屬性。因為它並沒有修改,我們在這裡並不指定,但是假設我們在一些欄位上應用AbsoluteValue,這些欄位代表了一個更復雜的型別(比如說與原點(origin)相關的一個點,或者一個複數(complex number))。之後我們可能想指定,轉換要為進一步的查詢返回FloatField型別。這可以通過向轉換新增output_field 屬性來實現:

from django.db.models import FloatField, Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'

    def as_sql(self, compiler, connection):
        lhs, params = compiler.compile(self.lhs)
        return "ABS(%s)" % lhs, params

    @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。這就是說,我們想要得到27 而不是ABS(27)。直接引用self.lhs.lhs是安全的,因為 AbsoluteValueLessThan只能夠通過AbsoluteValue查詢來訪問,這就是說 lhs始終是AbsoluteValue的例項。

也要注意,就像兩邊都要在查詢中使用多次一樣,引數也需要多次包含lhs_paramsrhs_params

最終的實現直接在資料庫中執行了反轉 (27變為 -27) 。這樣做的原因是如果self.rhs不是一個普通的整數值(比如是一個F()引用),我們在Python中不能執行這一轉換。

注意

實際上,大多數帶有__abs的查詢都實現為這種範圍查詢,並且在大多數資料庫後端中它更可能執行成這樣,就像你可以利用索引一樣。然而在PostgreSQL中,你可能想要向abs(change) 中新增索引,這會使查詢更高效。

一個雙向轉換器的示例

我們之前討論的,AbsoluteValue的例子是一個只應用在查詢左側的轉換。可能有一些情況,你想要把轉換同時應用在左側和右側。比如,你想過濾一個基於左右側相等比較操作的查詢集,在執行一些SQL函式之後它們是大小寫不敏感的。

讓我們測試一下這一大小寫不敏感的轉換的簡單示例。這個轉換在實踐中並不是十分有用,因為Django已經自帶了一些自建的大小寫不敏感的查詢,但是它是一個很好的,資料庫無關的雙向轉換示例。

我們定義使用SQL 函式UPPER()UpperCase轉換器,來在比較前轉換這些值。我們定義了bilateral = True來表明轉換同時作用在lhsrhs上面:

from django.db.models import Transform

class UpperCase(Transform):
    lookup_name = 'upper'
    bilateral = True

    def as_sql(self, compiler, connection):
        lhs, params = compiler.compile(self.lhs)
        return "UPPER(%s)" % lhs, params

接下來,讓我們註冊它:

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')

為現存查詢編寫自動的實現

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

我們可以通過建立帶有as_mysql方法的NotEqual的子類來修改特定後端上的行為。

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。內建後端的供應商名稱是 sqlite,postgresql, oracle 和mysql。

Django如何決定使用查詢還是轉換

有些情況下,你可能想要動態修改基於傳遞進來的名稱, Transform 或者 Lookup哪個會返回,而不是固定它。比如,你擁有可以儲存搭配( coordinate)或者任意一個維度(dimension)的欄位,並且想讓類似於.filter(coords__x7=4)的語法返回第七個搭配值為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
            finally:
                return get_coordinate_lookup(dimension)
        return super(CoordinatesField, self).get_lookup(lookup_name)

之後你應該合理定義get_coordinate_lookup。來返回一個 Lookup的子類,它處理dimension的相關值。

有一個名稱相似的方法叫做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')