回應(二)用 Schema 建立巢狀結構回應
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 14 篇。
在 API 開發中,我們經常會遇到關聯模型之間的資料需要同時返回的情況。
特別是在處理「一對一」或「一對多」關聯時,多層結構往往是常態。
我們希望以巢狀結構(Nested Objects)的方式返回資料,這樣可以讓 API 的使用者一次取得必要資訊,而不需要進行多次請求。
本文將繼續使用並擴充「單一文章資訊」API 這個範例,講述如何在 Django Ninja 中實現巢狀結構回應,讓我們的 API 回應更加豐富、有體系。
本文所有的程式碼變動,可參考這個 PR。
一、問題背景
在之前的 API 設計中,「取得單一文章資訊」的回應包括了文章資訊及作者的 id:
1 | class PostResponse(Schema): |
有經驗的開發者都知道,無論是id
還是author_id
,通常不是給服務的使用者看的——而是給前端人員靈活運用的。
比如在系統的畫面中,文章可能包括作者的個人資訊連結,點進去可以看到作者資訊。此時前端必須透過 id,再呼叫另一支 API「取得用戶資訊」來獲得額外的內容。
如果額外資訊很多,這樣的「解耦」設計是非常合理的。但如果我們希望一併呈現作者的「必要資訊」,那分次呼叫的設計就略嫌拖沓。
所以我們需要巢狀結構!
API 可以直接在回應中,嵌入作者的「必要資訊」,這樣用戶就不必再進行多次請求。這裡我們以一併顯示作者的「名字」和「email」為例。
二、API 改進:重新定義 Schema
只需要做一件事,就可以讓回應的內容、結構有所不同——重新定義PostResponse
:
1 | from ninja import Schema |
_AuthorInfo
包含了作者的id
、name
和email
,並將這個結構嵌入PostResponse
中的author
欄位(從author_id
易名而來,因為資訊內涵已有所不同)。
如此一來,我們便可以同時獲得文章和作者的必要資訊。
命名小建議
你可能留意到我在_AuthorInfo
使用了「底線開頭」這個命名原則。在 Python 中,這是一種慣例,用來表示這個屬性、函式、類別主要是作為內部使用。
所謂的「內部」可以有很多種解讀,而這裡我的用意是:它只是某個或多個 Schema 的一部分,不直接供 view 函式調用。
別小看這個命名細節。隨著你的 Schema 數量增加,在開發新 API 時,你總是需要先瀏覽現有的 Schema,以決定是重新定義還是延用既有的。
此時有這樣的命名區別就顯得很「貼心」了——你不必在大大小小的 Schema 中翻來覆去,看得眼睛要脫窗。
撰寫巢狀 Schema 的機會不少,所以我認為養成這樣的好習慣是值得的。
Nested Response
我們來看 API 的回應:
1 | // http://127.0.0.1:8000/posts/2/ |
看看新的author
欄位內容,巢狀結構,非常完美!
用戶可以直接到看文章作者的名字與 email,如果想看更多作者資訊,依舊能透過id
欄位,再讓前端呼叫另一支 API。
這是一個理想的折衷方案。
三、「攤平」巢狀資訊
前面的「折衷方案」確實挺理想。不過,有時我們的需求更簡單。
比如在「取得文章列表」API 中,我們可能也需要顯示作者的資訊——但此時只要名字就足夠了。
不需要作者 id,更不用 email,只要名字即可。
那麼,為何稱之為「攤平巢狀資訊」呢?因為作者的名字並非Post
模型的直接屬性,它實際上來自於關聯模型——User
。
我們必須要把有關作者的巢狀資訊進行化簡。
本來是這樣:
1 | "author": { |
現在變成這樣:
1 | "author_name": "Alice", |
從兩層變回一層(但不是作者 id 而是名字了),所以稱為「攤平」(flatten)。
Schema 解耦
還記得「取得文章列表」API 的回應格式,其實是和「取得單一文章資訊」共用的:
1 |
|
兩者都使用了PostResponse
。
本文上半部對「取得單一文章資訊」回應的修改,也會影響到「取得文章列表」——這通常不是我們想要的結果。
所以,我們要為「取得文章列表」API 建立一個屬於自己的回應 Schema,並依照前面提到的需求,簡化資訊!
我打算:
- 省略文章的內容(
content
)還有更新時間(updated_at
)這兩個欄位,因為在列表中並不需要。 - 作者的部分只留下「名字」即可。
四、實作攤平巢狀資訊——使用@property
我們先看看新 Schema 如何定義:
1 | class PostListResponse(Schema): |
你可能覺得奇怪,哪來的author_name
屬性?Post
模型並沒有啊?
沒錯!因為那是我們自己定義的——使用@property
:
1 | # post/models.py |
如此一來,你的 Post 模型物件,就會有author_name
這個屬性了。
但要注意,呼叫這個屬性通常意味著觸發第二次查詢(因為它是關聯模型上的屬性),所以 view 函式中要搭配 Django QuerySet 方法select_related
:
1 | posts.filter(title__icontains=title).select_related('author') |
這是 Django ORM 中常見的「N+1」議題,在此先不展開。
更好的做法
你可能覺得這個方式好像不怎麼優雅(至少我第一次看到時就是這麼想!)——尤其是和 Django REST framework 的做法相比。
Django REST framework 會在序列化器中這樣寫:
1 | author_name = serializers.CharField(source="author.name") |
是不是簡潔很多?
但這確實是 Django Ninja 作者早期推薦的方式。
別擔心,第 16 篇我們會介紹更好、更現代化的做法。不過@property
在某些情況下,還是很有用的。
攤平後的回應
最後,我們來看看「取得文章列表」API 的新回應:(假設只有一篇文章)
1 | // http://127.0.0.1:8000/posts/ |
漂亮!
小結
這篇文章中,我們展示了如何在 Django Ninja 中使用 Schema 實現巢狀結構回應。
接著介紹如何「攤平」這個巢狀結構,把原來的作者 id 替換成名字欄位。
這些方法大大增加了 API 回應的靈活性。
下一篇文章,我們將討論 Django Ninja 和 Django REST framework 在序列化與回應結構處理上的不同設計理念,並比較兩者的優劣。
相關文章