Dua bentuk relasi sebelumnya punya sisi yang jelas: pada satu-ke-satu kedua sisi setara, pada satu-ke-banyak satu sisi menjadi induk dan satu sisi menjadi anak. Kehidupan data tidak selalu serapat itu. Seorang mahasiswa mengambil banyak kursus dalam satu semester, dan satu kursus diambil oleh banyak mahasiswa sekaligus — kedua sisi sama-sama “banyak”, tidak ada tabel yang layak disebut induk. Relasi banyak-ke-banyak (many-to-many) seperti ini tidak bisa dibangun dengan satu kolom foreign key, dan upaya memaksanya selalu berakhir pada duplikasi data. Chapter ini membuktikan kegagalan itu secara nyata, lalu memperkenalkan solusinya: tabel perantara yang memecah relasi menjadi dua relasi satu-ke-banyak.

Konsep Many-to-Many #

Relasi banyak-ke-banyak menyatakan bahwa satu baris entitas A boleh berpasangan dengan banyak baris entitas B, dan satu baris entitas B juga boleh berpasangan dengan banyak baris entitas A. Relasi bentuk ini muncul di hampir semua domain:

Pasangan entitasKalimat kebutuhannya
mahasiswa dan kursussatu mahasiswa mengambil banyak kursus; satu kursus diambil banyak mahasiswa
pesanan dan produksatu pesanan berisi banyak produk; satu produk terjual di banyak pesanan
tugas dan labelsatu tugas boleh berlabel banyak; satu label dipakai banyak tugas
buku dan penulissatu buku bisa ditulis beberapa penulis; satu penulis menulis banyak buku
pasien dan doktersatu pasien dirawat beberapa dokter; satu dokter menangani banyak pasien

Pada relasi satu-ke-banyak kita menempatkan foreign key di sisi anak karena satu baris anak hanya butuh menyimpan satu penunjuk. Pada banyak-ke-banyak sisi anak tidak ada: keduanya butuh menyimpan “banyak” penunjuk sekaligus. Konflik inilah yang membuat penempatan kolom foreign key tunggal selalu gagal — dan kegagalan itu layak dilihat langsung, bukan sekadar dipercaya.

Percobaan Gagal: Foreign Key di Sisi Mahasiswa #

Upaya pertama yang paling sering terjadi: memberi tabel mahasiswa satu kolom kursus_id yang menunjuk tabel kursus. Kita bangun percobaan tersebut di database latihan belajar_rdb:

sql
DROP DATABASE IF EXISTS belajar_rdb;
CREATE DATABASE belajar_rdb;
USE belajar_rdb;

CREATE TABLE kursus (
  id INT AUTO_INCREMENT PRIMARY KEY,
  kode VARCHAR(10) NOT NULL UNIQUE,
  nama VARCHAR(50) NOT NULL,
  sks INT NOT NULL
);

CREATE TABLE mahasiswa_a (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  kursus_id INT NOT NULL,
  CONSTRAINT fk_mhs_a_kursus
    FOREIGN KEY (kursus_id) REFERENCES kursus(id)
);

INSERT INTO kursus (kode, nama, sks) VALUES
  ('BD001', 'Basis Data', 3),
  ('AL001', 'Algoritma dan Pemrograman', 3),
  ('JK001', 'Jaringan Komputer', 2);

INSERT INTO mahasiswa_a (nama, kursus_id) VALUES
  ('Siti Rahayu', 1),
  ('Budi Santoso', 1);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

Desain ini bekerja selama setiap mahasiswa mengambil tepat satu kursus. Masalah muncul ketika Siti ingin mengambil kursus kedua: satu baris mahasiswa_a hanya menyimpan satu kursus_id, sehingga pilihan yang tersedia hanya menimpa nilai lama (menghapus jejak kursus pertama) atau menambah baris baru untuk Siti. Kita coba pilihan kedua:

sql
INSERT INTO mahasiswa_a (nama, kursus_id) VALUES ('Siti Rahayu', 2);

SELECT id, nama, kursus_id FROM mahasiswa_a ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

+----+--------------+-----------+
| id | nama         | kursus_id |
+----+--------------+-----------+
|  1 | Siti Rahayu  |         1 |
|  2 | Budi Santoso |         1 |
|  3 | Siti Rahayu  |         2 |
+----+--------------+-----------+
3 rows in set (0.00 sec)

