Tìm Hiểu Sâu Về Serializer Django REST Framework: Validation, Nested và Tối Ưu N+1

Hướng dẫn toàn diện về DRF serializers: pipeline validation, nested serializers, xử lý quan hệ phức tạp và các kỹ thuật tối ưu để tránh vấn đề N+1 queries.

Tìm Hiểu Sâu Về Serializer Django REST Framework: Validation, Nested và Tối Ưu N+1

Serializer trong Django REST Framework đảm nhận nhiệm vụ quan trọng là chuyển đổi queryset và model instance thành JSON response—đồng thời validate dữ liệu đầu vào trước khi lưu vào database. Mặc dù việc sử dụng serializer cơ bản khá đơn giản, các ứng dụng production đòi hỏi sự thành thạo về pipeline validation, xử lý quan hệ nested và tối ưu hóa query.

Quy Tắc Hiệu Suất DRF Serializer

Mỗi SerializerMethodField hoặc nested serializer truy cập các đối tượng liên quan mà không có select_related/prefetch_related sẽ kích hoạt thêm database query. Danh sách 100 đối tượng với 3 quan hệ có nghĩa là 301 queries thay vì 4.

Hiểu Pipeline Validation của DRF Serializer

DRF serializer thực thi validation theo thứ tự cụ thể: deserialization cấp field, validator cấp field, sau đó là validation cấp object thông qua validate(). Pipeline này xác định thời điểm và cách thức can thiệp vào quá trình chuyển đổi dữ liệu.

Quy trình validation bắt đầu với to_internal_value(), thực hiện deserialization các kiểu dữ liệu nguyên thủy và chạy các field validator. Chỉ sau khi tất cả các field vượt qua validation riêng lẻ, validate() mới được thực thi để kiểm tra liên 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 cấp field chạy trước
        if value < timezone.now():
            raise serializers.ValidationError("Ngày bắt đầu không được ở quá khứ.")
        return value
    
    def validate(self, attrs):
        # Validation cấp object chạy sau khi tất cả field được validate
        start = attrs.get('start_date')
        end = attrs.get('end_date')
        
        if start and end and end <= start:
            raise serializers.ValidationError({
                'end_date': "Ngày kết thúc phải sau ngày bắt đầu."
            })
        return attrs

Sự phân tách này cho phép kiểm soát chi tiết: bắt lỗi các giá trị field rõ ràng không hợp lệ sớm, sau đó validate các quy tắc nghiệp vụ liên quan đến nhiều field.

Custom Validator và Logic Validation Tái Sử Dụng

DRF hỗ trợ ba mẫu validator: phương thức cấp field, lớp validator độc lập và tham số validators. Validator độc lập khuyến khích việc tái sử dụng trên nhiều serializer và duy trì nguyên tắc single responsibility.

python
# validators.py
from rest_framework import serializers
import re

