เจาะลึก Serializer ใน Django REST Framework: Validation, Nested และการแก้ปัญหา N+1

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

เจาะลึก Serializer ใน Django REST Framework: Validation, Nested และการแก้ปัญหา N+1

Serializer ใน Django REST Framework ทำหน้าที่สำคัญในการแปลง queryset และ model instance ให้เป็น JSON response—รวมถึงการ validate ข้อมูลขาเข้าก่อนที่จะบันทึกลง database แม้ว่าการใช้งาน serializer พื้นฐานจะดูเรียบง่าย แต่แอปพลิเคชัน production ต้องการความเชี่ยวชาญใน validation pipeline การจัดการความสัมพันธ์แบบ nested และการเพิ่มประสิทธิภาพ query

กฎประสิทธิภาพ DRF Serializer

ทุก 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

python
# 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

python
# 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 แม่ทั้งหมด

python
# 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

python
# 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() อย่างชัดเจน

python
# 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 หลายร้อยครั้งโดยไม่มีการเพิ่มประสิทธิภาพที่เหมาะสม

python
# 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 อย่างระมัดระวัง

python
# 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 เพิ่มเติม

python
# 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 ได้

python
# 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 ธรรมดาก็เพียงพอ

python
# 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
#drf
#serializers
#api
#python

แชร์

บทความที่เกี่ยวข้อง