Chapter sebelumnya menunjukkan bahwa referensi hanyalah nilai _id yang tersimpan pada field biasa. Konsekuensinya: MongoDB tidak akan pernah otomatis mengambil data yang ditunjuk — aplikasi harus menyelesaikannya sendiri, dan cara paling dasar adalah pola dua langkah (manual reference): query pertama membaca referensi, query kedua mencari document tujuannya. Chapter ini mempraktikkan pola itu di mongosh, ke arah maju (pelanggan → pesanan) maupun mundur (pesanan → pelanggan), lalu menghitung harga yang harus dibayar aplikasi: query beruntun N+1.

Dua Langkah: Simpan _id, Lalu Query Ulang #

Kita pakai ulang domain toko buku dengan data yang segar. Perhatikan bahwa penyisipan pesanan menuntut _id pelanggan dan buku tersedia lebih dulu — urutan dua langkah itu sudah dimulai sejak menulis data:

js
use latihan_relasi
db.pelanggan.drop()
db.buku.drop()
db.pesanan.drop()
db.pelanggan.insertMany([
  { nama: "Budi", email: "[email protected]", kota: "Bandung" },
  { nama: "Siti", email: "[email protected]", kota: "Jakarta" }
])
db.buku.insertMany([
  { sku: "BK-001", judul: "Laut Bercerita", harga: 98000 },
  { sku: "BK-002", judul: "Bumi", harga: 89000 },
  { sku: "BK-003", judul: "Filosofi Teras", harga: 72000 }
])
var idBudi = db.pelanggan.findOne({ nama: "Budi" })._id
var idSiti = db.pelanggan.findOne({ nama: "Siti" })._id
var idBuku = {}
db.buku.find({}, { sku: 1 }).forEach(b => idBuku[b.sku] = b._id)
db.pesanan.insertMany([
  { noPesanan: "PSN-001", pelangganId: idBudi, bukuId: idBuku["BK-001"], jumlah: 2 },
  { noPesanan: "PSN-002", pelangganId: idBudi, bukuId: idBuku["BK-003"], jumlah: 1 },
  { noPesanan: "PSN-003", pelangganId: idSiti, bukuId: idBuku["BK-002"], jumlah: 3 }
])

Output yang Dihasilkan:

text
switched to db latihan_relasi
true
true
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39a354a3f1d74837d1aca'),
    '1': ObjectId('6ad39a354a3f1d74837d1acb')
  }
}
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39a354a3f1d74837d1acc'),
    '1': ObjectId('6ad39a354a3f1d74837d1acd'),
    '2': ObjectId('6ad39a354a3f1d74837d1ace')
  }
}

{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad39a354a3f1d74837d1acf'),
    '1': ObjectId('6ad39a354a3f1d74837d1ad0'),
    '2': ObjectId('6ad39a354a3f1d74837d1ad1')
  }
}

Sekarang pertanyaan paling umum di sisi pembaca: pesanan mana saja milik Budi? Pola dua langkah menyelesaikannya — ambil _id Budi dulu, gunakan sebagai filter pada query kedua:

js
var budi = db.pelanggan.findOne({ nama: "Budi" })
db.pesanan.find({ pelangganId: budi._id }, { _id: 0 })

Output yang Dihasilkan:

text
[
  {
    noPesanan: 'PSN-001',
    pelangganId: ObjectId('6ad39a354a3f1d74837d1aca'),
    bukuId: ObjectId('6ad39a354a3f1d74837d1acc'),
    jumlah: 2
  },
  {
    noPesanan: 'PSN-002',
    pelangganId: ObjectId('6ad39a354a3f1d74837d1aca'),
    bukuId: ObjectId('6ad39a354a3f1d74837d1ace'),
    jumlah: 1
  }
]

Langkah pertama menghasilkan satu document pelanggan; nilai budi._id lalu menjadi kunci pencarian di pesanan. Dua query ini adalah bentuk paling sederhana dari manual reference — dan di sinilah letak kekuatannya sekaligus batasnya: semuanya eksplisit dan mudah di-debug, tetapi setiap penyelesaian referensi adalah satu perjalanan pulang-pergi ke server.

