鍍金池/ 教程/ Python/ 自定義查找
編寫(xiě)自定義存儲(chǔ)系統(tǒng)
高級(jí)
編寫(xiě)你的第一個(gè)Django應(yīng)用,第5部分
視圖層
Django管理文檔生成器
編寫(xiě)你的第一個(gè) Django 程序 第3部分
編寫(xiě)你的第一個(gè)Django應(yīng)用,第6部分
模型層
中間件
測(cè)試
數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)優(yōu)化
文件上傳
中間件
驗(yàn)證器
基于類(lèi)的內(nèi)建通用視圖
部署靜態(tài)文件
使用Django認(rèn)證系統(tǒng)
高級(jí)教程:如何編寫(xiě)可重用的應(yīng)用
Model 類(lèi)參考
Django 初探
使用Django輸出PDF
模型的實(shí)例
模型
文件上傳
進(jìn)行原始的sql查詢(xún)
面向程序員
中間件
編寫(xiě)數(shù)據(jù)庫(kù)遷移
TemplateResponse 和 SimpleTemplateResponse
異常
Django中的測(cè)試
基礎(chǔ)
管理器
File對(duì)象
URL調(diào)度器
加密簽名
國(guó)際化和本地化
日志
查詢(xún)集
django-admin 和 manage.py
使用基于類(lèi)的視圖處理表單
聚合
內(nèi)建基于類(lèi)的視圖的API
如何使用會(huì)話(huà)
部署 Django
其它
其它核心功能
高級(jí)
Django中的密碼管理
模型元選項(xiàng)
按需內(nèi)容處理
查找 API 參考
高級(jí)
Django 的快捷函數(shù)
Django 的設(shè)置
Admin
開(kāi)發(fā)過(guò)程
新手入門(mén)
基于類(lèi)的視圖
模型實(shí)例參考
信號(hào)
表單素材 ( <code>Media</code> 類(lèi))
自定義查找
常見(jiàn)的網(wǎng)站應(yīng)用工具
模型
django.contrib.humanize
Django模版語(yǔ)言
點(diǎn)擊劫持保護(hù)
管理操作
編寫(xiě)你的第一個(gè) Django 程序 第2部分
Django安全
模式編輯器
多數(shù)據(jù)庫(kù)
部署
基于類(lèi)的視圖
內(nèi)建的視圖
視圖裝飾器
面向設(shè)計(jì)師
編寫(xiě)視圖
應(yīng)用程序
如何使用WSGI 部署
參考
表單 API
文件儲(chǔ)存API
認(rèn)證
國(guó)際化和本地化
錯(cuò)誤報(bào)告
基礎(chǔ)
基礎(chǔ)
將遺留數(shù)據(jù)庫(kù)整合到Django
教程
Django異常
編寫(xiě)你的第一個(gè) Django 程序 第4部分
遷移
分頁(yè)
重定向應(yīng)用
表單
從零開(kāi)始
為模型提供初始數(shù)據(jù)
設(shè)置
使用Django輸出CSV
關(guān)聯(lián)對(duì)象參考
使用表單
Django 中的用戶(hù)認(rèn)證
快速安裝指南
安全問(wèn)題歸檔
數(shù)據(jù)庫(kù)函數(shù)
編寫(xiě)自定義的django-admin命令
高級(jí)教程
管理文件
格式本地化
基于類(lèi)的通用視圖 —— 索引
安全
系統(tǒng)檢查框架
為Django編寫(xiě)首個(gè)補(bǔ)丁
模板層
Widgets
編寫(xiě)你的第一個(gè) Django 程序 第1部分
執(zhí)行查詢(xún)

自定義查找

New in Django 1.7.

Django為過(guò)濾提供了大量的內(nèi)建的查找(例如,exacticontains)。這篇文檔闡述了如何編寫(xiě)自定義查找,以及如何修改現(xiàn)存查找的功能。關(guān)于查找的API參考,詳見(jiàn)查找API參考。

一個(gè)簡(jiǎn)單的查找示例

讓我們從一個(gè)簡(jiǎn)單的自定義查找開(kāi)始。我們會(huì)編寫(xiě)一個(gè)自定義查找ne,提供和exact相反的功能。Author.objects.filter(name__ne='Jack')會(huì)轉(zhuǎn)換成下面的SQL:

"author"."name" <> 'Jack'