Siti kini muncul sebagai dua baris dengan dua id berbeda — identitasnya terduplikasi hanya demi menampung relasinya. Konsekuensi duplikasi identitas langsung terasa ketika data identitas berubah, misalnya Siti resmi mengganti nama setelah menikah:

sql
UPDATE mahasiswa_a SET nama = 'Siti Rahayu Kusuma' WHERE nama = 'Siti Rahayu';

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

Satu perintah UPDATE ternyata menyentuh dua baris — dan bila ada satu baris yang terlewat, database mendadak punya dua identitas untuk orang yang sama. Semakin banyak kursus yang diambil semakin parah duplikasinya: lima kursus berarti lima baris data pribadi Siti yang saling menyalin.

Percobaan Gagal: Foreign Key di Sisi Kursus #

Upaya kedua membalik arah: tabel kursus diberi kolom mahasiswa_id. Bentuk ini tidak perlu kita bangun untuk melihat kegagalannya, karena cacatnya simetris dengan percobaan pertama. Satu baris kursus hanya sanggup menyimpan satu mahasiswa_id, padahal kelas Basis Data dihadiri empat puluh mahasiswa — artinya baris Basis Data harus diduplikasi empat puluh kali, sekali per mahasiswa. Nama kursus, kode, dan bobot SKS tersimpan berulang di seluruh baris tersebut, dan penggantian nama kursus berubah menjadi UPDATE empat puluh baris. Kedua arah gagal dengan alasan yang sama: satu kolom foreign key menyimpan tepat satu nilai, sementara relasi banyak-ke-banyak menuntut banyak nilai di kedua sisi.

Solusi: Tabel Perantara #

Jalan keluarnya sederhana namun elegan: relasi banyak-ke-banyak tidak dihubungkan langsung, melainkan dipecah menjadi dua relasi satu-ke-banyak yang bertemu di sebuah tabel perantara. Tabel enrollment menyimpan satu baris per pasangan mahasiswa-kursus; setiap barisnya hanya menunjuk satu mahasiswa dan satu kursus — persis seperti yang mampu ditampung kolom foreign key.

Diagram di atas adalah bentuk konseptual relasinya: notasi }o--o{ berarti “banyak baris di kedua sisi”. Implementasi di MySQL membutuhkan tabel perantara sehingga diagramnya berubah menjadi dua relasi satu-ke-banyak:

Membaca relasinya baris demi baris: satu mahasiswa bisa muncul di banyak baris enrollment (satu-ke-banyak antara mahasiswa dan enrollment), dan satu kursus juga bisa muncul di banyak baris enrollment (satu-ke-banyak antara kursus dan enrollment). Kedua kolom penunjuk berada di tabel enrollment sebagai sisi anak ganda — aturan “foreign key di tabel anak” dari chapter sebelumnya tetap berlaku, hanya saja tabel perantaranya menjadi anak dari dua induk sekaligus. Nama Siti dan nama kursus masing-masing tetap tersimpan sekali; baris enrollment hanya menyimpan dua penunjuk plus data yang memang milik relasinya.

Mengenali Many-to-Many dari Kebutuhan #

Di atas kertas rancangan, relasi banyak-ke-banyak mudah dikenali dari pola kalimat kebutuhannya. Frasa pemicunya berbunyi “banyak X terhubung banyak Y” atau “setiap X bisa punya banyak Y dan setiap Y bisa punya banyak X”. Ketika kalimat semacam itu muncul, langkah perancangannya tiga: pindahkan relasi ke tabel perantara, beri tabel itu kolom penunjuk ke kedua entitas, lalu tanyakan apakah relasi tersebut punya atribut sendiri. Atribut relasi inilah yang sering terlewat — nilai ujian, tanggal mendaftar, atau jumlah unit yang dibeli tidak dimiliki mahasiswa, tidak dimiliki kursus, melainkan dimiliki pasangan keduanya. Implementasi lengkapnya — composite key, dua foreign key, penolakan pasangan ganda, dan query tiga tabel — kita bangun di chapter berikutnya.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_rdb;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Relasi banyak-ke-banyak kini terpetakan: satu foreign key tidak akan pernah cukup karena kedua sisi sama-sama menuntut “banyak”, dan solusinya adalah tabel perantara yang memecah relasi menjadi dua satu-ke-banyak. Peta konsepnya sudah kita pegang — tinggal mewujudkannya dalam CREATE TABLE dengan kunci komposit, mengisi datanya, dan membuktikan bahwa pasangan ganda ditolak serta query tiga tabel berjalan mulus. Chapter berikutnya membangun enrollment secara utuh sebagai junction table.