Relasi ManyToMany
Baca Relasi ForeignKey di Django terlebih dahulu jika belum. Saat kita mulai mempelajari relasi ManyToMany ini, sebaiknya kita sudah nyaman dengan:
- Membuat model dengan
ForeignKey - Melakukan traverse relasi child → parent dan relasi parent → children
- Konsep
related_name
Yang Akan Kita Pelajari
Kali ini kita hanya akan fokus pada satu konsep saja: ManyToManyField, yang merepresentasikan relasi many-to-many. Setelah mempelajari ini, kita akan mengerti bagaimana menghubungkan dua model di mana kedua sisi bisa punya banyak sisi lainnya.
Analogi
Bayangkan buku dan tag di sebuah toko buku.
- Satu buku bisa punya banyak tag: “Python”, “Beginner”, “Programming”
- Satu tag bisa ada pada banyak buku: tag “Beginner” bisa disematkan pada beberapa judul berbeda
Keduanya bukan parent maupun child, ini hubungan yang “setara” (peer relationship).
1
2
3
4
5
| Book: "Python Crash Course" ←──memiliki──→ Tag: "Python"
Book: "Python Crash Course" ←──memiliki──→ Tag: "Beginner"
Book: "Django for Beginners" ←──memiliki──→ Tag: "Django"
Book: "Django for Beginners" ←──memiliki──→ Tag: "Beginner"
Book: "Django for Beginners" ←──memiliki──→ Tag: "Python"
|
Kenapa tidak menggunakan ForeignKey saja?
ForeignKey hanya mengizinkan satu parent per child-nya. Buku tidak punya satu tag saja. tapi bisa jadi punya banyak tag. Tag juga tidak “milik” satu buku saja. Oleh sebab itu, kita butuh tipe relasi yang berbeda.
Buat App
1
2
3
| cd my-drf-project
source .venv/bin/activate
python manage.py startapp bookstore
|
Tambahkan ke config/settings.py seperti biasa.
1
2
3
4
5
6
7
8
9
10
11
12
| INSTALLED_APPS = [
# ... Django built-ins ...
# Third-party
'rest_framework',
# Your apps
'books',
'articles',
'blog',
'bookstore',
]
|
Model (ManyToMany sederhana)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # bookstore/models.py
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
tags = models.ManyToManyField(
Tag,
related_name='books',
blank=True,
)
def __str__(self):
return self.title
|
Satu ManyToManyField pada model Book sudah membuat seluruh relasi many-to-many. Sekarang mari kita pelajari satu persatu.
Tag - model target
Argumen pertama sama seperti ForeignKey: model yang dihubungkan.
Ini untuk membuat kita bisa pergi dari Tag ke semua Books yang memiliki tag:
1
2
| tag = Tag.objects.get(name='Python')
tag.books.all() # semua buku yang diberi tag "Python"
|
Tanpa menggunakan related_name, Django secara otomatis akan generate book_set. Sama seperti ForeignKey, selalu set related_name secara eksplisit agar lebih enak dibaca.
blank=True - buku boleh tanpa tag
Ini memberi tahu Django pada saat validasi bahwa field ini bersifat opsional. Sebuah buku diperbolehkan untuk tidak memiliki tag sama sekali.
Perhatikan: jangan pakai null=True pada ManyToManyField karena Django akan error, karena relasi ManyToMany tidak membuat kolom baru pada tabel Book (akan dijelaskan di bawah), jadi tidak ada yang bisa NULL.
Di model mana kita letakkan field ManyToMany?
Kita bisa letakkan ManyToManyField ini di sisi manapun, baik di Book dan pointing ke Tag, maupun sebaliknya. Hasilnya akan sama saja.
Tetapi secara best practice, taruh pada model yang “memiliki” model lainnya. “Sebuah Book memiliki Tag” lebih natural daripada “Tag ini dimiliki oleh Book”
Apa yang Terjadi di Database
Perbedaan paling penting antara ForeignKey dan ManyToManyField adalah, ManyToManyField tidak membuat kolom di kedua tabel. Django akan membuat tabel ketiga yang juga disebut junction table yang menyimpan relasi keduanya.
Berbeda dengan ForeignKey: ManyToManyField tidak membuat kolom di kedua tabel. Django membuat tabel ketiga (junction table) yang menyimpan koneksi.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| bookstore_book table: bookstore_tag table:
+----+---------------------+ +----+-----------+
| id | title | | id | name |
+----+---------------------+ +----+-----------+
| 1 | Python Crash Course | | 1 | Python |
| 2 | Django for Beginners| | 2 | Beginner |
| 3 | Learn SQL | | 3 | Django |
+----+---------------------+ +----+-----------+
bookstore_book_tags table (auto-generated junction table):
+----+---------+--------+
| id | book_id | tag_id |
+----+---------+--------+
| 1 | 1 | 1 | ← "Python Crash Course" memiliki tag "Python"
| 2 | 1 | 2 | ← "Python Crash Course" memiliki tag "Beginner"
| 3 | 2 | 3 | ← "Django for Beginners" memiliki tag "Django"
| 4 | 2 | 2 | ← "Django for Beginners" memiliki tag "Beginner"
| 5 | 2 | 1 | ← "Django for Beginners" memiliki tag "Python"
+----+---------+--------+
|
Setiap baris di junction table ini merepresentasikan *satu koneksi" antara buku dan tag. Nama tabel ini juga digenerate secara otomatis dengan format {app}_{model}_{field}, sehingga dalam contoh kita menjadi bookstore_book_tags.
Kita tidak akan pernah berinteraksi langsung dengan tabel ini karena Django sudah menyediakan API yang clean.
Menggunakan dengan ManyToMany
Berbeda dengan ForeignKey, kita tidak bisa set relasi ManyToMany saat membuat object. Kita harus membuat object-nya terlebih dahulu, baru kemudian menambahkan relasinya:
1
2
3
4
5
6
| # ❌ Tidak bisa
book = Book.objects.create(title='Python Crash Course', author='Eric', tags=[tag1, tag2])
# ✅ Cara yang benar
book = Book.objects.create(title='Python Crash Course', author='Eric')
book.tags.add(tag1, tag2)
|
Karena book-nya harus punya id terlebih dahulu sebelum Django bisa membuat baris di tabel junction.
API lengkapnya
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
| # Asumsikan bahwa kita memiliki tag ini
python_tag = Tag.objects.create(name='Python')
beginner_tag = Tag.objects.create(name='Beginner')
django_tag = Tag.objects.create(name='Django')
# Membuat object bukunya
book = Book.objects.create(title='Python Crash Course', author='Eric Matthes')
# --- Menambahkan Tag ---
book.tags.add(python_tag) # menambahkan satu tag
book.tags.add(python_tag, beginner_tag) # menambahkan banyak tag (duplikat akan diabaikan)
# --- Query ---
book.tags.all() # <QuerySet [<Tag: Python>, <Tag: Beginner>]>
book.tags.count() # 2
# --- Reverse: tag → books ---
python_tag.books.all() # semua buku dengan tag "Python"
python_tag.books.count() # jumlah buku dengan tag ini
# --- Menghapus ---
book.tags.remove(beginner_tag) # menghapus satu tag
book.tags.clear() # menghapus SEMUA tag dari buku ini
# --- Merubah tag (replace semuanya sekaligus) ---
book.tags.set([python_tag, django_tag]) # replace semua tag dengan ini
# --- Check membership ---
book.tags.filter(pk=python_tag.pk).exists() # True atau False
|
Perbandingan akses ManyToManyField dengan ForeignKey
| Operation | ForeignKey | ManyToManyField |
|---|
| Set saat creation | Post.objects.create(category=cat) | Tidak bisa. Gunakan .add() setelah object dibuat |
| Access forward | post.category (single object) | book.tags.all() (QuerySet) |
| Access reverse | category.posts.all() (QuerySet) | tag.books.all() (QuerySet) |
| Add | N/A (set via field) | book.tags.add(tag) |
| Remove | N/A (rubah field) | book.tags.remove(tag) |
| Clear all | N/A | book.tags.clear() |
Kuncinya adalah: Dengan ManyToMany, kedua arah akan mengembalikan hasil berupa QuerySet. Jadi pada ManyToMany tidak ada sisi “single object” seperti pada Foreignkey.
The simple ManyToMany above only tracks which book has which tag. There’s no extra information about the connection itself.
But what if you needed to know when a tag was added to a book, or who added it? You can’t add columns to Django’s auto-generated junction table. Instead, you define the junction table yourself.
ManyToMany yang simple hanya mencatat buku apa yang terhubung dengan tag apa. Tidak ada informasi tambahan apapun mengenai relasi tersebut.
Lalu bagaimana jika kita perlu mengetahui kapan sebuah tag ditambahkan ke bukunya, atau siapa yang menambahkannya?Kita tidak bisa menambahkan kolom ke junction table yang di generate oleh Django tersebut. Dalam kasus seperti itu, kita akan mendefinisikan junction tablenya sendiri.
Contoh: Status Baca Sebuah Buku
Anggap kita mau membuat agar user dapat menambahkan buku ke reading list-nya mereka dengan status (want to read, reading, finished) dan tanggal mereka menambahkan statusnya. Ini adalah relasi many-to-many antara user/Reader dengan Book, tetapi di relasi tersebut memiliki informasi tambahan.
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
| from django.conf import settings
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
tags = models.ManyToManyField(
Tag,
related_name='books',
blank=True,
)
readers = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through='ReadingListEntry',
related_name='reading_list_books',
blank=True,
)
def __str__(self):
return self.title
class ReadingListEntry(models.Model):
class Status(models.TextChoices):
WANT_TO_READ = 'want_to_read', 'Want to Read'
READING = 'reading', 'Currently Reading'
FINISHED = 'finished', 'Finished'
reader = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='reading_list_entries',
)
book = models.ForeignKey(
Book,
on_delete=models.CASCADE,
related_name='reading_list_entries',
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.WANT_TO_READ,
)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['reader', 'book']
verbose_name_plural = 'reading list entries'
def __str__(self):
return f"{self.reader} - {self.book} ({self.status})"
|
Apa yang Berubah?
model Book sekarang memiliki readers = models.ManyToManyField(..., through='ReadingListEntry'). Parameter through memberitahu Django untuk “gunakan junction table yang kita buat” daripada generate itu secara otomatis.
ReadingListEntry adalah custom junction table dan memiliki:
reader - ForeignKey ke Userbook - ForeignKey ke Bookstatus - informasi/data tambahan mengenai relasinyaadded_at - kapan relasi ini dibuat
unique_together = ['reader', 'book']: Seorang user hanya dapat menambahkan buku yang sama ke reading list-nya satu kali saja.
Tampilan database
1
2
3
4
5
6
7
8
| bookstore_readinglistentry:
+----+-----------+---------+--------------+---------------------+
| id | reader_id | book_id | status | added_at |
+----+-----------+---------+--------------+---------------------+
| 1 | 1 | 1 | reading | 2026-04-07 10:30:00 |
| 2 | 1 | 2 | want_to_read | 2026-04-07 11:00:00 |
| 3 | 2 | 1 | finished | 2026-04-07 09:00:00 |
+----+-----------+---------+--------------+---------------------+
|
Menggunakan model yang memiliki “through”
Perbedaan terbesarnya adalah, kita tidak dapat menggunakan .add(), .remove(), atau .set() di ManyToMany manager jika menggunakan through. Kita harus create dan delete langsung dari penengah / intermediate-nya. Sebagai contoh:
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
| from django.contrib.auth.models import User
from bookstore.models import Book, ReadingListEntry
alice = User.objects.get(username='alice')
book = Book.objects.get(title='Python Crash Course')
# ❌ TIDAK bisa untuk model yang memiliki "through"
book.readers.add(alice)
# ✅ Buat melalui model intermediate-nya
ReadingListEntry.objects.create(
reader=alice,
book=book,
status=ReadingListEntry.Status.READING,
)
# Query ManyToMany masih bisa digunakan secara normal
book.readers.all() # semua user yang memiliki buku ini di list mereka
alice.reading_list_books.all() # semua buku yang ada di reading list Alice
# Tapi sekarang kita bisa query model "through" untuk tambahan informasi
entry = ReadingListEntry.objects.get(reader=alice, book=book)
print(entry.status) # "reading"
print(entry.added_at) # waktu status-nya ditambahkan
# Update status
entry.status = ReadingListEntry.Status.FINISHED
entry.save()
# Remove dari reading list
entry.delete()
|
Kapan perlu menggunakan “through”?
| Skenario | Yang Digunakan |
|---|
| Kita hanya perlu tau kalau “A memiliki relasi ke B” | ManyToManyField sederhana |
| Relasinya memiliki data (tanggal, status, jumlah) | model through |
Contoh ManyToMany sederhana:
- Tag pada buku (Bukunya ada tag atau tidak)
- Genre pada film
- Skill pada resume/CV
contoh model yang menggunakan through:
- Reading list (ada status dan date added)
- Bahan pada resep (memiliki jumlah: “200 gram tepung”)
- Keikutsertaan kursus (ada grade dan progress)
Untuk latihan, kita akan mengimplementasikan keduanya: baik tag sederhana tanpa data dan juga reading list (dengan informasi/data tambahan)
File Models Yang Sudah 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
| # bookstore/models.py
from django.conf import settings
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
tags = models.ManyToManyField(
Tag,
related_name='books',
blank=True,
)
readers = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through='ReadingListEntry',
related_name='reading_list_books',
blank=True,
)
def __str__(self):
return self.title
class ReadingListEntry(models.Model):
class Status(models.TextChoices):
WANT_TO_READ = 'want_to_read', 'Want to Read'
READING = 'reading', 'Currently Reading'
FINISHED = 'finished', 'Finished'
reader = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='reading_list_entries',
)
book = models.ForeignKey(
Book,
on_delete=models.CASCADE,
related_name='reading_list_entries',
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.WANT_TO_READ,
)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['reader', 'book']
verbose_name_plural = 'reading list entries'
def __str__(self):
return f"{self.reader} - {self.book} ({self.status})"
|
Latihan dan Praktek
Buat app dan daftarkan ke settings.py
1
| python manage.py startapp bookstore
|
Tambahkan 'bookstore' ke INSTALLED_APPS di config/settings.py.
Buat models
Gunakan file models yang lengkap dari part 8 ke bookstore/models.py.
Buat dan jalankan migrations
1
2
| python manage.py makemigrations bookstore
python manage.py migrate
|
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
| from bookstore.models import Tag, Book
# --- Buat tag ---
python_tag = Tag.objects.create(name='Python')
beginner_tag = Tag.objects.create(name='Beginner')
django_tag = Tag.objects.create(name='Django')
web_tag = Tag.objects.create(name='Web')
# --- Buat book ---
book1 = Book.objects.create(title='Python Crash Course', author='Eric Matthes')
book2 = Book.objects.create(title='Django for Beginners', author='William Vincent')
book3 = Book.objects.create(title='Learn SQL', author='Alan Beaulieu')
# --- Add tags to books ---
book1.tags.add(python_tag, beginner_tag)
book2.tags.add(django_tag, python_tag, beginner_tag, web_tag)
book3.tags.add(beginner_tag)
# --- Query dari sisi Book ---
print(book1.tags.all()) # <QuerySet [<Tag: Python>, <Tag: Beginner>]>
print(book2.tags.count()) # 4
# --- Query dari sisi Tag (reverse) ---
print(beginner_tag.books.all()) # Semua / 3 book
print(python_tag.books.all()) # book1 dan book2
print(python_tag.books.count()) # 2
# --- Remove tag ---
book2.tags.remove(web_tag)
print(book2.tags.count()) # 3 (tadinya 4)
# --- Replace semua tag sekaligus ---
book3.tags.set([beginner_tag, python_tag]) # Sekarang jadi 2 tag
print(book3.tags.all()) # <QuerySet [<Tag: Beginner>, <Tag: Python>]>
# --- Clear semua tag ---
book3.tags.clear()
print(book3.tags.count()) # 0
|
Mencoba ManyToMany dengan “through” (reading list) di shell python
Sebelum memulai ini, exit dulu shell sebelumnya dengan perintah exit() dan kemudian jalankan kembali 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
| from django.contrib.auth.models import User
from bookstore.models import Book, ReadingListEntry
# Buat user (atau gunakan yang ada)
alice = User.objects.create_user('alice_reader', password='test123')
bob = User.objects.create_user('bob_reader', password='test123')
book1 = Book.objects.get(title='Python Crash Course')
book2 = Book.objects.get(title='Django for Beginners')
# --- Tambahkan buku ke reading list (gunakan ReadingListEntry) ---
ReadingListEntry.objects.create(
reader=alice, book=book1,
status=ReadingListEntry.Status.READING,
)
ReadingListEntry.objects.create(
reader=alice, book=book2,
status=ReadingListEntry.Status.WANT_TO_READ,
)
ReadingListEntry.objects.create(
reader=bob, book=book1,
status=ReadingListEntry.Status.FINISHED,
)
# --- Query ManyToMany (sama seperti di ManyToMany sederhana) ---
print(book1.readers.all()) # Alice dan Bob
print(alice.reading_list_books.all()) # book1 dan book2
# --- Query model "through" untuk informasi/data tambahan ---
entry = ReadingListEntry.objects.get(reader=alice, book=book1)
print(entry.status) # "reading"
print(entry.added_at) # timestamp
# --- Update status ---
entry.status = ReadingListEntry.Status.FINISHED
entry.save()
print(entry.status) # "finished"
# --- Mencoba entry yang duplicate ---
try:
ReadingListEntry.objects.create(reader=alice, book=book1)
except Exception as e:
print(f"Error: {e}") # IntegrityError , karena harus unique_together
# --- Remove dari reading list ---
entry.delete()
print(book1.readers.all()) # jadi Bob saja
|
Rangkuman
1
2
3
4
5
6
7
8
9
10
| ManyToManyField → "many-to-many", kedua sisi bukan parent
Junction table → Django secara otomatis membuat tabel ketiga untuk menyimpan relasi
.add() / .remove() → cara mengelola relasi ManyToMany sederhana
.set() → mengganti semua koneksi sekaligus
.clear() → menghapus semua koneksi
blank=True → relasi bersifat opsional (boleh 0 koneksi)
null=True → JANGAN gunakan pada ManyToManyField
through='ModelName' → gunakan tabel junction custom ketika relasi menyimpan data tambahan
unique_together → mencegah duplikat di tabel junction
through + .add() → TIDAK bisa. Harus membuat model penengah (intermediate) secara langsung
|
Foto cover oleh hannahbusing dari Unsplash