資料查詢與過濾(上)FilterSchema 介紹
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 26 篇。
「查詢」是 API 中常見的附加需求,本質上是對資料的過濾(filtering)與篩選。
無論是篩選文章、商品,還是查詢用戶,根據不同條件來過濾資料並獲得結果,可說是大部分專案的必備功能。
在 view 函式中,實作查詢最簡單的方式,就是使用 Django ORM 的過濾方法。例如,我們可以用filter
方法來根據特定條件篩選 QuerySet。
這種方法簡單直接,適合基本的查詢需求。然而,它也有其局限性——隨著欄位與需求的增加,查詢條件可能變得越來越複雜,導致程式碼冗長且難以維護。
為了解決這一問題,Django Ninja 提供了 FilterSchema,讓我們可以用更「結構化」的方式,定義並管理查詢條件。
本文將介紹 FilterSchema,一步步實作與講解,讓你了解如何在 Django Ninja 中使用 FilterSchema,實現更加靈活、模組化的 API 查詢功能。
本文所有的程式碼改動,可參考這個 PR。
傳統查詢方法與問題
上一篇我們提到「取得文章列表」API,還記得我們在〈卷 11:請求(三)查詢參數 - Query Parameters〉為它加上的「依文章標題查詢」功能嗎?
這是目前程式碼的現況:(請留意查詢參數名稱title
)
1 | ... |
那時我們就是用了 Django ORM 的filter
方法!
1 | posts.filter(title__icontains=title).select_related('author') |
最後的select_related('author')
是為了避免「N+1」問題,和查詢邏輯無關,可以先不管。
潛在問題
這樣的寫法很直觀,對於簡單的查詢需求很有效。
但隨著專案規模的擴大、查詢需求變得複雜時,就會產生以下困境:
- 程式碼重複:當你有多個地方需要進行相似的過濾時,你可能會發現自己在重複相同的過濾邏輯。
- 維護困難:隨著過濾條件的增加,你的 view 函式可能變得難以維護。每增加一個新的過濾條件,都可能需要修改多個地方的程式碼。
- 資料驗證與轉換:你需要手動處理資料驗證和轉換的問題,這不僅增加了錯誤的可能性,也增加了開發的複雜度。
- 擴充性:當你需要支援更複雜的過濾條件,比如範圍查詢、多條件組合查詢時,手動組裝 ORM 查詢的方法可能會顯得非常笨重且難以管理。
因此,我們並不推薦在複雜查詢的情況下,直接使用 ORM 的filter
方法來組裝查詢條件。
新需求:同時查詢作者名字
新的需求是,用同一個關鍵字, 同時查詢文章的標題或作者的名字, 只要任一符合就顯示在結果中。(二者滿足其一即可,也可以都符合)
這需求類似於 iThome 鐵人賽官網的這個查詢功能:
不過我們只能查 2 種,而它可以同時查 3 種:題目、簡介、參賽者暱稱。
但本質上是一樣的。
用傳統方法 + Q 物件實作
如果用傳統方法 + Django 的 Q 物件實作,則查詢會長這樣:
1 | from django.db.models import Q |
此時查詢參數不適合再叫title
,因為它要查詢兩個欄位。沒關係,我們之後會改成query
。
這段程式碼有兩個重點:
- 查詢果然變長了!之後如果還有新的查詢條件,那豈不是……
- 這個
Q
是什麼東西?
Q
則是 Django ORM 的 Q 物件,它在複雜查詢邏輯中佔據了重要地位。因此,我們有必要先簡單介紹一下。
Django Q 物件簡介
為了改善多條件查詢時,程式結構複雜的問題,Django 提供了Q
物件。
Q
物件允許我們靈活地組織查詢條件,使用邏輯運算子(如&
、|
)進行條件合併。在處理複雜條件過濾時非常有用。
比如說,我們想要篩選出標題包含「Ninja」並且作者名稱包含「Alice」的文章,可以這樣寫:
1 | posts = posts.filter( |
上述寫法,其實就等價於我們常見的:
1 | posts = posts.filter( |
所以你通常不會在「AND」需求時使用Q
物件。
「OR」查詢條件才是Q
的經典場景。
現在條件改為——文章標題「或」作者名字有「Alice」就行。可以使用|
:
1 | posts = posts.filter( |
Q
物件讓查詢更加靈活且清晰,特別是在面對多個可選條件時。
使用 FilterSchema 改進查詢
了解傳統查詢方法容易造成程式冗長的問題,並學習了Q
物件的基礎後,我們要開始介紹今天的主角——FilterSchema。
Django Ninja 提供的 FilterSchema,主要的功能是讓查詢語句更加結構化、模組化,避免 view 函式變得冗長、難讀。
而且,與 Schema 中的驗證方法相同,它也一定程度實現了「關注點分離」原則——藉由將查詢邏輯從 view 函式中抽離出來。
不過,我們先不急著一步到位,容我分階段地改進程式碼。
這樣雖然有點笨拙,但你會對 FilterSchema 與複雜查詢的實作,有更深刻的了解。
第一版「改進」
我們先用 FilterSchema 實現上述的「新需求:同時查詢作者名字」。
在schemas.py
中建立新的 Schema,不過這次是 FilterSchema:
1 | # post/schemas.py |
這個 FilterSchema,其實是給「查詢參數(query parameters)」使用的。所以它的欄位(屬性)名稱,就是你認為客戶端應該使用的查詢參數名稱。
因為同時要查「文章標題」和「作者名字」,所以我命名為query
。
接下來,我們在 view 函式中使用它:
1 |
|
PS:這裡的專案範例程式碼有誤,第二個Q
查詢誤植為「content__icontains
」,請讀者留意。我已在下一個分支中修復。
看完這個新的 view 函式,你可能不禁心想:
這是在搞笑吧?完全沒有變簡單啊!
沒錯,因為這只是 FilterSchema 的「半成品」,所以看起來比不用還冗長。
儘管如此,其中還是有一些看點,值得我們了解。
重點解析
1 | q = Q(title__icontains=filters.query) | \ |
從這段能看出,Q
物件可以單獨進行各種合併操作,最後再丟給 Django filter
方法作為參數。
Django Ninja 常見句型
本例中,這樣的 view 函式參數「句型」在 Django Ninja 非常普遍:
1 | filters: PostFilterSchema = Query() |
而初學者看了會很容易「誤解」。
為什麼?因為你可能會以為,filters
的型別是PostFilterSchema
(這沒問題),然後它的預設值是Query()
,因為 Python 函式就是這樣定義的。
但並不是。
Query()
並不是filters
參數的預設值,否則它的型別不應該是Query
嗎?
事實上,= Query()
這段標記不是給你看的,是給 Django Ninja 看的,它相當於是在告訴 Django Ninja:
這個參數內容應該從 HTTP 請求中的查詢參數(query parameters)中取得,而不是從 body 或 path。
這樣想就很容易明白了。
Django Ninja 會試圖從查詢參數獲取字串,拆解它們(如果有複數個查詢字串),然後一一丟給PostFilterSchema
進行初始化與驗證:
- 驗證失敗:回傳 422。
- 驗證成功:將 Schema 物件傳入 view 函式作為函式的參數(區域變數)。
查詢結果
這是用「Alice」作為關鍵字的查詢結果:
查到了 30 篇文章,全都來自於作者名稱包含「Alice」的用戶。
小結與下一步
本篇先講到這裡,我們已經接觸了兩個新概念——Q 物件和 FilterSchema。
我們還分析了 Django Ninja 中常見的 view 函式參數「句型」,這對於理解框架的使用方式、習慣非常重要。
這些概念需要時間消化,但我可以向你保證,這樣的鋪陳是值得的。
比起直接深入 FilterSchema 的進階用法,這種循序漸進的學習方式更有助於理解。
下一篇你將會看到,為什麼認識 Q 物件很重要。以及如何透過 FilterSchema,來建立結構化、符合「關注點分離」的多欄位查詢。
我們下篇見。