Featured image of post Semua Jenis Relasi di Django

Semua Jenis Relasi di Django

Menggabungkan ForeignKey, ManyToMany, dan OneToOne di Django dalam satu studi kasus nyata, lengkap dengan pola akses relasi dan implikasinya ke bentuk JSON API.

Semua Jenis Relasi di Django

Sebelum memulai materi mengenai gambaran relasi pada django ini, baca ketiga lesson relasi berikut terlebih dahulu:

Di materi ini kita tidak memperkenalkan konsep baru. Materi ini menggabungkan semua konsep relasi ke dalam satu contoh agar kita dapat melihat bagaimana penerapan ketiga tipe relasi dalam skenario dunia nyata.


Skenario: Platform Kursus Online

Bayangkan mini Udemy atau Coursera. Kurang lebihnya seperti ini:

  • Setiap user memiliki tepat satu profile (bio, website) → OneToOneField
  • Seorang instructor (user) membuat banyak courseForeignKey
  • Setiap course punya banyak lessonForeignKey
  • Sebuah course punya banyak student, dan seorang student bisa mengambil banyak courseManyToManyField (dengan model through, karena kita ingin melacak tanggal enrollment dan completion)

Ketiga tipe relasi ada dalam satu app.


Peta Visual

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
User (built-in Django)
  ├──[OneToOne]───────▶  Profile                       → user.profile
  ├──[ForeignKey]─────▶  Course                        → user.courses_teaching.all()
  │                        │
  │                        ├──[ForeignKey]──▶  Lesson  → course.lessons.all()
  │                        │
  │                        └──[M2M through]──▶ User    → course.students.all()
  │                              │
  │                              └── Enrollment        → (tabel junction)
  └──[FK from Enrollment]──▶  Enrollment               → user.enrollments.all()

Model Lengkap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# courses/models.py

from django.conf import settings
from django.db import models


class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='profile',
    )
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)

    def __str__(self):
        return f"Profile of {self.user.username}"


class Course(models.Model):
    instructor = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='courses_teaching',
    )
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    students = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        through='Enrollment',
        related_name='courses_enrolled',
        blank=True,
    )
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title


class Lesson(models.Model):
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='lessons',
    )
    title = models.CharField(max_length=200)
    content = models.TextField()
    order = models.PositiveIntegerField(default=0)

    class Meta:
        ordering = ['order']

    def __str__(self):
        return f"{self.course.title} - {self.title}"


class Enrollment(models.Model):
    student = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='enrollments',
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='enrollments',
    )
    enrolled_at = models.DateTimeField(auto_now_add=True)
    completed = models.BooleanField(default=False)

    class Meta:
        unique_together = ['student', 'course']

    def __str__(self):
        return f"{self.student} enrolled in {self.course}"

Perhatikan polanya

RelationshipKita belajar ini diContoh di sini
OneToOneFieldOneToOneProfile.user, singular related_name='profile'
ForeignKeyForeignKeyCourse.instructor, Lesson.course, related_name berbentuk plural
ManyToManyField + throughManyToManyCourse.students via Enrollment
unique_togetherManyToManyMencegah enrollment duplikat
on_delete=CASCADEForeignKeyPada setiap FK dan OneToOne
blank=True pada M2MManyToManyCourse boleh punya nol student

Tidak ada hal baru, semuanya hanya digabungkan.


App ini punya empat relasi yang menunjuk ke User. Jika ada yang memakai related_name yang sama, Django akan memunculkan error bentrok. Mari lihat bagaimana masing-masing memberi accessor yang jelas dan berbeda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
user = User.objects.get(username='alice')

# OneToOne, singular
user.profile                    # object Profile milik Alice

# ForeignKey, plural, spesifik per peran
user.courses_teaching.all()     # course di mana Alice adalah instructor

# ManyToMany (via through), plural, spesifik per peran
user.courses_enrolled.all()     # course di mana Alice adalah student

# ForeignKey dari Enrollment, plural
user.enrollments.all()          # object Enrollment milik Alice (dengan tanggal dan status)

Setiap nama memberi tahu dengan tepat data apa yang diakses. Inilah kenapa kita menekankan pemilihan related_name yang baik. Pada app dengan banyak relasi, clarity dapat mencegah bug.


Perbandingan Sekilas dari Tiga Relasi

