iT邦幫忙

2024 iThome 鐵人賽

DAY 16
2
Python

Django 忍法帖——Django Ninja 入門指南系列 第 16

卷 16:回應(四)Resolver 方法——欄位資料格式化

  • 分享至 

  • xImage
  •  

上一篇提到,API 回應常常是對 Django Model 物件內容的篩選與加工——然後 JSON 序列化。

其中「加工」部分,用更專業的說法,大概是「資料格式化」——依照一定的規則,對輸出資料進行某種轉換或重新組織,以符合特定的輸出格式

資料格式化的種類很多,例如:

  1. 時間格式轉換:將資料庫中的時間戳(timestamp),轉換為更易讀的格式。
  2. 數值轉換:將數字轉換為貨幣格式,或將小數點位數進行四捨五入。
  3. 字串處理:截斷過長的文字、加上統一的前綴等。

不論原因為何,絕大部分時候都是為了資料的「可讀性」,或符合特定業務規則。

可想而知,像資料格式化這樣的需求,不僅實務上重要,在 API 開發中也十分常見,值得我們用一整篇文章,細細探討。

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


場景與需求

再次回到「取得單一文章資訊」API,這是目前的回傳格式:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "alice@example.com"
    },
    "created_at": "2024-09-12T02:28:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

我們決定簡化回應的時間字串,改採「"2024-09-12T02:28:16Z"」格式。

和舊版相比,只是少了「.801」這個小數部分而已,且依舊符合 ISO 8601 標準。

總之,回應中created_atupdated_at兩個欄位的內容,需要進行格式上的轉換。即上述提到的「資料格式化」。


Django REST Framework 做法

首先,我們還是不免俗地先介紹 Django REST Framework(以下簡稱 DRF)的做法,方便你對比兩者的差異——你會發現其實大同小異

在 DRF 中,我們可以透過SerializerMethodField實現時間格式的轉換。以下是透過 DRF 實現的範例:

class PostSerializer(serializers.ModelSerializer):
    ...
    created_at = serializers.SerializerMethodField()
    updated_at = serializers.SerializerMethodField()

    def get_created_at(self, obj):
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def get_updated_at(self, obj):
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

其中的重點有三:

  1. 要格式化的欄位,值必須是SerializerMethodField
  2. 在序列化器類別中,定義相同欄位名稱的實例方法(有第一位置參數self),且命名時要加上get_前綴,比如get_created_at
  3. obj參數指的是當前被序列化的物件。本例中,我們預期引數是一個Post模型實例。這個方法將在序列化過程中會被自動調用,將原始的 datetime 物件轉換為指定的字串格式。

附帶一提,在 DRF 序列化器的各種實例方法中,obj這個參數名稱可以稱得上是一個命名慣例


Django Ninja 的欄位資料格式化

看完 DRF,我們來看看 Django Ninja 怎麼做。

透過 Django Ninja 的 Resolver 方法,我們也能輕鬆處理這類需求。

Django Ninja 的 Resolver 方法

在 Django Ninja 中,我們用 Resolver 方法來實現同樣的功能:

class PostResponse(Schema):
    ...
    created_at: datetime
    updated_at: datetime

    @staticmethod
    def resolve_created_at(obj: Post) -> str:
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def resolve_updated_at(self, obj: Post) -> str:
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

方法的命名,相較於 DRF 用的是get_前綴,Django Ninja 則是採resolve_前綴。

此外,你沒有看錯,這裡使用了兩種寫法

  • resolve_created_at 是一個「靜態方法(static method)」,需要使用@staticmethod裝飾器,且沒有self參數。
  • resolve_updated_at 是一個典型的實例方法,有self參數。

因為文件的範例中,確實存在這兩種寫法:

class TaskSchema(Schema):
    ...
    owner: Optional[str] = None
    lower_title: str

    @staticmethod
    def resolve_owner(obj):
        if not obj.owner:
            return
        return f"{obj.owner.first_name} {obj.owner.last_name}"

    def resolve_lower_title(self, obj):
        return self.title.lower()

實例方法版本未實裝

但是!現階段,你只要知道「靜態方法」版本即可。

因為採第二種寫法,你將會得到下列錯誤訊息

Error extracting attribute: NotImplementedError: Non static resolves are not supported yet [type=get_attribute_error, input_value=<DjangoGetter: <Post: Ali...'s Django Ninja Post 1>>, input_type=DjangoGetter]

什麼?還沒有實作!

我只好乖乖都改成靜態方法。

回傳結果

最後看一下效果如何:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "alice@example.com"
    },
    "created_at": "2024-09-12T02:28:16Z",
    "updated_at": "2024-09-12T02:28:16Z"
}

非常好!時間字串格式已經被成功轉換——是16Z而不是16.801Z


