分頁(下)自定義分頁類別
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 25 篇。
上一篇我們介紹了 Django Ninja 的內建分頁器,並用它實作了簡單的分頁功能。
雖然內建的PageNumberPagination
確實方便,但在很多時候,我們仍需要一些客製化功能。
為了實現這個目的,你需要自定義一個分頁類別。
不過別擔心,這種自定義,並非從零開始。而是繼承 Django Ninja 所提供的基礎分頁類別,再進行自己的「加工」。
這篇文章就要來教你怎麼做。
本文所有的程式碼變動,可參考這個 PR。
客製化需求
除了基本的分頁,我們還希望能夠:
- 允許客戶端選擇每頁顯示的資料數量,可選範圍限定在 1 至 100 之間。
- 在回應中新增兩個欄位,顯示當前的分頁資訊:
- 當前頁數(
page
)。 - 每頁顯示數量(
per_page
)。
- 當前頁數(
這無疑是很常見的需求。我們將透過自定義分頁類別,來實現這些功能。
話不多說,直接開始!
實作:自定義分頁類別
分頁器(分頁類別)通常是是供全專案使用,所以不適合放在 Django app 目錄中。但也不能像 exception handlers 一樣,放在專案的api.py
,因為會引發循環引用。
所以,我在專案目錄 NinjaForum 建立一個新的 Python 模組——pagination.py
。
在這個新模組中,直接撰寫一個名為CustomPagination
的分頁類別,如下:
1 | from typing import Any |
這個分頁類別允許我們透過查詢參數——page
和per_page
——來決定分頁的大小與頁數,而且回應中還多了兩個同名的新欄位,作為額外的分頁資訊。
自定義分頁類別解說
雖然程式碼看起來細節繁多,但仔細閱讀後,你會發現它其實不難理解。
限於篇幅,我們只挑一些重點來講。
重點一:整體結構來自繼承的類別——PaginationBase
第一個疑惑應該是:「啊我怎麼會知道分頁類別要這樣寫?」
從官方文件我們可以得知,要繼承一個叫PaginationBase
的類別。但文件中對該類別的描寫還是有點簡略,所以需要看原始碼來了解更多的具體資訊。
然後模仿並覆寫類別中的一些屬性、方法——差不多就是如此。
重點二:Input Schema
1 | class Input(Schema): |
你一定能看出來,這個 Schema 就是拿來定義和驗證與分頁有關的 URL 查詢參數。
此外,Input
類別會作為引數傳入paginate_queryset
方法中,作為實現分頁邏輯的一部分。
Input
中的每一個屬性,就代表一個查詢參數(限分頁相關)——而且一樣可以使用Field
來設定細節!
這裡的Field
是 Pydantic 的Field
,我們在第 18 篇詳細介紹過。它允許我們為每個參數設定預設值、文件範例和基本的驗證規則。
本例中,page
的預設值是 1,且必須大於等於 1;per_page
的預設值是 10,且必須在 1 到 100 之間。這樣可以確保我們的分頁參數始終在合理的範圍內。
同樣的道理也適用於Output
,它決定了 HTTP 回應「應該要有」的格式,相當於分頁回應的 Schema。
重點三:paginate_queryset 方法
這個方法是所有分頁類別的核心,它實現了具體的分頁邏輯。
它的第一參數是self
,可見它是一個「實例方法」。
最值得注意的是第二參數——queryset
,它實際上就是 view 函式的 return 值,而且類型必須是 QuerySet。
paginate_queryset
會利用我們熟悉的「切片與索引」,對傳入的 QuerySet 進行「切割」。這是 Django 為 QuerySet 自行實作的功能,行為上類似 Python 的list
、tuple
等容器。
當它回應給客戶端時,我們就得到了切片後的 QuerySet 和自定義的回應格式。
測試自定義分頁
寫完上述的自定義類別,view 函式只要多一行@paginate(CustomPagination)
即可,這裡就省略程式碼。
直接看結果吧!我使用了/?page=2&per_page=5
(第 2 頁、每頁 5 筆)查詢參數:
十分理想!
那如果每頁數量設定為超過 100 會怎樣呢?
1 | { |
答案是 422 回應。
分頁功能總結
透過這兩篇文章,我們展示了如何在 DjangoNinja 中實作分頁,從簡單的內建方法,到複雜的自定義分頁類別。
根據專案需求,你可以選擇適合自己的分頁策略,讓每一個回應,都能以最適合的方式呈現給使用者。
為什麼「多重狀態碼回應」不實用?
還記得我們在第 13 篇、第 21 篇留下的伏筆嗎?
在〈卷 13:回應(一)Django Ninja 處理 HTTP 回應〉中我提到:
但我覺得這個「多重狀態碼回應」設定在實務上沒有很實用,為何?我們後續再談。
幫你複習一下,「多重狀態碼回應」指的是這個用法:
1 |
|
然後在 view 函式中,依照不同情況,給出不同的 return。
我在〈卷 21:錯誤處理(上)HttpError 與自定義 HTTP 回應〉又說了:
這樣看起來確實不錯,也很符合直覺,我以前寫 Django REST framework,都是這樣寫的。
可是,這個寫法在 Django Ninja 中,使用「分頁裝飾器」時,就會踢到鐵板了。
目前時機未到,在後續的〈卷 25:分頁(下)自定義分頁類別〉中,我們再把這件說清楚。
這不就來了嗎!
理由很簡單,關鍵就在於本文「重點三:paginate_queryset 方法」中的那句:
最值得注意的是第二參數——
queryset
,它實際上就是 view 函式的 return 值,而且類型必須是 QuerySet。
因為paginate_queryset
方法中,第二參數的類型必須是 QuerySet!
在paginate_queryset
內部,我們將這個參數視為 QuerySet 使用、操作。若傳入的不是 QuerySet,分頁邏輯就會出錯。
「多重狀態碼回應」與 paginate_queryset 方法的衝突
然而,多重狀態碼的回應,return 型別未必是 QuerySet——很可能是tuple
。
我舉一個簡單的例子你就懂,我們把「取得文章列表」API 改成這樣:
1 |
|
這個例子清楚地顯示了「多重狀態碼回應」與分頁器之間的衝突:
- 當查詢結果正常時,view 函式 return 一個 QuerySet(即
posts
),丟給分頁器進行分頁,一切運作良好。 - 當沒有找到文章時,view 函式則會試圖回傳一個
tuple
——因為 Django Ninja 的「非 200 回應」必須有狀態碼,所以是tuple
,而不是 QuerySet。
這將導致paginate_queryset
方法出錯,因為它預期接收一個 QuerySet,後續的內部操作也是以此為前提。
如果專案中所有的 API 都沒有分頁,使用「多重狀態碼回應」來處理「非 200」回應是完全可行的。
但只要有一個 API 需要分頁,這個有分頁的 API,為了避免上述衝突,就要改用一樣是第 21 篇提到的方式——raise HttpError
。
考慮到專案整體的一致性,其餘的 API,也應該採用raise HttpError
這個方式。
而分頁需求是如此的常見,所以「多重狀態碼回應」也就成為了雞肋。
相關文章