ForeignKeyManyToManyFieldOneToOneField
AnalogiFolder → FilesBooks ↔ TagsPerson ↔ Passport
KonsepSatu parent, banyak childBanyak ke banyakTepat satu ke satu
Implementasi DBKolom pada tabel child (parent_id)Tabel junction terpisahKolom dengan constraint UNIQUE
Model mana yang menyimpannya?Sisi “many” (child)Bisa di sisi mana pun (taruh di “owner” yang paling natural)Sisi “extension”
Akses forwardSatu object (post.category)QuerySet (book.tags.all())Satu object (profile.user)
Akses reverseQuerySet (category.posts.all())QuerySet (tag.books.all())Satu object (user.profile)
Konvensi related_namePlural ('posts')Plural, spesifik per peran ('courses_enrolled')Singular ('profile')
on_delete wajib?YaTidakYa
null=True valid?Ya (FK opsional)Tidak (tidak pernah)Ya (relasi opsional)
Set saat pembuatan?Ya (Post(category=cat))Tidak (pakai .add() setelahnya)Ya (Profile(user=u))

Bagaimana Relasi Mempengaruhi Serialization

Saat ini kita belum membangun serializer, tetapi memahami bagaimana relasi tampil di JSON akan membantu kita melihat kenapa desain model itu penting.

ForeignKey: tiga opsi

1
2
3
4
5
6
7
8
// Opsi A: Hanya ID (default)
{"id": 1, "title": "Python 101", "instructor": 3}

// Opsi B: Object nested (paling umum untuk API)
{"id": 1, "title": "Python 101", "instructor": {"id": 3, "username": "alice"}}

// Opsi C: Representasi string (__str__)
{"id": 1, "title": "Python 101", "instructor": "alice"}

ManyToMany: dua opsi

1
2
3
4
5
6
7
8
// Opsi A: List ID
{"id": 1, "title": "Python 101", "students": [1, 3, 7]}

// Opsi B: List object nested
{"id": 1, "title": "Python 101", "students": [
    {"id": 1, "username": "bob"},
    {"id": 3, "username": "charlie"}
]}

ManyToMany dengan through dengan data tambahan

1
2
3
4
{"id": 1, "title": "Python 101", "enrollments": [
    {"student": "bob", "enrolled_at": "2026-04-07T10:30:00Z", "completed": false},
    {"student": "charlie", "enrolled_at": "2026-04-07T11:00:00Z", "completed": true}
]}

OneToOne, sering di-flatten ke parent

1
2
3
4
5
// Daripada dinest...
{"id": 3, "username": "alice", "profile": {"bio": "Python dev", "website": "https://alice.dev"}}

// ...kita bisa flatten:
{"id": 3, "username": "alice", "bio": "Python dev", "website": "https://alice.dev"}

Hubungan antara desain model dan kerja serializer

Keputusan modelKonsekuensi di serializer
Pilihan related_name yang baikKode serializer jadi lebih bersih dan mudah dibaca
Model through pada M2MButuh custom nested serializer (lebih banyak kerja, tapi data lebih kaya)
OneToOneField dengan related_nameBisa di-flatten ke parent serializer secara natural
Method __str__ pada setiap modelMemberi opsi cepat untuk StringRelatedField

Bentuk model kita menentukan bentuk API kita. Inilah kenapa perlu memiliki dasar pengetahuan yang baik mengenai relasi sebelum mempelajari serializer.


Latihan

Latihan ini membangun app courses dengan ketiga tipe relasi. Jika kita sudah menyelesaikan materi ForeignKey, ManyToMany, dan OneToOne, semua konsep di sini seharusnya sudah familiar.

Buat app dan daftarkan

1
python manage.py startapp courses

Tambahkan 'courses' ke INSTALLED_APPS di config/settings.py.

Catatan: Jika kita sudah punya app profiles yang terdaftar dari materi sebelumnya, kita akan punya dua model Profile. Untuk menghindari konflik, kita bisa:

  • Hapus Profile dari app profiles dan pakai yang ini di courses
  • Atau lewati model Profile di courses dan import dari profiles saja
  • Atau cukup unregister app profiles saat mengerjakan latihan ini

Untuk kesederhanaan di project belajar ini, punya keduanya tetap aman karena ada di app berbeda dengan tabel database berbeda. Pilih opsi 3, berikan tanda # di ‘profiles’ pada settings.py

Tulis modelnya

Copy model dari Bagian 3 ke courses/models.py.

Buat dan apply migrations

1
2
python manage.py makemigrations courses
python manage.py migrate