這條SQL是后端獨(dú)立的,所以我們并不需要擔(dān)心不同的數(shù)據(jù)庫(kù)。

實(shí)現(xiàn)它需要兩個(gè)步驟。首先我們需要實(shí)現(xiàn)這個(gè)查找,然后我們需要告訴Django它的信息。實(shí)現(xiàn)是十分簡(jiǎn)單直接的:

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

我們只需要在我們想讓查找應(yīng)用的字段上調(diào)用register_lookup,來(lái)注冊(cè)NotEqual查找。這種情況下,查找在所有Field的子類(lèi)都起作用,所以我們直接使用Field注冊(cè)它。

from django.db.models.fields import Field
Field.register_lookup(NotEqual)

也可以使用裝飾器模式來(lái)注冊(cè)查找:

from django.db.models.fields import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...
Changed in Django 1.8:

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

我們現(xiàn)在可以為任何foo字段使用 foo__ne。你需要確保在你嘗試創(chuàng)建使用它的任何查詢(xún)集之前完成注冊(cè)。你應(yīng)該把實(shí)現(xiàn)放在models.py文件中,或者在AppConfigready()方法中注冊(cè)查找。

現(xiàn)在讓我們深入觀(guān)察這個(gè)實(shí)現(xiàn),首先需要的屬性是lookup_name。這需要讓ORM理解如何去解釋name__ne,以及如何使用NotEqual來(lái)生成SQL。按照慣例,這些名字一般是只包含字母的小寫(xiě)字符串,但是唯一硬性的要求是不能夠包含字符串__。

然后我們需要定義as_sql方法。這個(gè)方法需要傳入一個(gè)SQLCompiler對(duì)象,叫做 compiler,以及活動(dòng)的數(shù)據(jù)庫(kù)連接。SQLCompiler對(duì)象并沒(méi)有記錄,但是我們需要知道的唯一一件事就是他們擁有compile()方法,這個(gè)方法返回一個(gè)元組,含有SQL字符串和要向字符串插入的參數(shù)。在多數(shù)情況下,你并不需要世界使用它,并且可以把它傳遞給process_lhs()process_rhs()。

Lookup作用于兩個(gè)值,lhs和rhs,分別是左邊和右邊。左邊的值一般是個(gè)字段的引用,但是它可以是任何實(shí)現(xiàn)了查詢(xún)表達(dá)式API的對(duì)象。右邊的值由用戶(hù)提供。在例子Author.objects.filter(name__ne='Jack')中,左邊的值是Author模型的name 字段的引用,右邊的值是'Jack'

我們可以調(diào)用 process_lhsprocess_rhs 來(lái)將它們轉(zhuǎn)換為我們需要的SQL值,使用之前我們描述的compiler 對(duì)象。

最后我們用<>將這些部分組合成SQL表達(dá)式,然后將所有參數(shù)用在查詢(xún)中。然后我們返回一個(gè)元組,包含生成的SQL字符串以及參數(shù)。

一個(gè)簡(jiǎn)單的轉(zhuǎn)換器示例

上面的自定義轉(zhuǎn)換器是極好的,但是一些情況下你可能想要把查找放在一起。例如,假設(shè)我們構(gòu)建一個(gè)應(yīng)用,想要利用abs() 操作符。我們有用一個(gè)Experiment模型,它記錄了起始值,終止值,以及變化量(起始值 - 終止值)。我們想要尋找所有變化量等于一個(gè)特定值的實(shí)驗(yàn)(Experiment.objects.filter(change__abs=27)),或者沒(méi)有達(dá)到指定值的實(shí)驗(yàn)(Experiment.objects.filter(change__abs__lt=27))。

注意

這個(gè)例子一定程度上很不自然,但是很好地展示了數(shù)據(jù)庫(kù)后端獨(dú)立的功能范圍,并且沒(méi)有重復(fù)實(shí)現(xiàn)Django中已有的功能。

我們從編寫(xiě)AbsoluteValue轉(zhuǎn)換器來(lái)開(kāi)始。這會(huì)用到SQL函數(shù)ABS(),來(lái)在比較之前轉(zhuǎn)換值。

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

接下來(lái),為IntegerField注冊(cè)它:

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

我們現(xiàn)在可以執(zhí)行之前的查詢(xún)。Experiment.objects.filter(change__abs=27)會(huì)生成下面的SQL:

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

