เจาะลึก Serializer ใน Django REST Framework: Validation, Nested และการแก้ปัญหา N+1
คู่มือครบถ้วนสำหรับการใช้งาน DRF serializers: pipeline validation, nested serializers, การจัดการความสัมพันธ์ที่ซับซ้อน และเทคนิคการเพิ่มประสิทธิภาพเพื่อหลีกเลี่ยงปัญหา N+1 queries

Serializer ใน Django REST Framework ทำหน้าที่สำคัญในการแปลง queryset และ model instance ให้เป็น JSON response—รวมถึงการ validate ข้อมูลขาเข้าก่อนที่จะบันทึกลง database แม้ว่าการใช้งาน serializer พื้นฐานจะดูเรียบง่าย แต่แอปพลิเคชัน production ต้องการความเชี่ยวชาญใน validation pipeline การจัดการความสัมพันธ์แบบ nested และการเพิ่มประสิทธิภาพ query
ทุก SerializerMethodField หรือ nested serializer ที่เข้าถึง object ที่เกี่ยวข้องโดยไม่มี select_related/prefetch_related จะทำให้เกิด database query เพิ่มเติม รายการ 100 object ที่มี 3 ความสัมพันธ์หมายถึง 301 queries แทนที่จะเป็น 4
ทำความเข้าใจ Validation Pipeline ของ DRF Serializer
DRF serializer ดำเนินการ validation ตามลำดับที่เฉพาะเจาะจง: การ deserialize ระดับ field, validator ระดับ field จากนั้นจึงเป็น validation ระดับ object ผ่าน validate() Pipeline นี้กำหนดว่าจะเข้าแทรกการแปลงข้อมูลเมื่อใดและอย่างไร
ลำดับ validation เริ่มต้นด้วย to_internal_value() ซึ่ง deserialize ชนิดข้อมูล primitive และรัน field validator หลังจากทุก field ผ่าน validation ทีละตัวแล้วเท่านั้น validate() จึงจะทำงานเพื่อตรวจสอบข้าม field
# serializers.py
from rest_framework import serializers
from django.utils import timezone
from .models import Event
class EventSerializer(serializers.ModelSerializer):
start_date = serializers.DateTimeField()
end_date = serializers.DateTimeField()
class Meta:
model = Event
fields = ['id', 'title', 'start_date', 'end_date', 'location']
def validate_start_date(self, value):
# Validation ระดับ field ทำงานก่อน
if value < timezone.now():
raise serializers.ValidationError("วันที่เริ่มต้นต้องไม่อยู่ในอดีต")
return value
def validate(self, attrs):
# Validation ระดับ object ทำงานหลังจากทุก field ถูก validate แล้ว
start = attrs.get('start_date')
end = attrs.get('end_date')
if start and end and end <= start:
raise serializers.ValidationError({
'end_date': "วันที่สิ้นสุดต้องอยู่หลังวันที่เริ่มต้น"
})
return attrsการแยกนี้ช่วยให้ควบคุมได้ละเอียด: จับค่า field ที่ไม่ถูกต้องอย่างชัดเจนตั้งแต่เนิ่นๆ จากนั้นจึง validate กฎทางธุรกิจที่ครอบคลุมหลาย field
Custom Validator และ Logic Validation ที่นำกลับมาใช้ซ้ำได้
DRF รองรับ validator สามรูปแบบ: method ระดับ field, class validator แบบ standalone และ argument validators Validator แบบ standalone ส่งเสริมการนำกลับมาใช้ซ้ำข้าม serializer และรักษาหลักการ single responsibility
# validators.py
from rest_framework import serializers
import re
class SlugFormatValidator:
"""Validate รูปแบบ slug ที่ปลอดภัยสำหรับ URL"""
def __init__(self, allow_unicode=False):
self.allow_unicode = allow_unicode
self.pattern = r'^[\w-]+$' if allow_unicode else r'^[a-z0-9-]+$'
def __call__(self, value):
if not re.match(self.pattern, value):
raise serializers.ValidationError(
"Slug ต้องมีเฉพาะตัวอักษรพิมพ์เล็ก ตัวเลข และเครื่องหมายขีดกลาง"
)
class UniqueForUserValidator:
"""Validate ว่าค่าเป็นค่าเฉพาะสำหรับ user ปัจจุบัน"""
requires_context = True
def __init__(self, queryset, field_name):
self.queryset = queryset
self.field_name = field_name
def __call__(self, value, serializer_field):
request = serializer_field.context.get('request')
if not request or not request.user.is_authenticated:
return
filter_kwargs = {
self.field_name: value,
'user': request.user
}
# ยกเว้น instance ปัจจุบันเมื่อ update
instance = serializer_field.parent.instance
exists_query = self.queryset.filter(**filter_kwargs)
if instance:
exists_query = exists_query.exclude(pk=instance.pk)
if exists_query.exists():
raise serializers.ValidationError(
f"คุณมี item ที่มี {self.field_name} นี้อยู่แล้ว"
)Validator ที่ต้องการ context ใช้ flag requires_context = True และรับ serializer_field เป็น argument ที่สอง สิ่งนี้ให้การเข้าถึง request, instance และ serializer แม่ทั้งหมด
# serializers.py
from .validators import SlugFormatValidator, UniqueForUserValidator
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
slug = serializers.CharField(
max_length=100,
validators=[
SlugFormatValidator(allow_unicode=False),
UniqueForUserValidator(
queryset=Article.objects.all(),
field_name='slug'
)
]
)
class Meta:
model = Article
fields = ['id', 'title', 'slug', 'content', 'published']การจัดการ Nested Serializer สำหรับความสัมพันธ์ที่ซับซ้อน
Nested serializer จำลองความสัมพันธ์ database ใน API response ความท้าทายที่แท้จริงอยู่ที่การจัดการ operation create และ update—โดยเฉพาะเมื่อทำงานกับความสัมพันธ์ many-to-many หรือ reverse foreign key
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
class Book(models.Model):
title = models.CharField(max_length=300)
isbn = models.CharField(max_length=13, unique=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
tags = models.ManyToManyField(Tag, related_name='books')
published_date = models.DateField()
class Chapter(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='chapters')
title = models.CharField(max_length=200)
order = models.PositiveIntegerField()
content = models.TextField()Nested serializer แบบอ่านอย่างเดียวสามารถรวมได้โดยตรง แต่ nested ที่เขียนได้ต้องมีการ override create() และ update() อย่างชัดเจน
# serializers.py
from rest_framework import serializers
from django.db import transaction
from .models import Author, Tag, Book, Chapter
class ChapterSerializer(serializers.ModelSerializer):
class Meta:
model = Chapter
fields = ['id', 'title', 'order', 'content']
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ['id', 'name', 'email']
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
author_id = serializers.PrimaryKeyRelatedField(
queryset=Author.objects.all(),
source='author',
write_only=True
)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
source='tags',
many=True,
write_only=True
)
chapters = ChapterSerializer(many=True)
class Meta:
model = Book
fields = [
'id', 'title', 'isbn', 'author', 'author_id',
'tags', 'tag_ids', 'chapters', 'published_date'
]
@transaction.atomic
def create(self, validated_data):
chapters_data = validated_data.pop('chapters', [])
tags = validated_data.pop('tags', [])
book = Book.objects.create(**validated_data)
book.tags.set(tags)
for chapter_data in chapters_data:
Chapter.objects.create(book=book, **chapter_data)
return book
@transaction.atomic
def update(self, instance, validated_data):
chapters_data = validated_data.pop('chapters', None)
tags = validated_data.pop('tags', None)
# Update field ธรรมดา
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
# Update ความสัมพันธ์ many-to-many ถ้ามีการระบุ
if tags is not None:
instance.tags.set(tags)
# จัดการ update nested chapters
if chapters_data is not None:
# กลยุทธ์: ลบที่มีอยู่และสร้างใหม่
instance.chapters.all().delete()
for chapter_data in chapters_data:
Chapter.objects.create(book=instance, **chapter_data)
return instanceรูปแบบนี้แยก operation อ่านและเขียน: field nested ให้ output ที่จัดรูปแบบสวยงาม ขณะที่ field *_id รับ primary key สำหรับ input Wrapper @transaction.atomic รับประกันความสอดคล้องของข้อมูลเมื่อ operation หลายตัวล้มเหลวบางส่วน
เทคนิคการเพิ่มประสิทธิภาพเพื่อหลีกเลี่ยงปัญหา N+1 Query
ปัญหาประสิทธิภาพที่พบบ่อยที่สุดใน DRF มาจาก N+1 query ที่สร้างโดย nested serializer และ method field Endpoint ที่คืนค่า 50 หนังสือพร้อม author, tag และ chapter จะรัน query หลายร้อยครั้งโดยไม่มีการเพิ่มประสิทธิภาพที่เหมาะสม
# views.py
from rest_framework import viewsets
from django.db.models import Prefetch
from .models import Book, Chapter
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
def get_queryset(self):
return Book.objects.select_related(
'author'
).prefetch_related(
'tags',
Prefetch(
'chapters',
queryset=Chapter.objects.order_by('order')
)
)การเลือกระหว่าง select_related และ prefetch_related เป็นไปตามกฎง่ายๆ: select_related สำหรับความสัมพันธ์ single-object (ForeignKey, OneToOne) ใช้ SQL JOIN ขณะที่ prefetch_related สำหรับความสัมพันธ์ multi-object (ManyToMany, reverse ForeignKey) ใช้ query แยกต่างหาก
การจัดการ SerializerMethodField อย่างมีประสิทธิภาพ
SerializerMethodField มักทำให้เกิดปัญหา N+1 เพราะ query ถูกรันต่อ instance การแก้ไขเกี่ยวข้องกับการ annotate queryset หรือการใช้ prefetch อย่างระมัดระวัง
# serializers.py
class BookListSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.name', read_only=True)
chapter_count = serializers.IntegerField(read_only=True)
tag_names = serializers.SerializerMethodField()
class Meta:
model = Book
fields = ['id', 'title', 'author_name', 'chapter_count', 'tag_names']
def get_tag_names(self, obj):
# ปลอดภัยถ้า tags ถูก prefetch แล้ว
return [tag.name for tag in obj.tags.all()]
# views.py
from django.db.models import Count
class BookListViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = BookListSerializer
def get_queryset(self):
return Book.objects.select_related(
'author'
).prefetch_related(
'tags'
).annotate(
chapter_count=Count('chapters')
)การ annotate Count นับ chapter ใน query เดียวแทนที่จะ fetch ความสัมพันธ์และนับใน Python การเข้าถึง author.name ผ่าน source ทำงานได้เพราะ select_related('author') โหลดข้อมูล author แล้ว
Custom Prefetch สำหรับความสัมพันธ์ที่กรองแล้ว
Object Prefetch อนุญาตให้ custom query สำหรับความสัมพันธ์ที่ถูก prefetch ช่วยให้กรองและเรียงลำดับได้โดยไม่ต้องมี query เพิ่มเติม
# views.py
from django.db.models import Prefetch
from .models import Book, Chapter, Review
class BookDetailViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = BookDetailSerializer
def get_queryset(self):
return Book.objects.select_related(
'author'
).prefetch_related(
'tags',
Prefetch(
'chapters',
queryset=Chapter.objects.order_by('order').only(
'id', 'title', 'order', 'book_id'
)
),
Prefetch(
'reviews',
queryset=Review.objects.filter(
is_approved=True
).select_related('user').order_by('-created_at')[:5],
to_attr='recent_reviews'
)
)Attribute to_attr เก็บผลลัพธ์ prefetch ใน attribute ที่กำหนดเอง (recent_reviews) แทนที่จะแทนที่ relation manager เริ่มต้น สิ่งนี้มีประโยชน์เมื่อต้องการเข้าถึงทั้ง subset ที่กรองแล้วและความสัมพันธ์แบบเต็ม
การควบคุม Serialization ขั้นสูงด้วย to_representation
Method to_representation() ให้การควบคุมอย่างเต็มที่ต่อ output ที่ถูก serialize มันทำงานหลังจาก field serialization ทั้งหมด ช่วยให้แปลงตาม context หรือข้อมูล instance ได้
# serializers.py
class BookSerializer(serializers.ModelSerializer):
chapters = ChapterSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = ['id', 'title', 'isbn', 'author', 'chapters', 'published_date']
def to_representation(self, instance):
data = super().to_representation(instance)
request = self.context.get('request')
# ลบ field ที่ sensitive สำหรับ user ที่ไม่ใช่ staff
if request and not request.user.is_staff:
data.pop('isbn', None)
# เพิ่ม field แบบมีเงื่อนไข
if instance.published_date:
from django.utils import timezone
data['is_new_release'] = (
timezone.now().date() - instance.published_date
).days < 30
# ปรับโครงสร้าง output nested
if 'chapters' in data:
data['chapter_count'] = len(data['chapters'])
data['chapters'] = data['chapters'][:3] # แค่ 3 ตัวแรกใน response
return dataการ Debug Query Serializer
Package django-debug-toolbar และ django-silk มีคุณค่ามากสำหรับการระบุปัญหา query สำหรับ logging แบบ programmatic wrapper queryset ธรรมดาก็เพียงพอ
# utils.py
import logging
from django.db import connection, reset_queries
from django.conf import settings
from functools import wraps
logger = logging.getLogger(__name__)
def log_queries(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not settings.DEBUG:
return func(*args, **kwargs)
reset_queries()
result = func(*args, **kwargs)
queries = connection.queries
logger.debug(
f"{func.__name__} executed {len(queries)} queries"
)
for query in queries:
logger.debug(f" [{query['time']}s] {query['sql'][:100]}")
return result
return wrapper
# views.py
class BookViewSet(viewsets.ModelViewSet):
@log_queries
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)พร้อมที่จะพิชิตการสัมภาษณ์ Django แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
สรุป
การเชี่ยวชาญ DRF serializer ต้องการความเข้าใจใน validation pipeline, รูปแบบ nested relationship และกลยุทธ์การเพิ่มประสิทธิภาพ query องค์ประกอบหลักที่ต้องจดจำ ได้แก่: ใช้ validation ระดับ field สำหรับการตรวจสอบรายบุคคลและ validate() สำหรับกฎข้าม field, แยก field read-only และ write-only สำหรับ nested relationship ที่สะอาด, ใช้ select_related และ prefetch_related เสมอใน queryset ของ view และใช้ประโยชน์จาก to_representation() สำหรับ output แบบมีเงื่อนไข
การนำรูปแบบเหล่านี้ไปใช้รับประกันว่า API ไม่เพียงแต่ทำงานถูกต้องเท่านั้น แต่ยังทำงานได้ดีภายใต้ภาระงาน production จริง Serializer ที่มีประสิทธิภาพลดเวลาตอบสนองและภาระ server ได้อย่างมาก
แท็ก
แชร์
บทความที่เกี่ยวข้อง

คำถามสัมภาษณ์งาน Django: ORM, Middleware และ DRF เจาะลึก
คำถามสัมภาษณ์งาน Django ครอบคลุมการปรับแต่ง ORM ด้วย select_related และ prefetch_related, สถาปัตยกรรม middleware, ประสิทธิภาพ serializer ของ Django REST Framework, permissions และรูปแบบ pagination

Django Async Views และ ASGI ในปี 2026: ประสิทธิภาพและคำถามสัมภาษณ์
เจาะลึก Django async view และ ASGI ในปี 2026: กลไกเบื้องหลัง, เซิร์ฟเวอร์ที่ควรดีพลอย, async ORM กับกับดัก SynchronousOnlyOperation พร้อมคำถามสัมภาษณ์

Django และ PostgreSQL ในปี 2026: การทำ Indexing, Full-Text Search และคำถามสัมภาษณ์
คู่มือปฏิบัติสำหรับการปรับแต่งประสิทธิภาพ Django PostgreSQL: B-tree, partial index, covering index, การทำ full-text search ด้วย SearchVector และ GIN พร้อมคำถามสัมภาษณ์ปี 2026