DRF Serializers 進階:巢狀序列化與自訂方法 | Django 教學
在前一篇文章中,我們認識了 DRF Serializers 的基本用法,包括 ModelSerializer 的自動欄位產生與資料驗證。但在真實專案中,API 的資料結構往往不是扁平的——文章要帶出作者資訊、訂單要包含商品明細、評論要顯示用戶頭像。這些需求都指向同一個核心技術:巢狀序列化(Nested Serialization)。本篇將深入探討巢狀序列化器、各種 關聯欄位(Relational Fields) 的表示方式、SerializerMethodField 自訂計算欄位,以及自訂
create()/update()方法處理巢狀寫入,最後聚焦在 效能優化 策略,幫助你打造高效且結構清晰的 API。
範例 Model 定義
為了讓後續所有範例一致,我們先定義本篇使用的 Model:
# models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
tags = models.ManyToManyField(Tag, blank=True)
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
關聯欄位的五種表示方式
DRF 提供了多種方式來呈現 Model 之間的關聯關係,每種都有不同的使用場景。讓我們從最基礎的開始逐一介紹。
PrimaryKeyRelatedField:預設的關聯表示
PrimaryKeyRelatedField 是 DRF 預設處理 ForeignKey 和 ManyToManyField 的方式,它以主鍵(Primary Key)的 ID 來表示關聯物件:
# serializers.py
from rest_framework import serializers
from .models import Post, Category
class PostSerializer(serializers.ModelSerializer):
# 明確宣告,等同 DRF 的預設行為
category = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
allow_null=True,
required=False,
)
class Meta:
model = Post
fields = ['id', 'title', 'author', 'category', 'created_at']
回應結果如下:
{
"id": 1,
"title": "Django 入門教學",
"author": 42,
"category": 3,
"created_at": "2026-06-12T10:00:00Z"
}
優點是資料量小、可讀寫;缺點是前端拿到 ID 後還需要額外請求才能得知作者名稱或分類名稱。
StringRelatedField:用 __str__ 表示
StringRelatedField 會呼叫關聯物件的 __str__() 方法來產生可讀的文字表示,它是 唯讀(read-only) 的:
class PostSerializer(serializers.ModelSerializer):
# 呼叫 Category.__str__(),顯示分類名稱
category = serializers.StringRelatedField()
class Meta:
model = Post
fields = ['id', 'title', 'category']
{
"id": 1,
"title": "Django 入門教學",
"category": "Web 開發"
}
適合只需要顯示名稱、不需要進一步查詢關聯物件的場景。
SlugRelatedField:用 slug 欄位取代 PK
SlugRelatedField 讓你用指定的欄位值(而非 PK)來表示與查找關聯物件。這個欄位必須是唯一的(unique),否則查找時可能出現多筆結果的錯誤:
class PostSerializer(serializers.ModelSerializer):
# 讀取時顯示 category 的 slug 欄位,寫入時也用 slug 查詢
category = serializers.SlugRelatedField(
slug_field='slug',
queryset=Category.objects.all(),
allow_null=True,
required=False,
)
# ManyToMany 也可使用,加上 many=True
tags = serializers.SlugRelatedField(
slug_field='name',
queryset=Tag.objects.all(),
many=True,
)
class Meta:
model = Post
fields = ['id', 'title', 'category', 'tags']
{
"id": 1,
"title": "Django 入門教學",
"category": "web-development",
"tags": ["python", "django", "tutorial"]
}
寫入時,前端可以直接傳送 "category": "web-development",DRF 會自動用 slug 欄位查找對應的 Category 物件。這比傳 ID 更直覺,也更適合用於 URL 友善的 API 設計。
HyperlinkedRelatedField:超連結表示
HyperlinkedRelatedField 以完整的 URL 來表示關聯物件,符合 HATEOAS(Hypermedia as the Engine of Application State,超媒體作為應用程式狀態引擎)的 REST 設計原則:
class PostSerializer(serializers.ModelSerializer):
author = serializers.HyperlinkedRelatedField(
view_name='user-detail', # 對應 URL name
read_only=True,
)
category = serializers.HyperlinkedRelatedField(
view_name='category-detail',
read_only=True,
)
class Meta:
model = Post
fields = ['id', 'title', 'author', 'category']
{
"id": 1,
"title": "Django 入門教學",
"author": "http://api.example.com/users/42/",
"category": "http://api.example.com/categories/3/"
}
使用時需要注意:Serializer 必須接收 request context 才能產生完整的絕對 URL。通常在 ViewSet 中 DRF 會自動傳入 context,但如果你手動實例化 Serializer,需要自行傳遞。
巢狀 Serializer(Nested Serializers)
當 API 需要一次回傳關聯物件的完整資訊時,前面四種方式都不夠用——你需要的是 巢狀序列化器(Nested Serializer)。
手動巢狀:在 PostSerializer 中嵌入 AuthorSerializer
from django.contrib.auth.models import User
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'slug']
class PostDetailSerializer(serializers.ModelSerializer):
# 巢狀序列化器:將 author 以完整物件呈現
author = AuthorSerializer(read_only=True)
category = CategorySerializer(read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'category', 'created_at']
回應結果:
{
"id": 1,
"title": "Django 入門教學",
"content": "這是一篇介紹 Django 的文章...",
"author": {
"id": 42,
"username": "alice",
"email": "alice@example.com"
},
"category": {
"id": 3,
"name": "Web 開發",
"slug": "web-development"
},
"created_at": "2026-06-12T10:00:00Z"
}
注意這裡的 read_only=True——預設的巢狀序列化器是唯讀的。如果你需要寫入巢狀資料,需要自訂 create() 和 update() 方法(稍後介紹)。
depth 參數自動巢狀
DRF 提供了一個快捷方式:在 Meta 中設定 depth 參數,自動將指定深度內的所有關聯欄位展開為巢狀物件:
class PostAutoNestedSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['id', 'title', 'author', 'category', 'tags']
depth = 1 # 自動展開一層關聯
depth = 1 會將 author、category、tags 全部自動巢狀展開,不需要額外定義子 Serializer。
depth 的優缺點:
- 優點:程式碼極簡,快速原型開發時非常方便
- 缺點:無法控制巢狀物件包含哪些欄位(會顯示 Model 所有欄位,包括可能不該暴露的欄位);只能唯讀;效能較難控制
- 建議:正式 API 中優先使用手動巢狀序列化器,
depth適合用於除錯或內部工具
SerializerMethodField:自訂計算欄位
SerializerMethodField 讓你在序列化時動態計算欄位值。它是 唯讀(read-only) 的,透過定義 get_<field_name>() 方法來返回值:
class PostSerializer(serializers.ModelSerializer):
# 自訂計算欄位
author_name = serializers.SerializerMethodField()
summary = serializers.SerializerMethodField()
tag_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = [
'id', 'title', 'author_name', 'summary',
'tag_count', 'is_published', 'created_at',
]
def get_author_name(self, obj) -> str:
"""組合作者的顯示名稱"""
return obj.author.get_full_name() or obj.author.username
def get_summary(self, obj) -> str:
"""擷取文章前 100 個字元作為摘要"""
return obj.content[:100] + '...' if len(obj.content) > 100 else obj.content
def get_tag_count(self, obj) -> int:
"""計算標籤數量"""
return obj.tags.count()
{
"id": 1,
"title": "Django 入門教學",
"author_name": "Alice Wang",
"summary": "這是一篇介紹 Django 的文章,涵蓋了從安裝到部署的完整流程...",
"tag_count": 3,
"is_published": true,
"created_at": "2026-06-12T10:00:00Z"
}
注意:
get_tag_count中的obj.tags.count()如果沒有搭配prefetch_related,每筆資料都會產生一次額外查詢。我們會在效能優化章節中說明如何解決這個問題。
讀寫分離:巢狀讀取 + PK 寫入
實務上最常見的需求是:讀取時回傳巢狀物件,寫入時接受 PK(或 slug)。這可以透過 source 參數搭配 read_only / write_only 來實現:
class PostReadWriteSerializer(serializers.ModelSerializer):
# 讀取用:巢狀序列化器,唯讀
category = CategorySerializer(read_only=True)
author = AuthorSerializer(read_only=True)
# 寫入用:接受 PK,write_only 不會出現在回應中
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category', # 對應到 Model 的 category 欄位
write_only=True,
allow_null=True,
required=False,
)
author_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
source='author',
write_only=True,
)
class Meta:
model = Post
fields = [
'id', 'title', 'content',
'author', 'author_id',
'category', 'category_id',
'created_at',
]
這樣,POST 請求只需傳送 {"title": "...", "author_id": 42, "category_id": 3},而 GET 回應會自動展開為巢狀物件。這是一個非常實用的設計模式(Design Pattern),幾乎所有正式專案都會採用。
自訂 create() 方法(處理巢狀寫入)
當你需要在建立主物件時同時建立或關聯巢狀物件(例如一次建立文章與其標籤),就必須覆寫 create() 方法:
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name']
class PostWithTagsSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True) # 可寫入的巢狀序列化器
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'tags']
read_only_fields = ['author']
def create(self, validated_data):
# 第一步:從 validated_data 取出巢狀資料
tags_data = validated_data.pop('tags', [])
# 第二步:建立主物件
post = Post.objects.create(**validated_data)
# 第三步:處理巢狀關聯(ManyToMany)
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(**tag_data)
post.tags.add(tag)
return post
這裡使用 get_or_create() 而非 create(),是為了避免重複建立同名的 Tag。
自訂 update() 方法
更新巢狀資料比建立更複雜,因為你需要決定策略:是 清除後重建、逐一比對更新,還是 僅新增不刪除?
class PostWithTagsSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'tags']
def update(self, instance, validated_data):
# 取出巢狀資料
tags_data = validated_data.pop('tags', None)
# 更新主物件的一般欄位
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
# 更新 ManyToMany 關聯(策略:清除後重建)
if tags_data is not None:
instance.tags.clear() # 清除現有關聯
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(**tag_data)
instance.tags.add(tag)
return instance
多層巢狀寫入的處理策略
當巢狀層級超過一層(例如 Post -> Comment -> Reply),處理邏輯會更加複雜。以下是幾個建議策略:
- 扁平化 API 設計:將深層巢狀拆分為獨立的 endpoint。例如不在 Post API 中處理 Comment 的建立,而是提供獨立的
/posts/{id}/comments/endpoint - 使用第三方套件:drf-writable-nested 提供了
WritableNestedModelSerializer,自動處理多層巢狀的 create/update 邏輯 - 使用 Transaction(交易):多層巢狀寫入時,務必使用
transaction.atomic()確保資料一致性
from django.db import transaction
class PostWithTagsSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'tags']
@transaction.atomic
def create(self, validated_data):
tags_data = validated_data.pop('tags', [])
post = Post.objects.create(**validated_data)
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(**tag_data)
post.tags.add(tag)
return post
使用 @transaction.atomic 裝飾器(Decorator)可以確保:如果巢狀物件建立過程中任何一步失敗,所有變更都會被回滾(Rollback),避免資料庫出現不一致的狀態。
Serializer 效能優化
巢狀序列化器最容易引發的效能問題就是 N+1 查詢(N+1 Query Problem)。假設你查詢 100 篇文章,每篇文章的巢狀 author 都會額外觸發一次資料庫查詢,總共產生 1 + 100 = 101 次查詢。
select_related / prefetch_related 搭配
解決 N+1 問題的關鍵在於在 ViewSet 層級預載入關聯資料:
# views.py
from rest_framework import viewsets
from .models import Post
from .serializers import PostDetailSerializer
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostDetailSerializer
def get_queryset(self):
return Post.objects.select_related(
'author', # ForeignKey:使用 SQL JOIN 一次查詢
'category', # ForeignKey:同上
).prefetch_related(
'tags', # ManyToMany:額外一次查詢預載入
)
select_related vs prefetch_related 的選擇原則:
| 方法 | 適用關聯類型 | 實作方式 | 查詢次數 |
|---|---|---|---|
select_related | ForeignKey、OneToOneField | SQL JOIN | 1 次 |
prefetch_related | ManyToManyField、反向 ForeignKey | 額外 SELECT + Python 組合 | 2 次 |
套用上述最佳化後,無論查詢多少篇文章,資料庫查詢次數始終固定為 2-3 次。
避免 N+1 查詢:SerializerMethodField 搭配 annotate
前面提到的 get_tag_count 方法會對每筆資料觸發一次 COUNT 查詢。更好的做法是在 QuerySet 層級使用 annotate()(註解)預先計算:
from django.db.models import Count
# views.py
class PostViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Post.objects.select_related(
'author', 'category',
).prefetch_related(
'tags',
).annotate(
tag_count=Count('tags'), # 在 SQL 層級計算標籤數量
)
# serializers.py
class PostSerializer(serializers.ModelSerializer):
# 直接從 annotate 的結果取值,無需 SerializerMethodField
tag_count = serializers.IntegerField(read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'tag_count', 'created_at']
這樣 tag_count 在 SQL 查詢階段就已經計算完成,序列化時直接讀取屬性即可,完全不會產生額外查詢。
使用 source 參數減少計算
source 參數可以直接映射 Model 的屬性、方法或關聯欄位路徑,在許多場景下可以取代 SerializerMethodField,既簡潔又高效:
class PostSerializer(serializers.ModelSerializer):
# 用 source 取代 SerializerMethodField
author_username = serializers.CharField(source='author.username', read_only=True)
author_email = serializers.EmailField(source='author.email', read_only=True)
category_name = serializers.CharField(source='category.name', read_only=True)
class Meta:
model = Post
fields = [
'id', 'title', 'author_username',
'author_email', 'category_name', 'created_at',
]
source='author.username' 讓 DRF 自動沿著關聯路徑取值,等同於 Python 的 instance.author.username。搭配 select_related('author') 就不會產生額外查詢。相比 SerializerMethodField 需要額外定義方法,source 參數讓程式碼更簡潔。
總結
本篇文章從 DRF 的五種關聯欄位表示方式出發,逐步深入巢狀序列化器的讀取與寫入,最後聚焦在效能優化上。以下是核心要點的整理:
關聯欄位選擇指南:
- PrimaryKeyRelatedField:預設行為,可讀寫,傳輸 ID
- StringRelatedField:唯讀,顯示
__str__()結果,適合簡易展示 - SlugRelatedField:可讀寫,用指定欄位值取代 PK,API 更直覺
- HyperlinkedRelatedField:唯讀/可讀寫,以 URL 表示,符合 HATEOAS
- Nested Serializer:最完整的巢狀物件展示,預設唯讀
巢狀寫入核心原則:
- 覆寫
create()/update()手動處理巢狀資料 - 使用
transaction.atomic()確保資料一致性 - 多層巢狀建議拆分為獨立 endpoint 或使用 drf-writable-nested
效能優化三步驟:
select_related():預載入 ForeignKey / OneToOneFieldprefetch_related():預載入 ManyToManyField / 反向 ForeignKeyannotate():在 SQL 層級預先計算聚合值,避免 SerializerMethodField 的逐筆查詢
掌握這些進階技巧後,你就能設計出結構清晰、效能優異的 DRF API。下一篇我們將探討 DRF 的認證(Authentication)與權限(Permissions)機制,進一步完善 API 的安全性。