資料查詢與過濾(下)FilterSchema 多欄位查詢
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 27 篇。
上篇中,我們學習了 Django ORM 的Q
物件和 Django Ninja 的 FilterSchema,但後者感覺只學了一半。
討論比較多的是,view 函式中使用 FilterSchema 的參數定義方式——這確實很重要,但這只是 FilterSchema 的一部分。
本篇要來補完剩下的內容:
- 完善 FilterSchema:使用「更道地」的寫法,釋放 FilterSchema 真正的力量。
- 實作更進階的欄位查詢功能:多欄位查詢——篩選日期區間。
- 追加實作第 20 篇學到的「跨欄位驗證」:驗證查詢參數的日期區間是否合法。
看來又是資訊滿滿的一篇,話不多說,直接開始吧!
本文所有的程式碼變動,可參考這個 PR。
一、將查詢邏輯遷移到 FilterSchema
還記得我們上一篇的程式碼實作嗎?
明明多定義了 FilterSchema,但 view 函式中的程式碼不僅沒有減少,反而還增加了!(雖然查詢邏輯也變多了,因為要同時查詢兩個欄位)
1 |
|
這簡直莫名其妙🐸
那是因為,FilterSchema 不是這麼用的!
FilterSchema 的「正確」用法
我們應該盡可能將查詢邏輯封裝到 FilterSchema 中,這樣可以讓 view 函式更簡潔,並達到「關注點分離」的效果。
來看看更合理的寫法——將查詢邏輯遷移到 FilterSchema:
1 | class PostFilterSchema(FilterSchema): |
主要的變動是query
欄位的 Field 部分,現在加上了q=
參數內容:
1 | q=["title__icontains", "author__name__icontains"] |
很眼熟吧?沒錯,它們實際上就是Q
物件的條件語句,Django Ninja 會在背後自動調用Q
物件來執行這些查詢。
來自 Mypy 的提醒
一旦使用q=
參數,Mypy 又會提醒你:
Unexpected keyword argument “q” for “Field”
它說的並沒有錯,因為 Pydantic Field 確實沒有這個參數——這是 Django Ninja 自行實作的。
你可以無視它,或加上必要的註解。
View 函式簡化
如此一來,view 函式只需要這樣寫就好了:
1 | ... |
是不是簡單很多?
因為查詢邏輯從 view 函式「分離」出來了,這使得 view 函式的職責更單一、更利於維護。
二、多欄位查詢:新增日期篩選功能
新需求:除了可以查文章標題或作者名稱,現在還要加入對「發文日期」的過濾!
我們將引入兩個新的 URL 查詢參數:
start_date
end_date
兩者將用來查詢、過濾Post
模型中的created_at
欄位(即發文日期),以篩選特定時間範圍內的文章資料。
還有一個額外要求:兩者必須「全有全無」——可以都沒有,但不可以只填其中一個。
這是一個典型的「多欄位」查詢。
新增程式碼
這是加入了上述邏輯後的 FilterSchema:
1 | class PostFilterSchema(FilterSchema): |
其中,start_date
和end_date
都是對模型欄位created_at
的查詢條件。
所以我們使用created_at__gte
和created_at__lte
來描述過濾邏輯(它們都對應了各自的Q
物件),以篩選出符合條件的資料。
那 view 函式呢?你猜得沒錯——完全不用動!
這就是使用 FilterSchema 的好處。
API 文件渲染問題
有趣的是,當我試著為這些查詢參數加上文件範例時,如果這樣寫:
1 | start_date: str | None = Field( |
查看 API 文件將會得到:
😱 Could not render Parameters, see the console.
但寫example='2021-01-01'
卻可以成功。
這可能是 Django Ninja 與 Pydantic 在整合上的一個 bug,我們暫且就先略過吧!
客戶端查詢範例
當我們要查詢某段時間內的文章時,可以使用以下的 URL 查詢參數:
1 | ?start_date=2023-01-01&end_date=2023-01-31 |
這樣就能輕鬆查詢出 2023 年 1 月份的所有文章。
附帶一提,日期中的時間因為沒有指定,預設上都是 0 點 0 分 0 秒。所以如果填同一天,就查不到任何東西。
這是一個需要改善或重新調整的細節,常見的做法是在程式內部把end_date
加 1 天,而我直接選擇讓兩者不能相同XD。實際該怎麼做,取決於你的需求。
除了單純的期間查詢,我們也可以查詢某作者在某段時間內的文章,以查詢作者 Alice 為例:
1 | ?start_date=2023-01-01&end_date=2023-01-31&query=alice |
結果將顯示 Alice 在 2023 年 1 月份的所有文章。
FilterSchema 的預設查詢條件關係
這部分一定要特別介紹,依文件所述,預設上:
- Field-level expressions are joined together using
OR
operator. - The fields themselves are joined together using
AND
operator.
意思就是說,單一欄位內的多個 Q 語句,彼此是 OR 關係,比如上面query
的:
1 | q=['title__icontains', 'author__username__icontains'] |
可以查詢文章標題「或」作者名稱。
而不同欄位中的條件(如果都有),則是 AND 關係——必須同時符合才行。所以作者名稱與日期區間,兩者的條件必須同時符合。
這些預設邏輯可以自行變更,詳情請參考上述文件內容。
三、日期區間驗證
本例中,除了欄位查詢,我們還要確保,使用者輸入的開始日期必須早於結束日期。
並且,兩個欄位的查詢值必須是「全有」或「全無」(全無則不必驗證)。
這個需求非常眼熟——不就是第 20 篇提到的「跨欄位驗證」嗎?
沒錯,我們要透過 Pydantic 的model_validator
來實現,它允許我們在驗證過程中,對輸入資料進行自定義的邏輯檢查。
使用 Pydantic model_validator 實作日期區間驗證
程式碼有點多,我們直接看重點:
1 | class PostFilterSchema(FilterSchema): |
對於查詢條件,我們使用 Pydantic 的model_validator
進行跨欄位驗證,確保使用者輸入的日期是有效且合理的。
事實上,跨欄位驗證往往要考慮很多細節,否則可能掛一漏萬,間接產生新的 bug。
這個例子就是一個典型案例。
我們必須全盤考慮各種可能的輸入情況,包括日期格式是否正確、日期範圍是否合理,以及兩個日期欄位是否同時存在或同時為空。
細緻的驗證邏輯能提升 API 的可靠性,避免因為無效或不合理的輸入而導致系統出現意外行為。而粗糙的邏輯則反之。
測試錯誤回應
PS:這裡的拋出錯誤,範例程式碼中仍使用了ValueError
,我並沒有變更為 Django 的ValidationError
。(最新版已修正)
但以下回應則是模擬 Django 的ValidationError
,以減少不必要的重複。
一、只輸入開始日期:(這發生機率不高,因為前端通常會限制)
1 | { |
二、輸入不合法的日期,比如2023-02-30
:
1 | { |
三、輸入不合法的日期區間,比如start_date=2023-01-31&end_date=2023-01-01
:
1 | { |
小結與下一步
這兩篇文章,我們介紹了 FilterSchema 的有效用法,完成了多欄位查詢和日期篩選,並示範如何使用model_validator
來強化資料驗證,確保查詢邏輯的正確性。
我們還看了這些應用場景的實例程式碼,幫助讀者更好地理解每個步驟的用途和效果。
下一章,我們將探討 Django Ninja 中的認證(Authentication)機制,並介紹如何使用 pytest 進行單元測試,這些都是後端開發中,不可或缺的要素。