Join Manual di Sisi Aplikasi #

Laporan yang butuh data dari beberapa collection harus menggabungkan sendiri. Di mongosh kita bisa melakukannya dengan variabel dan perulangan — pola yang sama akan Anda tulis di Node.js atau Python:

js
var laporan = []
db.pesanan.find({}, { _id: 0 }).forEach(function (p) {
  var plg = db.pelanggan.findOne({ _id: p.pelangganId })
  var bk = db.buku.findOne({ _id: p.bukuId })
  laporan.push({
    noPesanan: p.noPesanan,
    pelanggan: plg.nama,
    buku: bk.judul,
    jumlah: p.jumlah,
    total: p.jumlah * bk.harga
  })
})
laporan

Output yang Dihasilkan:

text
[
  {
    noPesanan: 'PSN-001',
    pelanggan: 'Budi',
    buku: 'Laut Bercerita',
    jumlah: 2,
    total: 196000
  },
  {
    noPesanan: 'PSN-002',
    pelanggan: 'Budi',
    buku: 'Filosofi Teras',
    jumlah: 1,
    total: 72000
  },
  {
    noPesanan: 'PSN-003',
    pelanggan: 'Siti',
    buku: 'Bumi',
    jumlah: 3,
    total: 267000
  }
]

Perulangan di atas menyelesaikan dua referensi per pesanan: pelangganId ke pelanggan dan bukuId ke buku, lalu merakit object gabungan di array laporan. Arah mundur sama mudahnya — dari satu pesanan, kita menelusuri pelanggannya:

js
var pesanan = db.pesanan.findOne({ noPesanan: "PSN-003" })
db.pelanggan.findOne({ _id: pesanan.pelangganId }, { _id: 0 })

Output yang Dihasilkan:

text
{ nama: 'Siti', email: '[email protected]', kota: 'Jakarta' }

Harga Pola N+1 #

Blok laporan tadi mengandung biaya tersembunyi. Untuk 3 pesanan, perulangan menjalankan 3 × 2 = 6 query findOne — dan untuk 500 pesanan, 1000 query. Pola ini dikenal sebagai masalah N+1: satu query utama ditambah N query penyelesaian referensi, masing-masing dengan latensi jaringannya sendiri. Untuk aplikasi production, pola ini menjadi sumber latensi yang paling sering luput dari perhatian.

Untuk menguranginya, referensi dikumpulkan lebih dulu lalu diselesaikan dalam satu query $in:

js
var semuaPelanggan = db.pesanan.distinct("pelangganId")
db.pelanggan.find({ _id: { $in: semuaPelanggan } }, { _id: 0, nama: 1, kota: 1 })

Output yang Dihasilkan:

text
[
  { nama: 'Budi', kota: 'Bandung' },
  { nama: 'Siti', kota: 'Jakarta' }
]

distinct() (dari Chapter Distinct Values) memetakan seluruh pesanan menjadi daftar _id pelanggan yang unik, lalu satu query $in mengambil keduanya sekaligus — dua query total, berapa pun jumlah pesanannya. Pada collection buku pola yang sama berlaku, sehingga laporan lengkap sebenarnya cukup tiga query: satu untuk pesanan, satu untuk pelanggan, satu untuk buku.

Solusi yang lebih komplet lagi adalah memindahkan join ke server lewat $lookup — satu perintah, tanpa perulangan di aplikasi. Itu materi Chapter $lookup untuk Join.

Rangkuman #

  • Manual reference = dua query: ambil nilai referensi, lalu filter collection tujuan dengan nilai itu.
  • Join lintas collection di sisi aplikasi memakai variabel dan perulangan — fleksibel, tetapi setiap referensi adalah satu roundtrip.
  • N+1 query adalah jebakan performa klasik; kumpulkan referensi dengan distinct() lalu selesaikan dengan satu $in.
  • Arah maju (pelanggan → pesanan) dan mundur (pesanan → pelanggan) sama-sama hanya query biasa pada field referensi.

Dua langkah manual terbukti ringkas untuk satu-dua document. Untuk laporan yang menggabungkan banyak document sekaligus, kita serahkan join-nya ke server — pembahasan $lookup menanti di chapter berikutnya.