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:

js
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:

text
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:

  1. Array di kedua sisi — buku.penulisId dan penulis.bukuId menyimpan relasi yang sama dari dua sudut pandang; keduanya harus diperbarui beriringan.

  2. _id numerik sengaja — contoh ini memakai angka agar relasi mudah dibaca; di production, ObjectId bekerja sama persis.

  3. Buku kolaborasi — Filosofi Teras memuat dua penulisId sekaligus; 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:

js
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:

text
{ 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:

js
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:

text
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 #

AspekDua arah (array di kedua sisi)Satu arah + $lookup
Membaca arah “banyak” dari sisi pertamaSatu query pada document$lookup atau query containment
Membaca arah sebaliknyaSatu query pada document lawanQuery containment pada sisi penyimpan
Menambah relasi baruTulis di dua collectionTulis di satu document
KonsistensiBisa saling menyalahiSatu sumber kebenaran
Ukuran arrayTumbuh di dua tempatTumbuh di satu tempat
Kecocokan pola aksesKedua arah sama panasSatu 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 $lookup atau 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.