Belajar Django - Models & ORM
Episode 6 of 27

Belajar Django - Models & ORM

Mendesain Model Django dengan field types, relasi ForeignKey, ManyToManyField, dan OneToOneField, plus Meta dan constraints. Menguasai QuerySet: chaining, filtering, aggregation, serta select_related dan prefetch_related untuk menghindari query N+1.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Template di episode 5 menampilkan data, tetapi kita belum punya model nyata untuk menampungnya. Sekarang kita membangun lapisan paling penting arsitektur Django: Models & ORM — jembatan antara Python dan database yang selalu kita bicarakan sejak episode 2.

Mengapa episode ini krusial? Karena desain model menentukan kualitas data, performa query, dan kemudahan fitur di masa depan. Keputusan yang kalian buat di sini — relasi mana yang ForeignKey atau ManyToMany, field mana yang butuh indeks — akan terasa (atau menyakitkan) sampai bertahun-tahun kemudian. Di episode 6 ini kita merancang skema blog devblog secara lengkap.

Field Types

Django menyediakan puluhan field. Yang paling sering dipakai:

Pythonblog/models.py - field types umum
from django.db import models
 
class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=250, unique=True)
    body = models.TextField()
    excerpt = models.TextField(blank=True)
    status = models.CharField(
        max_length=10,
        choices=[("draft", "Draft"), ("published", "Published")],
        default="draft",
    )
    view_count = models.PositiveIntegerField(default=0)
    price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    published_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

Poin penting per field:

FieldKapan dipakaiCatatan
CharFieldString pendekwajib max_length
TextFieldString panjangtanpa max_length di sebagian besar DB
SlugFieldURL slugpakai unique=True bila jadi identifier
DateTimeFieldTanggal/waktuauto_now_add (set 1x) vs auto_now (tiap save)
DecimalFieldUang/angka presisijangan pakai FloatField untuk uang
BooleanField/PositiveIntegerFieldFlag/hitungdefault bijak untuk menghindari null

Note

Hati-hati dengan null=True pada CharField/TextField. Konvensi Django: untuk string, gunakan blank=True (validasi form mengizinkan kosong) tapi simpan null=False (DB menyimpan string kosong, bukan NULL). null=True lebih tepat untuk field numerik/date yang memang bisa "tidak ada nilainya".

Relasi: FK, M2M, O2O

Relasi adalah tempat desain model jadi menarik. Blog devblog memakai tiga jenis relasi:

Pythonblog/models.py - relasi
from django.contrib.auth.models import User
from django.db import models
 
class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(unique=True)
 
    class Meta:
        verbose_name_plural = "categories"
 
    def __str__(self):
        return self.name
 
class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
 
class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    body = models.TextField()
    category = models.ForeignKey(
        Category, on_delete=models.PROTECT, related_name="posts"
    )
    tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
    author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="posts"
    )
    created_at = models.DateTimeField(auto_now_add=True)
 
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
 
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
    bio = models.TextField(blank=True)
  • ForeignKey (Post.category, Post.author, Comment.post): banyak-ke-satu. related_name memberi nama reverse relation — category.posts dan post.comments.
  • ManyToManyField (Post.tags): banyak-ke-banyak, Django membuat tabel pivot otomatis.
  • OneToOneField (UserProfile.user): satu-ke-satu, kebalikannya bisa diakses via user.profile.
  • on_delete=models.PROTECT pada category mencegah penghapusan category yang masih punya post; CASCADE menghapus data turunan mengikuti induk.

Warning

Jangan lupa related_name pada semua relasi. Tanpa itu Django menamai reverse relation dengan post_set (nama kelas + _set), yang membingungkan dan rawan konflik bila ada dua FK ke model yang sama. related_name membuat arah balik relasi eksplisit dan bersih.

Meta dan Constraints

class Meta di dalam model mengontrol perilaku DB dan admin:

