2024 iThome 鐵人賽2024 iThome 鐵人賽

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

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 時,多數時候感覺更加輕鬆