2024 iThome 鐵人賽2024 iThome 鐵人賽

Django API 回應,常常是對 Model 物件(即 db 資料)內容進行一定的篩選與加工

比如「取得單一文章資訊」API,實際上就是從Post物件挑選欄位,再進行序列化。

這個過程中,我們需要考慮如何將模型物件轉換為 API 的回應結構,同時保持程式碼的可維護性與靈活。

對此,Django REST framework(以下簡稱 DRF)提供了非常實用的「特製」序列化器——ModelSerializer,可說是 DRF 開發者必學的核心功能。

Django Ninja 雖然也有類似的實踐——ModelSchema,對我而言卻是雞肋般的存在,我幾乎不曾使用

這樣的差異,無疑是兩者的核心設計理念不同所導致。

我們曾在第 3 篇中討論過,兩者在功能上的主要區別。本文將透過「Django 模型物件的序列化」這個頗具代表性的議題,說明「為何相比於 DRF,我更喜歡寫 Django Ninja」。


ModelSerializer 的亮點

DRF 中的ModelSerializer是個非常強大的工具,它能夠自動將 Django 模型轉換為 API 需要的資料結構——序列化器,大大簡化了「為序列化器定義欄位」的過程。

附帶一提,DRF 序列化器,相當於 Django Ninja 所使用的 Schema,兩者的概念大同小異,都是用於資料的驗證與序列化

如果我們把「取得單一文章資訊」API 回應用ModelSerializer改寫,它將長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework import serializers

# Author 序列化器
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']

# Post 序列化器
class PostSerializer(serializers.ModelSerializer):
author = AuthorSerializer() # 嵌套的 Author 序列化器

class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at']

如你所見,透過ModelSerializer,我們只需要少少的程式碼便能定義完序列化器,從而避免了手動設定的重複與麻煩。


ModelSerializer 的隱憂

然而,這樣的方便也帶來一定的隱憂

因為不用自己定義欄位,所以ModelSerializer幫你做了許多欄位的隱式轉換——從 Django Model 欄位轉換為序列化器欄位。

為何說「隱式」呢?因為自動轉換後的序列化器欄位,其欄位的型別、特性、是否唯讀(read_only)等細節,你未必清楚

換言之,ModelSerializer不僅會自動生成欄位,還會自動推斷欄位的型別、屬性、屬性的參數等。

舉例說明

這樣講有點抽象,對於沒寫過 DRF 的讀者可能不好太理解。我們直接看一個例子:

1
2
3
4
5
from django.db import models

class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)

這是一個超簡單的 Django Model,我引用自 Django 官方文件

它有兩個欄位first_namelast_name,實際上它還有一個 Django 自動生成的id欄位,在程式碼中沒有顯示。

ModelSerializer 的「魔法」

使用ModelSerializer,我們可以這樣定義序列化器:

1
2
3
4
5
6
7
from rest_framework import serializers
from .models import Person

class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ['id''first_name''last_name']

程式碼很簡單,但它背後的「魔法」卻很多。

因為實際上的序列化器和欄位是長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework import serializers

class PersonSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
first_name = serializers.CharField(max_length=30)
last_name = serializers.CharField(max_length=30)

def create(self, validated_data):
return Person.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)
instance.save()
return instance

有沒有覺得有點吃驚

隱式轉換了做很多事

其中,id欄位被自動加上了read_only=Truefirst_namelast_name則被自動加上了max_length=30

這還是在 Django Model 的設計與欄位參數相對簡單的情況下,當 Model 欄位更複雜時,ModelSerializer的「魔法」也會變得更加複雜。

它背後有很多轉換邏輯,讓開發者在某些情況下必須去理解這些「隱藏規則」——因為這個推斷有時可能不符合你的需求,導致你需要手動覆寫

總之,自動推斷與轉換固然省去了手動設定的麻煩,但當你需要調整某些細節,或理解具體的轉換邏輯時,這種隱式行為可能會讓你感到困惑

魔法的代價

在實際開發中,這種隱式轉換的「魔法」會讓開發者失去對轉換過程的理解與掌控。你很可能會發現,序列化的結果和你想的並不完全一致!

此時我們往往需要翻閱 DRF 的官方文件來理解內部如何處理這些欄位轉換,但也不是每個細節都寫得清楚明白。

對開發者而言,特別是在處理複雜 API 時,會明顯增加學習和維護成本。

以上正是我的經驗!

即使寫了 2 年 DRF,遇到序列化問題,我還是很常需要重新查看文件


ModelSchema

Django Ninja 的 ModelSchema 相較於 ModelSerializer,則顯得「陽春」許多。

怎麼說?我們看一下官方文件中的例示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.contrib.auth.models import User
from ninja import ModelSchema

class UserSchema(ModelSchema):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']

# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str

說它陽春,因為它只會幫你自動轉換、定義欄位的「型別」而已。其他欄位細節,比如max_length,都要靠Field來設定——ModelSchema 不會幫你做這些。

而 DRF 的ModelSerializer,如前所述,則是會「做更多」。

既然 ModelSchema 的自動轉換相對單純,那為何我還是不建議使用呢?有兩個理由。

其中第一個理由,就是標題所說「為何我更偏愛 Django Ninja」的理由。


理由一:低耦合 + 明確優於隱晦

Django Ninja 更強調開發者對 API 結構的掌握,而 DRF 則偏向於提供高度整合且便利的工具。

這種差異反映在它們對待 Django 模型序列化的方式上,也影響了開發者在使用這兩個框架時的風格和思維方式

Django REST framework 和 Django 高度耦合

我們可以發現, DRF 幾乎是一個「為 Django 高度定製」的 API 開發工具。

這種緊密的結合雖然帶來了便利性,但也意味著 DRF 在很大程度上依賴於 Django 的內部結構和功能。不管是 Generic views,還是本文的 ModelSerializer,都是如此。

高耦合的優點就是你可以少做很多事,而代價則是你要很了解自己在做什麼

明確優於隱晦

相較於 DRF,Django Ninja 與 Django 的耦合程度則要低得多

在我看來,Django Ninja 更偏好「明確優於隱晦」,Django Ninja 的 Schema 定義是基於 Pydantic,它要求開發者明確定義每個欄位,無論是輸入還是輸出。

雖然這樣相對繁瑣,但它帶來的好處是顯而易見的。

明確的兩大優點

首先,手動定義 Schema 讓開發者對資料結構有著絕對的掌控權。沒有任何隱藏規則或暗箱操作,一切都清晰可見。

其次,這種方法有效地降低了模型層與 API 層之間的耦合。在實際開發中,模型設計可能會隨著需求變化而更新,但這不應該直接影響到 API。

總的來說,Django Ninja 強調以 Schema 為核心的控制,讓 API 的設計更具穩定性和靈活性,並賦予開發者對資料流的完全掌控。


理由二:更好、更可讀的 API 文件

在第 18 篇,我們會詳細討論 Schema 欄位設定對 API 文件的影響。

簡言之,如果使用 ModelSchema,那麼渲染出來的 API 文件將會相當陽春

並不符合我對 API 文件清晰與明確性的追求。


結語

不可否認,Django REST framework 有一些非常方便且貼心的設計,比如上一篇提到的source=參數,它直觀而優雅。

Django Ninja 則要求開發者,盡可能手動定義每個欄位,減少模型與 API 層的耦合,這更符合 Python 哲學中的「明確優於隱晦」,同時避免隱式行為帶來的潛在問題。

這正是我更偏愛 Django Ninja 的原因。

Django Ninja 對明確性的追求,讓我在開發和維護 API 時,多數時候感覺更加輕鬆