Mendalami Serializer Django REST Framework: Validasi, Nested, dan Optimasi N+1

Panduan lengkap menguasai DRF serializers: pipeline validasi, nested serializers, penanganan relasi kompleks, dan teknik optimasi untuk menghindari N+1 queries.

Mendalami Serializer Django REST Framework: Validasi, Nested, dan Optimasi N+1

Serializer pada Django REST Framework mengemban tugas penting dalam mengkonversi queryset dan instance model menjadi respons JSON—serta memvalidasi data yang masuk sebelum tersimpan di database. Meskipun penggunaan dasar serializer tampak sederhana, aplikasi produksi menuntut penguasaan pipeline validasi, penanganan relasi nested, dan optimasi query yang tepat.

Aturan Performa DRF Serializer

Setiap SerializerMethodField atau nested serializer yang mengakses objek relasi tanpa select_related/prefetch_related akan memicu query database tambahan. Daftar 100 objek dengan 3 relasi berarti 301 queries, bukan 4.

Memahami Pipeline Validasi Serializer DRF

Serializer DRF mengeksekusi validasi dalam urutan spesifik: deserialisasi tingkat field, validator tingkat field, kemudian validasi tingkat objek melalui validate(). Pipeline ini menentukan kapan dan bagaimana mencegat transformasi data.

Urutan validasi dimulai dengan to_internal_value(), yang melakukan deserialisasi tipe data primitif dan menjalankan validator field. Hanya setelah semua field lolos validasi individual, barulah validate() dieksekusi untuk pemeriksaan lintas 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):
        # Validasi tingkat field berjalan pertama
        if value < timezone.now():
            raise serializers.ValidationError("Tanggal mulai tidak boleh di masa lampau.")
        return value
    
    def validate(self, attrs):
        # Validasi tingkat objek berjalan setelah semua field tervalidasi
        start = attrs.get('start_date')
        end = attrs.get('end_date')
        
        if start and end and end <= start:
            raise serializers.ValidationError({
                'end_date': "Tanggal akhir harus setelah tanggal mulai."
            })
        return attrs

Pemisahan ini memungkinkan kontrol granular: menangkap nilai field yang jelas invalid lebih awal, kemudian memvalidasi aturan bisnis yang mencakup beberapa field.

Validator Kustom dan Logika Validasi yang Dapat Digunakan Ulang

DRF mendukung tiga pola validator: metode tingkat field, kelas validator mandiri, dan argumen validators. Validator mandiri mendorong penggunaan ulang di berbagai serializer dan mempertahankan prinsip single responsibility.

python
# validators.py
from rest_framework import serializers
import re

class SlugFormatValidator:
    """Memvalidasi format slug yang aman untuk 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 hanya boleh berisi huruf kecil, angka, dan tanda hubung."
            )

class UniqueForUserValidator:
    """Memvalidasi bahwa nilai unik untuk user saat ini."""
    
    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
        }
        
        # Kecualikan instance saat ini saat 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"Anda sudah memiliki item dengan {self.field_name} ini."
            )

Validator yang memerlukan context menggunakan flag requires_context = True dan menerima serializer_field sebagai argumen kedua. Ini memberikan akses ke request, instance, dan seluruh serializer parent.

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

Menangani Nested Serializer untuk Relasi Kompleks

Nested serializer memodelkan relasi database dalam respons API. Tantangan sebenarnya terletak pada penanganan operasi create dan update—terutama ketika berhubungan dengan relasi many-to-many atau 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 read-only dapat langsung disertakan, tetapi nested yang dapat ditulis memerlukan override create() dan update() eksplisit.

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 sederhana
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        
        # Update relasi many-to-many jika disediakan
        if tags is not None:
            instance.tags.set(tags)
        
        # Tangani update nested chapters
        if chapters_data is not None:
            # Strategi: hapus yang ada dan buat ulang
            instance.chapters.all().delete()
            for chapter_data in chapters_data:
                Chapter.objects.create(book=instance, **chapter_data)
        
        return instance

Pola ini memisahkan operasi read dan write: field nested menyediakan output terformat dengan baik, sementara field *_id menerima primary key untuk input. Wrapper @transaction.atomic memastikan konsistensi data ketika operasi multiple gagal sebagian.

Teknik Optimasi untuk Menghindari Masalah N+1 Query

Masalah performa paling umum pada DRF berasal dari query N+1 yang dihasilkan oleh nested serializer dan method field. Sebuah endpoint yang mengembalikan 50 buku dengan author, tag, dan chapter akan mengeksekusi ratusan query tanpa optimasi yang tepat.

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

Pilihan antara select_related dan prefetch_related mengikuti aturan sederhana: select_related untuk relasi single-object (ForeignKey, OneToOne) menggunakan SQL JOIN, sedangkan prefetch_related untuk relasi multi-object (ManyToMany, reverse ForeignKey) menggunakan query terpisah.

Menangani SerializerMethodField dengan Efisien

SerializerMethodField sering menyebabkan masalah N+1 karena query dieksekusi per instance. Solusinya melibatkan anotasi queryset atau penggunaan prefetch yang cermat.

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):
        # Aman jika tags sudah di-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')
        )

Anotasi Count menghitung chapter dalam query tunggal alih-alih mengambil relasi dan menghitung di Python. Akses author.name melalui source berfungsi karena select_related('author') sudah memuat data author.

Prefetch Kustom untuk Relasi Tersaring

Objek Prefetch memungkinkan query kustom untuk relasi yang sudah di-prefetch, memungkinkan filtering dan ordering tanpa query tambahan.

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

Atribut to_attr menyimpan hasil prefetch dalam atribut kustom (recent_reviews) alih-alih mengganti manager relasi default. Ini berguna saat diperlukan akses ke subset tersaring dan relasi penuh.

Kontrol Serialisasi Lanjutan dengan to_representation

Method to_representation() memberikan kontrol penuh atas output yang diserialisasi. Ini mengeksekusi setelah semua serialisasi field, memungkinkan transformasi berdasarkan context atau data 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')
        
        # Hilangkan field sensitif untuk user non-staff
        if request and not request.user.is_staff:
            data.pop('isbn', None)
        
        # Tambahkan field terkondisi
        if instance.published_date:
            from django.utils import timezone
            data['is_new_release'] = (
                timezone.now().date() - instance.published_date
            ).days < 30
        
        # Restrukturisasi output bersarang
        if 'chapters' in data:
            data['chapter_count'] = len(data['chapters'])
            data['chapters'] = data['chapters'][:3]  # Hanya 3 pertama di respons
        
        return data

Mendebug Query Serializer

Paket django-debug-toolbar dan django-silk sangat berharga untuk mengidentifikasi masalah query. Untuk logging programatis, wrapper queryset sederhana sudah cukup.

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)

Siap menguasai wawancara Django Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Kesimpulan

Menguasai serializer DRF memerlukan pemahaman pipeline validasi, pola nested relationship, dan strategi optimasi query. Komponen-komponen kunci yang perlu diingat meliputi: menggunakan validasi tingkat field untuk pemeriksaan individual dan validate() untuk aturan lintas field, memisahkan field read-only dan write-only untuk nested relationship yang bersih, selalu menerapkan select_related dan prefetch_related dalam queryset view, dan memanfaatkan to_representation() untuk output bersyarat.

Penerapan pola-pola ini memastikan API tidak hanya berfungsi dengan benar tetapi juga berkinerja baik di bawah beban produksi yang nyata. Serializer yang efisien mengurangi waktu respons dan beban server secara signifikan.

Tag

#django
#drf
#serializers
#api
#python

Bagikan

Artikel terkait