Relasi many-to-many muncul begitu satu entitas boleh berelasi dengan banyak entitas di kedua sisi: satu buku ditulis banyak penulis, satu penulis menulis banyak buku; siswa dan kursus juga bekerja begini. Tidak ada “induk” yang jelas seperti pada one-to-many, jadi pertanyaan desainnya bergeser: referensi disimpan di satu sisi saja, atau di kedua sisi sekaligus? Chapter ini menguji dua pendekatan itu dan menghitung harganya masing-masing.
Dua Arah: Array References di Kedua Sisi #
Pendekatan pertama menyimpan referensi di kedua sisi — buku memuat array penulisId, penulis memuat array bukuId. Setiap arah pembacaan menjadi satu query tanpa join:
use latihan_modeling
db.dropDatabase()
db.penulis.insertMany([
{ _id: 1, nama: "Tere Liye", bukuId: [101, 103] },
{ _id: 2, nama: "Leila S. Chudori", bukuId: [102] },
{ _id: 3, nama: "Dee Lestari", bukuId: [103] }
])
db.buku.insertMany([
{ _id: 101, judul: "Bumi", penulisId: [1] },
{ _id: 102, judul: "Laut Bercerita", penulisId: [2] },
{ _id: 103, judul: "Filosofi Teras", penulisId: [1, 3] }
])
db.buku.find({ penulisId: 1 }, { judul: 1 })
db.penulis.findOne({ _id: 1 })Output yang Dihasilkan:
switched to db latihan_modeling
{ ok: 1, dropped: 'latihan_modeling' }
{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
{ acknowledged: true, insertedIds: { '0': 101, '1': 102, '2': 103 } }
[ { _id: 101, judul: 'Bumi' }, { _id: 103, judul: 'Filosofi Teras' } ]
{ _id: 1, nama: 'Tere Liye', bukuId: [ 101, 103 ] }Query pertama menjawab “karya apa saja dari penulis 1” lewat containment pada array penulisId; query kedua menjawab arah sebaliknya — daftar buku penulis — langsung dari document penulisnya. Kedua halaman, halaman buku dan halaman penulis, sama-sama cepat.
Bedah struktur datanya:
Array di kedua sisi —
buku.penulisIddanpenulis.bukuIdmenyimpan relasi yang sama dari dua sudut pandang; keduanya harus diperbarui beriringan._idnumerik sengaja — contoh ini memakai angka agar relasi mudah dibaca; di production,ObjectIdbekerja sama persis.Buku kolaborasi —
Filosofi Terasmemuat duapenulisIdsekaligus; array menampung relasi banyak-ke-banyak dengan mulus.
Ada satu lensa yang belum kita hitung: growth. Penulis produktif dengan puluhan judul membuat array bukuId memanjang terus — masih aman untuk puluhan, tetapi untuk ratusan judul per penulis, array itu mulai membebani document penulis dan setiap penambahan karya baru menyentuhnya lagi.
Harga Dua Arah: Tulis Dua Kali #
Masalah muncul saat relasi baru dibuat. Buku baru dengan dua penulis menuntut tiga operasi tulis — satu insert buku, dua update array di sisi penulis:
db.buku.insertOne({ _id: 104, judul: "Supernova: Ksatria, Putri, dan Bintang Jatuh", penulisId: [2, 3] })
db.penulis.updateOne({ _id: 2 }, { $push: { bukuId: 104 } })
db.penulis.updateOne({ _id: 3 }, { $push: { bukuId: 104 } })
db.penulis.find({ _id: { $in: [2, 3] } })Output yang Dihasilkan:
{ acknowledged: true, insertedId: 104 }
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
[
{ _id: 2, nama: 'Leila S. Chudori', bukuId: [ 102, 104 ] },
{ _id: 3, nama: 'Dee Lestari', bukuId: [ 103, 104 ] }
]Tiga operasi tulis untuk satu relasi, dan tidak ada yang menjamin ketiganya berhasil bersama — kalau aplikasi berhenti di tengah, array bukuId di penulis akan menyalahi penulisId di buku. Inilah risiko konsistensi bawaan dari menyimpan relasi yang sama di dua tempat; transaksi multi-document bisa menutupnya, tetapi dengan kompleksitas tambahan.
Satu Arah + $lookup #
Pendekatan kedua memilih satu sisi penyimpan referensi — cukup buku yang memuat penulisId — dan melayani arah sebaliknya lewat $lookup:
db.penulis.drop()
db.buku.drop()
db.penulis.insertMany([
{ _id: 1, nama: "Tere Liye" },
{ _id: 2, nama: "Leila S. Chudori" },
{ _id: 3, nama: "Dee Lestari" }
])
db.buku.insertMany([
{ _id: 101, judul: "Bumi", penulisId: [1] },
{ _id: 102, judul: "Laut Bercerita", penulisId: [2] },
{ _id: 103, judul: "Filosofi Teras", penulisId: [1, 3] }
])
db.penulis.aggregate([
{ $match: { _id: 1 } },
{ $lookup: { from: "buku", localField: "_id", foreignField: "penulisId", as: "karya" } },
{ $project: { nama: 1, "karya.judul": 1 } }
])Output yang Dihasilkan:
true
true
{ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
{ acknowledged: true, insertedIds: { '0': 101, '1': 102, '2': 103 } }
[
{
_id: 1,
nama: 'Tere Liye',
karya: [ { judul: 'Bumi' }, { judul: 'Filosofi Teras' } ]
}
]Informasi yang sama dengan model dua arah — penulis beserta daftar karyanya — kini diperoleh tanpa field bukuId yang harus dijaga. Menambahkan relasi baru hanya menuntut satu insert buku: field penulisId-nya langsung menjadi kebenaran tunggal, dan arah manapun bisa dilayani dari sana. Untuk relasi siswa–kursus, pola ini setara dengan “kursus menyimpan siswaIds” lalu daftar kursus seorang siswa dicari lewat $lookup atau query containment.
Perbandingan Dua Pendekatan #
| Aspek | Dua arah (array di kedua sisi) | Satu arah + $lookup |
|---|---|---|
| Membaca arah “banyak” dari sisi pertama | Satu query pada document | $lookup atau query containment |
| Membaca arah sebaliknya | Satu query pada document lawan | Query containment pada sisi penyimpan |
| Menambah relasi baru | Tulis di dua collection | Tulis di satu document |
| Konsistensi | Bisa saling menyalahi | Satu sumber kebenaran |
| Ukuran array | Tumbuh di dua tempat | Tumbuh di satu tempat |
| Kecocokan pola akses | Kedua arah sama panas | Satu arah dominan, arah lain jarang |
Rangkuman #
- Many-to-many bisa dimodelkan dengan array references di kedua sisi — kedua arah baca menjadi satu query, tetapi setiap relasi baru menuntut tulis ganda.
- Model dua arah berisiko array yang saling menyalahi bila pembaruan terputus di tengah; transaksi multi-document menutup risiko itu.
- Model satu arah menyimpan referensi di satu sisi saja dan melayani arah balik lewat
$lookupatau query containment — satu sumber kebenaran dengan biaya join. - Pilih dua arah hanya bila kedua arah sama panas di pola akses dan query balik terbukti mahal.
Semua relasi klasik sudah di meja. Chapter berikutnya membahas konsekuensi dari pilihan yang baru saja kita buat ketika data disalin ke banyak tempat: duplikasi.