Coba skenario lengkap di shell

1
python manage.py shell
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
from django.contrib.auth.models import User
from courses.models import Profile, Course, Lesson, Enrollment

# ============================================================
# SETUP: Buat users
# ============================================================
alice = User.objects.create_user('alice_instructor', password='test123')
bob = User.objects.create_user('bob_student', password='test123')
charlie = User.objects.create_user('charlie_student', password='test123')


# ============================================================
# OneToOne: Buat profiles
# ============================================================
alice_profile = Profile.objects.create(
    user=alice,
    bio='Python instructor with 5 years of experience',
    website='https://alice.dev',
)
bob_profile = Profile.objects.create(
    user=bob,
    bio='Aspiring web developer',
)

# Akses
print(alice.profile.bio)       # "Python instructor with 5 years of experience"
print(bob.profile.website)     # "" (kosong)


# ============================================================
# ForeignKey: Alice membuat courses, courses punya lessons
# ============================================================
py_course = Course.objects.create(
    instructor=alice,
    title='Python 101',
    description='Learn Python from scratch',
)
dj_course = Course.objects.create(
    instructor=alice,
    title='Django for Beginners',
    description='Build web apps with Django',
)

# Tambahkan lessons
Lesson.objects.create(course=py_course, title='Variables', content='...', order=1)
Lesson.objects.create(course=py_course, title='Functions', content='...', order=2)
Lesson.objects.create(course=py_course, title='Classes', content='...', order=3)
Lesson.objects.create(course=dj_course, title='Models', content='...', order=1)
Lesson.objects.create(course=dj_course, title='Views', content='...', order=2)

# Traverse
print(alice.courses_teaching.count())        # 2
print(py_course.lessons.count())             # 3
print(py_course.instructor.profile.bio)      # lewat FK lalu OneToOne!


# ============================================================
# ManyToMany (through): Enroll students
# ============================================================
Enrollment.objects.create(student=bob, course=py_course)
Enrollment.objects.create(student=bob, course=dj_course)
Enrollment.objects.create(student=charlie, course=py_course)

# Query dari kedua sisi
print(py_course.students.all())         # Bob dan Charlie
print(bob.courses_enrolled.all())       # Python 101 dan Django for Beginners

# Akses data melalui model through
enrollment = Enrollment.objects.get(student=bob, course=py_course)
print(enrollment.enrolled_at)           # timestamp
print(enrollment.completed)             # False

# Tandai sebagai selesai
enrollment.completed = True
enrollment.save()


# ============================================================
# GAMBARAN BESAR: Traverse semuanya dari satu titik awal
# ============================================================
for course in alice.courses_teaching.all():
    student_count = course.students.count()
    lesson_count = course.lessons.count()
    completed_count = course.enrollments.filter(completed=True).count()

    print(f"\n  Course: {course.title}")
    print(f"  Students: {student_count} ({completed_count} completed)")
    print(f"  Lessons:")
    for lesson in course.lessons.all():
        print(f"    {lesson.order}. {lesson.title}")

# Output yang diharapkan:

#   Course: Python 101
#   Students: 2 (1 completed)
#   Lessons:
#     1. Variables
#     2. Functions
#     3. Classes
#
#   Course: Django for Beginners
#   Students: 1 (0 completed)
#   Lessons:
#     1. Models
#     2. Views

Simpulan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ForeignKey              →  "banyak child, satu parent", child menyimpan referensinya
ManyToManyField         →  "many-to-many", tabel junction menghubungkan kedua sisi
ManyToManyField+through →  tabel junction dengan data tambahan (tanggal, status, dll.)
OneToOneField           →  "tepat satu", seperti FK dengan UNIQUE, reverse mengembalikan single object

related_name (plural)   →  ForeignKey dan M2M: user.courses_teaching.all()
related_name (singular) →  OneToOne: user.profile

on_delete               →  "apa yang terjadi pada child saat parent dihapus?"
unique_together         →  mencegah baris duplikat pada tabel junction
blank=True pada M2M     →  relasi bersifat opsional (nol koneksi diperbolehkan)

.add() / .remove()      →  hanya untuk M2M sederhana (bukan yang pakai through)
through model           →  buat/hapus object intermediate secara langsung

Foto cover oleh lmn_fotografie dari Unsplash

Dibawah Lisensi CC BY-NC-SA 4.0
Dibangun dengan Hugo
Tema Stack dirancang oleh Jimmy