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:
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:
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:
var budi = db.pelanggan.findOne({ nama: "Budi" })
db.pesanan.find({ pelangganId: budi._id }, { _id: 0 })Output yang Dihasilkan:
[
{
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:
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
})
})
laporanOutput yang Dihasilkan:
[
{
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:
var pesanan = db.pesanan.findOne({ noPesanan: "PSN-003" })
db.pelanggan.findOne({ _id: pesanan.pelangganId }, { _id: 0 })Output yang Dihasilkan:
{ 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:
var semuaPelanggan = db.pesanan.distinct("pelangganId")
db.pelanggan.find({ _id: { $in: semuaPelanggan } }, { _id: 0, nama: 1, kota: 1 })Output yang Dihasilkan:
[
{ 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.