# 自定義查找 #
```
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 文檔協作翻譯小組](http://python.usyiyi.cn/django/index.html),原文:[Custom lookups](https://docs.djangoproject.com/en/1.8/howto/custom-lookups/)。
>
> 本文以 [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/cn/) 協議發布,轉載請保留作者署名和文章出處。
>
> [Django 文檔協作翻譯小組](http://python.usyiyi.cn/django/index.html)人手緊缺,有興趣的朋友可以加入我們,完全公益性質。交流群:467338606。
- 新手入門
- 從零開始
- 概覽
- 安裝
- 教程
- 第1部分:模型
- 第2部分:管理站點
- 第3部分:視圖和模板
- 第4部分:表單和通用視圖
- 第5部分:測試
- 第6部分:靜態文件
- 高級教程
- 如何編寫可重用的應用
- 為Django編寫首個補丁
- 模型層
- 模型
- 模型語法
- 元選項
- 模型類
- 查詢集
- 執行查詢
- 查找表達式
- 模型的實例
- 實例方法
- 訪問關聯對象
- 遷移
- 模式編輯器
- 編寫遷移
- 高級
- 管理器
- 原始的SQL查詢
- 聚合
- 多數據庫
- 自定義查找
- 條件表達式
- 數據庫函數
- 其它
- 遺留的數據庫
- 提供初始數據
- 優化數據庫訪問
- 視圖層
- 基礎
- URL配置
- 視圖函數
- 快捷函數
- 裝飾器
- 參考
- 內建的視圖
- TemplateResponse 對象
- 文件上傳
- 概覽
- File 對象
- 儲存API
- 管理文件
- 自定義存儲
- 基于類的視圖
- 概覽
- 內建顯示視圖
- 內建編輯視圖
- API參考
- 分類索引
- 高級
- 生成 CSV
- 生成 PDF
- 中間件
- 概覽
- 內建的中間件類
- 模板層
- 基礎
- 面向設計師
- 語言概覽
- 人性化
- 面向程序員
- 表單
- 基礎
- 概覽
- 表單API
- 內建的Widget
- 高級
- 整合媒體
- 開發過程
- 設置
- 概覽
- 應用程序
- 異常
- 概覽
- django-admin 和 manage.py
- 添加自定義的命令
- 測試
- 介紹
- 部署
- 概述
- WSGI服務器
- 部署靜態文件
- 通過email追蹤代碼錯誤
- Admin
- 管理操作
- 管理文檔生成器
- 安全
- 安全概述
- 說明Django中的安全問題
- 點擊劫持保護
- 加密簽名
- 國際化和本地化
- 概述
- 本地化WEB UI格式化輸入
- “本地特色”
- 常見的網站應用工具
- 認證
- 概覽
- 使用認證系統
- 密碼管理
- 日志
- 分頁
- 會話
- 數據驗證
- 其它核心功能
- 按需內容處理
- 重定向
- 信號
- 系統檢查框架