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.
Bayangkan mini Udemy atau Coursera. Kurang lebihnya seperti ini:
- Setiap user memiliki tepat satu profile (bio, website) → OneToOneField
- Seorang instructor (user) membuat banyak course → ForeignKey
- Setiap course punya banyak lesson → ForeignKey
- Sebuah course punya banyak student, dan seorang student bisa mengambil banyak course → ManyToManyField (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
| Relationship | Kita belajar ini di | Contoh di sini |
|---|
| OneToOneField | OneToOne | Profile.user, singular related_name='profile' |
| ForeignKey | ForeignKey | Course.instructor, Lesson.course, related_name berbentuk plural |
| ManyToManyField + through | ManyToMany | Course.students via Enrollment |
unique_together | ManyToMany | Mencegah enrollment duplikat |
on_delete=CASCADE | ForeignKey | Pada setiap FK dan OneToOne |
blank=True pada M2M | ManyToMany | Course 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
| ForeignKey | ManyToManyField | OneToOneField |
|---|
| Analogi | Folder → Files | Books ↔ Tags | Person ↔ Passport |
| Konsep | Satu parent, banyak child | Banyak ke banyak | Tepat satu ke satu |
| Implementasi DB | Kolom pada tabel child (parent_id) | Tabel junction terpisah | Kolom dengan constraint UNIQUE |
| Model mana yang menyimpannya? | Sisi “many” (child) | Bisa di sisi mana pun (taruh di “owner” yang paling natural) | Sisi “extension” |
| Akses forward | Satu object (post.category) | QuerySet (book.tags.all()) | Satu object (profile.user) |
| Akses reverse | QuerySet (category.posts.all()) | QuerySet (tag.books.all()) | Satu object (user.profile) |
Konvensi related_name | Plural ('posts') | Plural, spesifik per peran ('courses_enrolled') | Singular ('profile') |
on_delete wajib? | Ya | Tidak | Ya |
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 model | Konsekuensi di serializer |
|---|
Pilihan related_name yang baik | Kode serializer jadi lebih bersih dan mudah dibaca |
Model through pada M2M | Butuh custom nested serializer (lebih banyak kerja, tapi data lebih kaya) |
OneToOneField dengan related_name | Bisa di-flatten ke parent serializer secara natural |
Method __str__ pada setiap model | Memberi 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
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