使用 Alias 攤平欄位資訊

另一個常見的格式化需求,是我們之前提過的「攤平」(flatten)複雜資料結構。

這是一種對資料的「重組」,而結構重組同樣屬於本文所探討的資料格式化範疇。

還記得在第 14 篇,我們透過@property產生「取得文章列表」回應中author_name欄位內容嗎?——這是對User模型的攤平,直接獲取其username欄位資訊。

這裡我們換一個更優雅的做法——alias

使用 Alias

Django Ninja(幾乎是從 Pydantic 照搬來的)提供了Fieldalias參數來實現這一功能。

有關Field,在〈卷 18:Pydantic Field 設定範例與預設值〉將會有更多著墨。

我們先來看看如何使用:

class PostListResponse(Schema):
    id: int
    title: str
    created_at: datetime
    author_name: str = Field(alias='author.username')

注意,原先Post模型的@property方法要拿掉,或至少不能author_name撞名,否則會出錯唷!

我選擇了移除@property方法,直接改用這個新做法。

重點解析

透過alias=author.username取得Post關聯模型——Userusername屬性值。實現了巢狀資料的攤平

這種設計,顯然是向 DRF 的優秀借鑑,相當於 DRF 中的 source=author.username寫法。

雖然有點抽象,卻非常優雅。

alias的用途不限於資料攤平(這反而是比較進階的用法),其它細節,如欄位名稱替換等,可直接參考 Pydantic 文件

回傳結果

這個做法,和之前使用@property的效果是完全一樣的:

// http://127.0.0.1:8000/posts/
[
    {
        "id": 1,
        "title": "Alice's Django Ninja Post 1",
        "created_at": "2024-09-12T02:28:16.801Z",
        "author_name": "Alice"  // 攤平後的作者名字
    }
]

可以看到author_name欄位已經成功攤平,直接顯示了作者的名字。


結語

Django Ninja 的 Resolver 方法允許我們對 API 回應中的欄位資料進行動態處理,滿足各種格式轉換與自定義需求。

在處理像created_atupdated_at這樣的時間欄位時,Resolver 方法不僅簡單易用,還能保證程式碼的結構清晰。

Fieldalias參數則更優雅地實現了另一種常見的資料格式化——「攤平」。不僅簡化了 API 回應,且無需修改背後的 Django 模型。

透過這些方式,我們能更靈活地控制 API 的輸出,以符合客戶端需求。

下一章預告

完成了對「Django Ninja 處理 HTTP 回應」共 4 篇的學習,第三章也正式告一段落。接下來,我們要將目光轉向 API 開發中的另一個重要主題——文件

隨著專案規模的增長,清晰的 API 文件對於任何需要使用 API 的人員都至關重要——包括後端開發者自己

一份好的 API 文件能夠大幅降低溝通成本,提高開發效率、減少錯誤。它不僅是一種技術文書,更是團隊協作的重要樞紐

第四章,我們會探討如何有效地透過 Django Ninja 程式碼,產生高品質的 API 文件,從而提升整體的開發體驗。

本文同步發表於我的部落格——Code and Me


上一篇
卷 15:回應(三)為何不用 ModelSchema?——相比 DRF,我更偏愛 Django Ninja 的理由
下一篇
卷 17:API 文件(上)Django Ninja 文件實踐指南
系列文
Django 忍法帖——Django Ninja 入門指南31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
gbaian10
iT邦新手 5 級 ‧ 2024-09-28 17:59:03

typo: 相較於 DRF 用的是_get前綴

疑問: DRF 有傳入了 self,那所以是可以在該 func 中用 self 讀取其他屬性的值嗎?

例如: 我總是想將 get_updated_at 的結果改成 get_created_at 的1秒鐘後(首次建立時),可以使用 self 在 get_updated_at 讀取到 created_at的值嗎?

obj看起來像是解析了 func 名稱並截掉前面 get_resolve 就是該屬性。
在創立物件之後讀取該屬性值然後重新賦值? 之後讀取都會讀取到重寫後的值?

Kyo Huang iT邦新手 4 級 ‧ 2024-09-29 02:38:48 檢舉

typo: 相較於 DRF 用的是_get前綴

感謝告知!

DRF 只用實例方法,所以有 self 也有 obj,但我實際開發時幾乎都只用 obj 而已。Django Ninja 因為目前只有靜態方法,就省得思考這個問題

obj 代表當前正在被序列化的 model 實例(回應時),而 self 代表的是「序列化器實例」本身

根據我之前和 AI 的討論,這個 self 應該主要用來呼叫序列化器實例的其它自定義方法(或屬性),而不是 model 實例的屬性

所以我認為是讀不到

我要留言

立即登入留言