New in Django 1.7.
Django為過濾提供了大量的內(nèi)建的查找(例如,exact
和icontains
)。這篇文檔闡述了如何編寫自定義查找,以及如何修改現(xiàn)存查找的功能。關(guān)于查找的API參考,詳見查找API參考。
讓我們從一個(gè)簡(jiǎn)單的自定義查找開始。我們會(huì)編寫一個(gè)自定義查找ne
,提供和exact
相反的功能。Author.objects.filter(name__ne='Jack')
會(huì)轉(zhuǎn)換成下面的SQL:
"author"."name" <> 'Jack'
這條SQL是后端獨(dú)立的,所以我們并不需要擔(dān)心不同的數(shù)據(jù)庫。
實(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
,來注冊(cè)NotEqual
查找。這種情況下,查找在所有Field
的子類都起作用,所以我們直接使用Field
注冊(cè)它。
from django.db.models.fields import Field
Field.register_lookup(NotEqual)
也可以使用裝飾器模式來注冊(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)建使用它的任何查詢集之前完成注冊(cè)。你應(yīng)該把實(shí)現(xiàn)放在models.py
文件中,或者在AppConfig
的ready()
方法中注冊(cè)查找。
現(xiàn)在讓我們深入觀察這個(gè)實(shí)現(xiàn),首先需要的屬性是lookup_name
。這需要讓ORM理解如何去解釋name__ne
,以及如何使用NotEqual
來生成SQL。按照慣例,這些名字一般是只包含字母的小寫字符串,但是唯一硬性的要求是不能夠包含字符串__
。
然后我們需要定義as_sql
方法。這個(gè)方法需要傳入一個(gè)SQLCompiler
對(duì)象,叫做 compiler
,以及活動(dòng)的數(shù)據(jù)庫連接。SQLCompiler
對(duì)象并沒有記錄,但是我們需要知道的唯一一件事就是他們擁有compile()
方法,這個(gè)方法返回一個(gè)元組,含有SQL字符串和要向字符串插入的參數(shù)。在多數(shù)情況下,你并不需要世界使用它,并且可以把它傳遞給process_lhs()
和 process_rhs()
。
Lookup
作用于兩個(gè)值,lhs和rhs,分別是左邊和右邊。左邊的值一般是個(gè)字段的引用,但是它可以是任何實(shí)現(xiàn)了查詢表達(dá)式API的對(duì)象。右邊的值由用戶提供。在例子Author.objects.filter(name__ne='Jack')
中,左邊的值是Author
模型的name
字段的引用,右邊的值是'Jack'
。
我們可以調(diào)用 process_lhs
和process_rhs
來將它們轉(zhuǎn)換為我們需要的SQL值,使用之前我們描述的compiler
對(duì)象。
最后我們用<>
將這些部分組合成SQL表達(dá)式,然后將所有參數(shù)用在查詢中。然后我們返回一個(gè)元組,包含生成的SQL字符串以及參數(shù)。
上面的自定義轉(zhuǎn)換器是極好的,但是一些情況下你可能想要把查找放在一起。例如,假設(shè)我們構(gòu)建一個(gè)應(yīng)用,想要利用abs()
操作符。我們有用一個(gè)Experiment
模型,它記錄了起始值,終止值,以及變化量(起始值 - 終止值)。我們想要尋找所有變化量等于一個(gè)特定值的實(shí)驗(yàn)(Experiment.objects.filter(change__abs=27)
),或者沒有達(dá)到指定值的實(shí)驗(yàn)(Experiment.objects.filter(change__abs__lt=27)
)。
注意
這個(gè)例子一定程度上很不自然,但是很好地展示了數(shù)據(jù)庫后端獨(dú)立的功能范圍,并且沒有重復(fù)實(shí)現(xiàn)Django中已有的功能。
我們從編寫AbsoluteValue
轉(zhuǎn)換器來開始。這會(huì)用到SQL函數(shù)ABS()
,來在比較之前轉(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
接下來,為IntegerField
注冊(cè)它:
from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)
我們現(xiàn)在可以執(zhí)行之前的查詢。Experiment.objects.filter(change__abs=27)
會(huì)生成下面的SQL:
SELECT ... WHERE ABS("experiments"."change") = 27
通過使用Transform
來替代Lookup
,這說明了我們能夠把以后更多的查找放到一起。所以Experiment.objects.filter(change__abs__lt=27)
會(huì)生成以下的SQL:
SELECT ... WHERE ABS("experiments"."change") < 27
注意在沒有指定其他查找的情況中,Django會(huì)將 change__abs=27
解釋為change__abs__exact=27
。
當(dāng)尋找在 Transform
之后,哪個(gè)查找可以使用的時(shí)候,Django使用output_field
屬性。因?yàn)樗]有修改,我們?cè)谶@里并不指定,但是假設(shè)我們?cè)谝恍┳侄紊蠎?yīng)用AbsoluteValue,這些字段代表了一個(gè)更復(fù)雜的類型(比如說與原點(diǎn)(origin)相關(guān)的一個(gè)點(diǎn),或者一個(gè)復(fù)數(shù)(complex number))。之后我們可能想指定,轉(zhuǎn)換要為進(jìn)一步的查找返回FloatField
類型。這可以通過向轉(zhuǎn)換添加output_field
屬性來實(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)的一樣。
abs__lt
查找當(dāng)我們使用上面編寫的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()
。而是它跳過了由AbsoluteValue
完成的lhs
,并且使用原始的lhs
。這就是說,我們想要得到27
而不是ABS(27)
。直接引用self.lhs.lhs
是安全的,因?yàn)?AbsoluteValueLessThan
只能夠通過AbsoluteValue
查找來訪問,這就是說 lhs
始終是AbsoluteValue
的實(shí)例。
也要注意,就像兩邊都要在查詢中使用多次一樣,參數(shù)也需要多次包含lhs_params
和rhs_params
。
最終的實(shí)現(xiàn)直接在數(shù)據(jù)庫中執(zhí)行了反轉(zhuǎn) (27變?yōu)?-27) 。這樣做的原因是如果self.rhs
不是一個(gè)普通的整數(shù)值(比如是一個(gè)F()
引用),我們?cè)赑ython中不能執(zhí)行這一轉(zhuǎn)換。
注意
實(shí)際上,大多數(shù)帶有__abs的查找都實(shí)現(xiàn)為這種范圍查詢,并且在大多數(shù)數(shù)據(jù)庫后端中它更可能執(zhí)行成這樣,就像你可以利用索引一樣。然而在PostgreSQL中,你可能想要向abs(change) 中添加索引,這會(huì)使查詢更高效。
我們之前討論的,AbsoluteValue
的例子是一個(gè)只應(yīng)用在查找左側(cè)的轉(zhuǎn)換。可能有一些情況,你想要把轉(zhuǎn)換同時(shí)應(yīng)用在左側(cè)和右側(cè)。比如,你想過濾一個(gè)基于左右側(cè)相等比較操作的查詢集,在執(zhí)行一些SQL函數(shù)之后它們是大小寫不敏感的。
讓我們測(cè)試一下這一大小寫不敏感的轉(zhuǎn)換的簡(jiǎn)單示例。這個(gè)轉(zhuǎn)換在實(shí)踐中并不是十分有用,因?yàn)镈jango已經(jīng)自帶了一些自建的大小寫不敏感的查找,但是它是一個(gè)很好的,數(shù)據(jù)庫無關(guān)的雙向轉(zhuǎn)換示例。
我們定義使用SQL 函數(shù)UPPER()
的UpperCase
轉(zhuǎn)換器,來在比較前轉(zhuǎn)換這些值。我們定義了bilateral = True
來表明轉(zhuǎn)換同時(shí)作用在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
接下來,讓我們注冊(cè)它:
from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)
現(xiàn)在,查詢集Author.objects.filter(name__upper="doe")
會(huì)生成像這樣的大小寫不敏感查詢:
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
有時(shí)不同的數(shù)據(jù)庫供應(yīng)商對(duì)于相同的操作需要不同的SQL。對(duì)于這個(gè)例子,我們會(huì)為MySQL重新編寫一個(gè)自定義的,NotEqual
操作的實(shí)現(xiàn)。我們會(huì)使用 !=
而不是 <>
操作符。(注意實(shí)際上幾乎所有數(shù)據(jù)庫都支持這兩個(gè),包括所有Django支持的官方數(shù)據(jù)庫)。
我們可以通過創(chuàng)建帶有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
中注冊(cè)它。它取代了原始的NotEqual
類,由于它具有相同的lookup_name
。
當(dāng)編譯一個(gè)查詢的時(shí)候,Django首先尋找as_%s % connection.vendor
方法,然后回退到 as_sql
。內(nèi)建后端的供應(yīng)商名稱是 sqlite,postgresql, oracle 和mysql。
有些情況下,你可能想要?jiǎng)討B(tài)修改基于傳遞進(jìn)來的名稱, Transform
或者 Lookup
哪個(gè)會(huì)返回,而不是固定它。比如,你擁有可以儲(chǔ)存搭配( coordinate)或者任意一個(gè)維度(dimension)的字段,并且想讓類似于.filter(coords__x7=4)
的語法返回第七個(gè)搭配值為4的對(duì)象。為了這樣做,你可以用一些東西覆寫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
。來返回一個(gè) Lookup
的子類,它處理dimension
的相關(guān)值。
有一個(gè)名稱相似的方法叫做get_transform()
。get_lookup()
應(yīng)該始終返回 Lookup
的子類,而get_transform()
返回Transform
的子類。記住Transform
對(duì)象可以進(jìn)一步過濾,而 Lookup
對(duì)象不可以,這非常重要。
過濾的時(shí)候,如果還剩下只有一個(gè)查找名稱要處理,它會(huì)尋找Lookup
。如果有多個(gè)名稱,它會(huì)尋找Transform
。在只有一個(gè)名稱并且 Lookup找不到的情況下,會(huì)尋找Transform
,之后尋找在Transform
上面的exact
查找。所有調(diào)用的語句都以一個(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ì)回退來調(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。