Pythonblog/models.py - Meta & constraints
class Post(models.Model):
    # ... field ...
    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["-created_at"]),
            models.Index(fields=["status", "-created_at"]),
        ]
        constraints = [
            models.CheckConstraint(
                check=models.Q(status__in=["draft", "published"]),
                name="post_status_valid",
            ),
        ]
  • ordering menentukan urutan default QuerySet.
  • indexes membuat indeks database untuk field yang sering difilter/diurutkan — kunci performa yang kita maksimalkan di episode 14.
  • constraints (sejak Django 4.1 bisa dibuat di level database) memperkuat integritas di DB, bukan hanya di aplikasi.

QuerySet: Chaining dan Filtering

Model.objects adalah manager yang mengembalikan QuerySet — representasi lazy dari query SQL. Lazy artinya query tidak dieksekusi sampai benar-benar dipakai (di-iterasi, di-slice, di-convert). Ini memungkinkan chaining:

PythonQuerySet chaining
from blog.models import Post
 
# lazy - belum ada query ke DB
qs = Post.objects.filter(status="published")
 
# tetap lazy - ditambah kondisi
qs = qs.exclude(author__username="admin").order_by("-created_at")
 
# eksekusi terjadi di sini
posts = list(qs[:10])

filter(**kwargs), exclude(**kwargs), dan order_by() berantai menjadi satu query SQL. Follow accessor relasi dengan __: author__username, category__slug, comments__body. Ini sistem yang sama yang dipakai di episode 14 untuk Q objects dan F expressions.

Aggregation

Untuk menghitung agregat, gunakan aggregate (skalar) dan annotate (per-baris):

PythonAggregation & annotate
from django.db.models import Avg, Count, Sum
 
Post.objects.aggregate(total=Count("id"))          # {"total": 123}
Comment.objects.aggregate(avg_len=Avg("body__len"))  # rata-rata
 
# per-baris: jumlah komentar tiap post
posts = Post.objects.annotate(
    comment_count=Count("comments"),
    total_views=Sum("view_count"),
)
for post in posts:
    print(post.title, post.comment_count, post.total_views)

Count("comments") memunculkan kolom comment_count virtual di tiap post — tanpa N+1, karena dihitung dalam satu query JOIN.

Ini topik paling penting untuk performa. Perhatikan perbedaan dua akses relasi:

  • select_related: untuk relasi FK/O2O (forward). JOIN dilakukan di query yang sama.
  • prefetch_related: untuk relasi reverse/M2M (banyak). Query kedua dijalankan dan di-cache dalam satu panggilan.
PythonHindari N+1 dengan select/prefetch
# N+1! 1 query untuk post + 1 query per post untuk author
bad = Post.objects.filter(status="published")
for post in bad:
    print(post.author.username)
 
# 1 query: JOIN dengan auth_user
good = Post.objects.filter(status="published").select_related("author")
for post in good:
    print(post.author.username)
 
# 2 query total: post + semua comments sekaligus
better = Post.objects.prefetch_related("comments").select_related("author")
for post in better:
    print(post.title, post.comments.count())

Warning

Mengakses relasi dalam loop tanpa select_related/prefetch_related adalah masalah N+1: 1 query untuk 100 post + 100 query untuk author = 101 query. Di database jarak jauh atau traffic tinggi, ini pembunuh performa yang diam-diam. Kita bedah pengoptimasian lanjutan di episode 14.

Penutup

Inti yang harus dibawa pulang:

  • Pilih field yang tepat: DecimalField untuk uang, SlugField + unique untuk identifier URL.
  • Relasi: FK banyak-ke-satu, M2M banyak-ke-banyak, O2O satu-ke-satu — semua butuh related_name.
  • on_delete menentukan nasib data saat induk dihapus: CASCADE vs PROTECT.
  • Meta.ordering, indexes, dan constraints mengontrol urutan, performa, dan integritas.
  • QuerySet lazy dan bisa di-chain; aggregate untuk skalar, annotate untuk per-baris.
  • select_related untuk FK, prefetch_related untuk reverse/M2M — cegah N+1.

Di episode 7 selanjutnya kita mewujudkan model menjadi tabel: Migrations & Data Managementmakemigrations, migrate, custom dan data migrations, plus fixtures untuk seed data blog. Skema yang kita desain hari ini akan mulai hidup di database!

Belajar Django - Models & ORM | Belajar Django