通過(guò)使用Transform來(lái)替代Lookup,這說(shuō)明了我們能夠把以后更多的查找放到一起。所以Experiment.objects.filter(change__abs__lt=27)會(huì)生成以下的SQL:

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

注意在沒(méi)有指定其他查找的情況中,Django會(huì)將 change__abs=27 解釋為change__abs__exact=27。

當(dāng)尋找在 Transform之后,哪個(gè)查找可以使用的時(shí)候,Django使用output_field屬性。因?yàn)樗](méi)有修改,我們?cè)谶@里并不指定,但是假設(shè)我們?cè)谝恍┳侄紊蠎?yīng)用AbsoluteValue,這些字段代表了一個(gè)更復(fù)雜的類(lèi)型(比如說(shuō)與原點(diǎn)(origin)相關(guān)的一個(gè)點(diǎn),或者一個(gè)復(fù)數(shù)(complex number))。之后我們可能想指定,轉(zhuǎn)換要為進(jìn)一步的查找返回FloatField類(lèi)型。這可以通過(guò)向轉(zhuǎn)換添加output_field 屬性來(lái)實(shí)現(xiàn):

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

這確保了更進(jìn)一步的查找,像abs__lte的行為和對(duì)FloatField表現(xiàn)的一樣。

編寫(xiě)高效的 abs__lt 查找

當(dāng)我們使用上面編寫(xiě)的abs查找的時(shí)候,在一些情況下,生成的SQL并不會(huì)高效使用索引。尤其是我們使用change__abs__lt=27的時(shí)候,這等價(jià)于change__gt=-27 AND change__lt=27。(對(duì)于lte 的情況,我們可以使用 SQL子句BETWEEN)。

所以我們想讓Experiment.objects.filter(change__abs__lt=27)生成以下SQL:

SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

它的實(shí)現(xiàn)為:

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并不調(diào)用process_lhs()。而是它跳過(guò)了由AbsoluteValue完成的lhs,并且使用原始的lhs。這就是說(shuō),我們想要得到27 而不是ABS(27)。直接引用self.lhs.lhs是安全的,因?yàn)?AbsoluteValueLessThan只能夠通過(guò)AbsoluteValue查找來(lái)訪(fǎng)問(wèn),這就是說(shuō) lhs始終是AbsoluteValue的實(shí)例。

也要注意,就像兩邊都要在查詢(xún)中使用多次一樣,參數(shù)也需要多次包含lhs_paramsrhs_params。

最終的實(shí)現(xiàn)直接在數(shù)據(jù)庫(kù)中執(zhí)行了反轉(zhuǎn) (27變?yōu)?-27) 。這樣做的原因是如果self.rhs不是一個(gè)普通的整數(shù)值(比如是一個(gè)F()引用),我們?cè)赑ython中不能執(zhí)行這一轉(zhuǎn)換。

注意

實(shí)際上,大多數(shù)帶有__abs的查找都實(shí)現(xiàn)為這種范圍查詢(xún),并且在大多數(shù)數(shù)據(jù)庫(kù)后端中它更可能執(zhí)行成這樣,就像你可以利用索引一樣。然而在PostgreSQL中,你可能想要向abs(change) 中添加索引,這會(huì)使查詢(xún)更高效。

一個(gè)雙向轉(zhuǎn)換器的示例

我們之前討論的,AbsoluteValue的例子是一個(gè)只應(yīng)用在查找左側(cè)的轉(zhuǎn)換。可能有一些情況,你想要把轉(zhuǎn)換同時(shí)應(yīng)用在左側(cè)和右側(cè)。比如,你想過(guò)濾一個(gè)基于左右側(cè)相等比較操作的查詢(xún)集,在執(zhí)行一些SQL函數(shù)之后它們是大小寫(xiě)不敏感的。

讓我們測(cè)試一下這一大小寫(xiě)不敏感的轉(zhuǎn)換的簡(jiǎn)單示例。這個(gè)轉(zhuǎn)換在實(shí)踐中并不是十分有用,因?yàn)镈jango已經(jīng)自帶了一些自建的大小寫(xiě)不敏感的查找,但是它是一個(gè)很好的,數(shù)據(jù)庫(kù)無(wú)關(guān)的雙向轉(zhuǎn)換示例。

