天天看點

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

自定義查找

New in Django 1.7.           

Django為過濾提供了大量的内建的查找(例如,

exact

icontains

)。這篇文檔闡述了如何編寫自定義查找,以及如何修改現存查找的功能。關于查找的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

檔案中,或者在

AppConfig

ready()

方法中注冊查找。

現在讓我們深入觀察這個實作,首先需要的屬性是

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_lhs

process_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_params

rhs_params

最終的實作直接在資料庫中執行了反轉 (27變為 -27) 。這樣做的原因是如果

self.rhs

不是一個普通的整數值(比如是一個

F()

引用),我們在Python中不能執行這一轉換。

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

一個雙向轉換器的示例

我們之前讨論的,

AbsoluteValue

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

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

我們定義使用SQL 函數

UPPER()

UpperCase

轉換器,來在比較前轉換這些值。我們定義了

bilateral = True

來表明轉換同時作用在

lhs

rhs

上面:

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

譯者: Django 文檔協作翻譯小組 ,原文: Custom lookups 本文以 CC BY-NC-SA 3.0 協定釋出,轉載請保留作者署名和文章出處。 人手緊缺,有興趣的朋友可以加入我們,完全公益性質。交流群:467338606。