2024 iThome 鐵人賽2024 iThome 鐵人賽

這是 Django Ninja 系列教學的第 24 篇。

分頁(pagination)功能,就算在資料量較少的小型專案,也具有相當的重要性。

沒分頁,API 照樣能運作——只是效能會受到影響,特別是在資料量大的情況下。

當 API 一次回傳大量資料時,不僅會增加伺服器負擔,還可能導致客戶端處理緩慢,甚至出現超時記憶體不足等問題。

透過分頁,我們可以避免一次性傳輸大量資料,提高 API 的效能,同時提升使用者的體驗。

這個主題將分為上、下兩篇,介紹如何在 Django Ninja 中實作分頁功能——從內建的分頁器自定義分頁類別,以滿足不同需求。

本文所有的程式碼變動,可參考這個 PR


分頁的重要性

分頁的核心作用將大量資料拆分成小部分傳輸,每次只給一些,從而避免效能問題。

具體而言,分頁能夠幫助我們:

  1. 減少伺服器壓力:不必一次回傳全部資料,只需處理單個頁面的資料。
  2. 提升網路傳輸速度:傳輸太大量的資料會增加網路延遲和資料丟失的風險,通過分頁,能有效降低傳輸需求
  3. 提升使用者體驗:客戶端可以快速獲取並顯示初步資料,無需等待所有資料傳輸完成。同時,分頁還能減少客戶端處理大量資料的壓力

因此,無論專案規模大小,實作高效的分頁策略,對 API 的擴充性與使用者體驗,都有明顯幫助

了解了分頁的重要性後,我們來開始實作吧!


這次的範例 API 是——「取得文章列表」。

因為專案的資料庫中已經有超過 60 筆文章資料,非常適合演示。

什麼?你說你沒有?歡迎參考〈卷 12:請求(四)Request Body 與 Schema 介紹〉結尾的「中場休息與準備」段落。

本篇我們將使用 Django Ninja 內建的PageNumberPagination分頁器,來實作簡單、有效的分頁功能。自定義部分,則留在下一篇。

Django Ninja 內建的分頁器

在 Django Ninja 中,分頁功能可以透過內建的paginate裝飾器加上分頁器(即分頁類別)實現。

Django Ninja 提供了兩個內建分頁器,為方便你理解,以下是它們的白話文介紹

  1. LimitOffsetPagination:根據「從哪一筆資料開始」和「要抓多少筆資料」來進行分頁,適合資料量很大時使用。例如:「從第 20 筆開始,抓 10 筆資料」。
  2. PageNumberPagination:透過頁碼進行分頁,用戶只需要指定想要的頁數,例如「抓第 2 頁的資料」。每頁的資料數量可以由開發者自行設定。

我個人偏好使用PageNumberPagination,或自定義一個類似的版本(也就是下一篇的內容)。

預設的分頁器是LimitOffsetPagination,所以在使用paginate裝飾器時,需要你主動聲明第一參數。等一下你就知道了。

在那之前,我們先來回顧「取得文章列表」的 API 現狀。

API 現狀

如下所示,由於尚未實作分頁功能,它會一次性回傳所有文章資料

1
2
3
4
5
6
7
8
9
10
11
12
13
@router.get('/posts/', response=list[PostListResponse], ...)
def get_posts(
request: HttpRequest,
title: None | str = Query(None, min_length=2, max_length=10),
) -> QuerySet[Post]:
"""
取得文章列表
"""
posts = Post.objects.all()
if title:
posts = posts.filter(
title__icontains=title).select_related('author')
return posts

這在文章少的時候可以運作如常,但隨著資料量增加,效能將會受到影響

接下來,我們要透過 Django Ninja 內建的分頁器來改善這個問題。


PageNumberPagination 實作分頁

使用內建的PageNumberPagination,我們可以輕鬆為 API 加上分頁功能。

只要在 view 函式上使用@paginate裝飾器並加入參數,如下:

1
2
3
4
5
6
7
8
9
10
from ninja.pagination import PageNumberPagination, paginate
...

@router.get(...)
@paginate(PageNumberPagination, page_size=10) # 分頁實作
def get_posts(...) -> QuerySet[Post]:
"""
取得文章列表
"""
...

我省略了大部分內容,這裡只需要關注分頁的實作。

如前所述,第一參數PageNumberPagination必須由你主動聲明——如果是用LimitOffsetPagination則不必。

實作的效果是,每頁改為顯示 10 筆文章,這數量可以透過page_size參數控制。

咦,那換頁怎麼辦?我們看一下PageNumberPagination的原始碼:

1
2
3
4
5
class PageNumberPagination(AsyncPaginationBase):
class Input(Schema):
page: int = Field(1, ge=1)

...

Input代表請求的查詢參數(下一篇會細論),換句話說,你可以在 URL 的查詢參數(query parameters)中使用參數page來指定「頁碼」,以達到換頁效果。

測試分頁效果

呼叫 API,並使用?page=2作為查詢參數,看看效果如何:

符合預期!

回應中顯示的確實是第 2 頁內容——文章 id 從 11 開始,總共 10 筆。


使用內建分頁器的優點與局限

優點

  • 只用簡單的分頁裝飾器 + 內建分頁器即可實現分頁,而且可以控制每頁的數量,非常方便且實用。
  • 適合不需要複雜自定義的情況。

局限

  • 缺乏靈活性,例如我們無法讓使用者指定每頁顯示多少筆資料。
  • 回應的欄位與格式是固定的——而且有點簡略。

小結

透過 Django Ninja 內建的分頁器,我們能夠迅速為 API 加入分頁功能,立即實現簡單的分頁需求。

然而,內建分頁器在控制靈活性上有所不足,也無法建立客製化回應。當分頁需求變得複雜時,就顯得有點捉襟見肘。

此時,自定義分頁器會是一個更好的解決方案。

下一篇,我們將探討如何在 Django Ninja 中自定義分頁類別,以滿足這些進階需求。