class SlugFormatValidator:
    """Validate định dạng slug an toàn cho 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 chỉ được chứa chữ thường, số và dấu gạch ngang."
            )

class UniqueForUserValidator:
    """Validate giá trị là duy nhất cho user hiện tại."""
    
    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
        }
        
        # Loại trừ instance hiện tại khi 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"Bạn đã có một item với {self.field_name} này."
            )

Validator cần context sử dụng cờ requires_context = True và nhận serializer_field làm tham số thứ hai. Điều này cung cấp quyền truy cập vào request, instance và toàn bộ serializer cha.

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']

Xử Lý Nested Serializer cho Quan Hệ Phức Tạp

Nested serializer mô hình hóa các quan hệ database trong API response. Thách thức thực sự nằm ở việc xử lý các thao tác create và update—đặc biệt khi làm việc với quan hệ many-to-many hoặc 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 chỉ đọc có thể được đưa vào trực tiếp, nhưng nested có thể ghi yêu cầu override create()update() rõ ràng.

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 các field đơn giản
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        
        # Update quan hệ many-to-many nếu được cung cấp
        if tags is not None:
            instance.tags.set(tags)
        
        # Xử lý update nested chapters
        if chapters_data is not None:
            # Chiến lược: xóa hiện có và tạo lại
            instance.chapters.all().delete()
            for chapter_data in chapters_data:
                Chapter.objects.create(book=instance, **chapter_data)
        
        return instance

Mẫu này tách biệt các thao tác đọc và ghi: các field nested cung cấp output được định dạng đẹp, trong khi các field *_id chấp nhận primary key cho input. Wrapper @transaction.atomic đảm bảo tính nhất quán dữ liệu khi các thao tác multiple thất bại một phần.

Kỹ Thuật Tối Ưu để Tránh Vấn Đề N+1 Query

Vấn đề hiệu suất phổ biến nhất trong DRF xuất phát từ N+1 query được tạo ra bởi nested serializer và method field. Một endpoint trả về 50 cuốn sách với author, tag và chapter sẽ thực thi hàng trăm query nếu không được tối ưu đúng cách.

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')
            )
        )

Việc lựa chọn giữa select_relatedprefetch_related tuân theo quy tắc đơn giản: select_related cho quan hệ single-object (ForeignKey, OneToOne) sử dụng SQL JOIN, trong khi prefetch_related cho quan hệ multi-object (ManyToMany, reverse ForeignKey) sử dụng query riêng biệt.

Xử Lý SerializerMethodField Hiệu Quả

SerializerMethodField thường gây ra vấn đề N+1 vì query được thực thi cho mỗi instance. Giải pháp liên quan đến annotation queryset hoặc sử dụng prefetch cẩn thận.

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):
        # An toàn nếu tags đã được 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')
        )

Annotation Count đếm chapter trong một query duy nhất thay vì fetch quan hệ và đếm trong Python. Truy cập author.name qua source hoạt động vì select_related('author') đã tải dữ liệu author.

Custom Prefetch cho Quan Hệ Đã Lọc

Đối tượng Prefetch cho phép custom query cho các quan hệ được prefetch, cho phép lọc và sắp xếp mà không cần query bổ sung.

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'
            )
        )

Thuộc tính to_attr lưu kết quả prefetch trong thuộc tính tùy chỉnh (recent_reviews) thay vì thay thế relation manager mặc định. Điều này hữu ích khi cần truy cập cả subset đã lọc và quan hệ đầy đủ.

Kiểm Soát Serialization Nâng Cao với to_representation

Phương thức to_representation() cung cấp kiểm soát hoàn toàn đối với output được serialize. Nó thực thi sau tất cả serialization field, cho phép biến đổi dựa trên context hoặc dữ liệu 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')
        
        # Loại bỏ field nhạy cảm cho user không phải staff
        if request and not request.user.is_staff:
            data.pop('isbn', None)
        
        # Thêm field có điều kiện
        if instance.published_date:
            from django.utils import timezone
            data['is_new_release'] = (
                timezone.now().date() - instance.published_date
            ).days < 30
        
        # Tái cấu trúc output nested
        if 'chapters' in data:
            data['chapter_count'] = len(data['chapters'])
            data['chapters'] = data['chapters'][:3]  # Chỉ 3 đầu tiên trong response
        
        return data

Debug Query Serializer

Các package django-debug-toolbardjango-silk rất có giá trị để xác định vấn đề query. Để logging có lập trình, một wrapper queryset đơn giản là đủ.

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)

Sẵn sàng chinh phục phỏng vấn Django?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Kết Luận

Làm chủ DRF serializer đòi hỏi hiểu biết về pipeline validation, mẫu nested relationship và chiến lược tối ưu query. Các thành phần chính cần ghi nhớ bao gồm: sử dụng validation cấp field cho kiểm tra riêng lẻ và validate() cho quy tắc liên field, tách biệt field read-only và write-only cho nested relationship rõ ràng, luôn áp dụng select_relatedprefetch_related trong queryset của view, và tận dụng to_representation() cho output có điều kiện.

Áp dụng các mẫu này đảm bảo API không chỉ hoạt động chính xác mà còn hoạt động tốt dưới tải production thực tế. Serializer hiệu quả giảm đáng kể thời gian phản hồi và tải server.

Thẻ

#django
#drf
#serializers
#api
#python

Chia sẻ

Bài viết liên quan