我們定義使用SQL 函數(shù)UPPER()UpperCase轉(zhuǎn)換器,來(lái)在比較前轉(zhuǎn)換這些值。我們定義了bilateral = True來(lái)表明轉(zhuǎn)換同時(shí)作用在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

接下來(lái),讓我們注冊(cè)它:

from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)

現(xiàn)在,查詢(xún)集Author.objects.filter(name__upper="doe")會(huì)生成像這樣的大小寫(xiě)不敏感查詢(xún):

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

為現(xiàn)存查找編寫(xiě)自動(dòng)的實(shí)現(xiàn)

有時(shí)不同的數(shù)據(jù)庫(kù)供應(yīng)商對(duì)于相同的操作需要不同的SQL。對(duì)于這個(gè)例子,我們會(huì)為MySQL重新編寫(xiě)一個(gè)自定義的,NotEqual操作的實(shí)現(xiàn)。我們會(huì)使用 != 而不是 <>操作符。(注意實(shí)際上幾乎所有數(shù)據(jù)庫(kù)都支持這兩個(gè),包括所有Django支持的官方數(shù)據(jù)庫(kù))。

我們可以通過(guò)創(chuàng)建帶有as_mysql方法的NotEqual的子類(lèi)來(lái)修改特定后端上的行為。

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中注冊(cè)它。它取代了原始的NotEqual類(lèi),由于它具有相同的lookup_name

當(dāng)編譯一個(gè)查詢(xún)的時(shí)候,Django首先尋找as_%s % connection.vendor方法,然后回退到 as_sql。內(nèi)建后端的供應(yīng)商名稱(chēng)是 sqlite,postgresql, oracle 和mysql。

Django如何決定使用查找還是轉(zhuǎn)換

有些情況下,你可能想要?jiǎng)討B(tài)修改基于傳遞進(jìn)來(lái)的名稱(chēng), Transform 或者 Lookup哪個(gè)會(huì)返回,而不是固定它。比如,你擁有可以?xún)?chǔ)存搭配( coordinate)或者任意一個(gè)維度(dimension)的字段,并且想讓類(lèi)似于.filter(coords__x7=4)的語(yǔ)法返回第七個(gè)搭配值為4的對(duì)象。為了這樣做,你可以用一些東西覆寫(xiě)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)

之后你應(yīng)該合理定義get_coordinate_lookup。來(lái)返回一個(gè) Lookup的子類(lèi),它處理dimension的相關(guān)值。

有一個(gè)名稱(chēng)相似的方法叫做get_transform()。get_lookup()應(yīng)該始終返回 Lookup 的子類(lèi),而get_transform() 返回Transform 的子類(lèi)。記住Transform 對(duì)象可以進(jìn)一步過(guò)濾,而 Lookup 對(duì)象不可以,這非常重要。

過(guò)濾的時(shí)候,如果還剩下只有一個(gè)查找名稱(chēng)要處理,它會(huì)尋找Lookup。如果有多個(gè)名稱(chēng),它會(huì)尋找Transform。在只有一個(gè)名稱(chēng)并且 Lookup找不到的情況下,會(huì)尋找Transform,之后尋找在Transform上面的exact查找。所有調(diào)用的語(yǔ)句都以一個(gè)Lookup結(jié)尾。解釋一下:

  • .filter(myfield__mylookup)會(huì)調(diào)用 myfield.get_lookup('mylookup')
  • .filter(myfield__mytransform__mylookup) 會(huì)調(diào)用 myfield.get_transform('mytransform'),然后調(diào)用mytransform.get_lookup('mylookup')
  • .filter(myfield__mytransform) 會(huì)首先調(diào)用 myfield.get_lookup('mytransform'),這樣會(huì)失敗,所以它會(huì)回退來(lái)調(diào)用 myfield.get_transform('mytransform') ,之后是 mytransform.get_lookup('exact')。

譯者:Django 文檔協(xié)作翻譯小組,原文:Custom lookups。

本文以 CC BY-NC-SA 3.0 協(xié)議發(fā)布,轉(zhuǎn)載請(qǐng)保留作者署名和文章出處。

Django 文檔協(xié)作翻譯小組人手緊缺,有興趣的朋友可以加入我們,完全公益性質(zhì)。交流群:467338606。

上一篇:使用Django輸出PDF下一篇:使用表單