Data yang sering berubah atau dipakai bersama banyak document sebaiknya disimpan terpisah lalu dirujuk — pola referencing yang sudah kita pilih di Section Data Modeling. Di sisi aplikasi, pola itu menimbulkan pekerjaan baru: query buku hanya mengembalikan ObjectId kategori, lalu kita harus query kedua untuk nama kategorinya. Mongoose merapikan pekerjaan itu dengan populate() — satu panggilan yang mengganti ObjectId menjadi document utuh dari collection tujuan. Bab ini membangun referensi bertingkat (ulasan → buku → kategori), memuatnya dengan populate satu tingkat dan bersarang, lalu menutup dengan kapan pendekatan ini sepadan dibanding embedding.

Referensi: ObjectId dan ref #

Referensi dideklarasikan dengan dua bagian: tipe field ObjectId untuk bentuk datanya, dan ref untuk arah tujuannya. Nilai ref adalah nama model yang terdaftar di mongoose.model(), bukan nama collection — Mongoose memakainya untuk mencari model yang tepat saat populate dijalankan:

js
kategori: {
  type: mongoose.Schema.Types.ObjectId,
  ref: "Kategori", // nama MODEL, bukan nama collection
  required: true,
},

Yang tersimpan di collection hanyalah ObjectId 12 byte; tidak ada nama kategori, tidak ada salinan data. Berikut file lengkap dengan empat model yang saling merujuk:

js
// populate-buku.js — referensi antar collection dan populate
const mongoose = require("mongoose")

const uri =
  process.env.MONGODB_URI ??
  "mongodb://127.0.0.1:27017/?directConnection=true"

async function main() {
  await mongoose.connect(uri, { dbName: "latihan_node_populate" })
  await mongoose.connection.db.dropDatabase()

  const kategoriSchema = new mongoose.Schema({
    nama: { type: String, required: true },
    deskripsi: String,
  })
  const Kategori = mongoose.model("Kategori", kategoriSchema)

  const penulisSchema = new mongoose.Schema({ nama: String })
  const Penulis = mongoose.model("Penulis", penulisSchema)

  const bukuSchema = new mongoose.Schema({
    judul: { type: String, required: true },
    kategori: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Kategori", // nama MODEL, bukan nama collection
      required: true,
    },
    penulis: { type: mongoose.Schema.Types.ObjectId, ref: "Penulis" },
  })
  const Buku = mongoose.model("Buku", bukuSchema)

  const ulasanSchema = new mongoose.Schema({
    buku: { type: mongoose.Schema.Types.ObjectId, ref: "Buku", required: true },
    pembaca: String,
    bintang: { type: Number, min: 1, max: 5 },
  })
  const Ulasan = mongoose.model("Ulasan", ulasanSchema)

  // data contoh
  const teknologi = await Kategori.create({ nama: "Teknologi", deskripsi: "Buku teknik dan pemrograman" })
  const fiksi = await Kategori.create({ nama: "Fiksi", deskripsi: "Cerita rekaan" })
  const budi = await Penulis.create({ nama: "Budi Santoso" })
  const buku1 = await Buku.create({ judul: "Mongoose di Praktik", kategori: teknologi._id, penulis: budi._id })
  const buku2 = await Buku.create({ judul: "Petualangan di Jakarta", kategori: fiksi._id })
  await Ulasan.create({ buku: buku1._id, pembaca: "Siti", bintang: 5 })
  await Ulasan.create({ buku: buku2._id, pembaca: "Dewi", bintang: 4 })

  // 1) tanpa populate: yang tersimpan hanyalah ObjectId
  const polos = await Buku.findOne({ judul: "Mongoose di Praktik" })
  console.log("1. tipe field kategori:", polos.kategori.constructor.name)

  // 2) populate satu path
  const penuh = await Buku.findOne({ judul: "Mongoose di Praktik" }).populate("kategori")
  console.log("2. kategori setelah populate:", penuh.kategori.nama)

  // 3) populate dengan pemilihan field
  const ringkas = await Buku.findOne({ judul: "Mongoose di Praktik" }).populate("penulis", "nama")
  console.log("3. penulis (field nama saja):", JSON.stringify(ringkas.penulis))

  // 4) populate bersarang: ulasan -> buku -> kategori
  const ulasan = await Ulasan.findOne({ pembaca: "Siti" }).populate({
    path: "buku",
    populate: { path: "kategori" },
  })
  console.log("4. ulasan Siti — buku:", ulasan.buku.judul, "| kategori:", ulasan.buku.kategori.nama)

  await mongoose.disconnect()
}

main().catch((err) => {
  console.error("GAGAL:", err.name, "-", err.message)
  process.exit(1)
})
bash
cd /home/z/my-project/tutorial
MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node content/mongodb/mongodb-dengan-nodejs/populate-dan-references/populate-buku.js

Output yang Dihasilkan:

text
1. tipe field kategori: ObjectId
2. kategori setelah populate: Teknologi
3. penulis (field nama saja): {"_id":"6ad3f37b4920531a64d40269","nama":"Budi Santoso"}
4. ulasan Siti — buku: Mongoose di Praktik | kategori: Teknologi

Mari kita bedah per baris:

  1. Baris 1 — query biasa mengembalikan kategori sebagai objek ObjectId. Tidak ada nama, tidak ada deskripsi; itulah yang sebenarnya tersimpan.
  2. Baris 2 — .populate("kategori") mengganti ObjectId tersebut dengan document kategori lengkap, sehingga penuh.kategori.nama langsung terbaca. Di balik layar Mongoose menjalankan query kedua ke collection kategori dengan $in daftar ObjectId yang dikumpulkannya dari hasil query pertama.
  3. Baris 3 — argumen kedua "nama" membatasi field yang dimuat; _id tetap disertakan secara otomatis oleh perilaku projection MongoDB.
  4. Baris 4 — populate bersarang memakai bentuk objek: path buku dimuat dulu, lalu field kategori di dalam document buku itu dipopulatekan juga. Satu ulasan akhirnya membawa nama buku sekaligus nama kategorinya.

Populate Bukan Join SQL #

Meski terasa seperti JOIN, populate bekerja sepenuhnya di sisi aplikasi: server MongoDB hanya menerima query-query biasa yang diterbitkan Mongoose satu per satu. Perbedaan ini punya dua sisi. Sisi baiknya, populate tetap bisa dipakai pada cluster dan driver apa pun tanpa konfigurasi khusus, dan hasil antara bisa bahkan di-cache oleh aplikasi. Sisi yang perlu dihitung: filter dan sorting pada field hasil populate tidak bisa dilakukan server dalam satu operasi — bila kita butuh “semua buku pada kategori Teknologi, diurutkan judul”, arah query yang benar justru dari kategori ke buku (query buku dengan kategori: idTeknologi), bukan populate dari buku.

Untuk pipeline yang memang butuh penggabungan terpusat di server — grouping, penguraian array, penyaringan berdasarkan field collection tujuan — $lookup pada aggregation adalah alat yang tepat; bab berikutnya membandingkan keduanya langsung.

Populate Bersarang dan Batasnya #

Bentuk bersarang boleh berlanjut beberapa tingkat (populate: { path: "a", populate: { path: "b", populate: ... } }), tetapi setiap tingkat berarti satu query tambahan ke server. Untuk daftar besar — misalnya 100 ulasan yang masing-masing memuat penulisnya — populate tetap efisien karena Mongoose mengumpulkan semua ObjectId unik per tingkat lalu memakai satu query $in per tingkat, bukan satu query per document. Yang perlu diwaspadai justru desainnya: bila data acuan selalu dibaca bersama induknya dan jarang berubah, embedding di Section Data Modeling adalah jawaban yang lebih tepat.

Embed atau Populate #

Keputusan embed vs reference diambil saat merancang data, bukan saat menulis query. Ringkasan praktisnya (pembahasan penuh ada di Chapter Embedding vs Referencing):

KondisiPilihanAlasan
Data kecil, selalu dibaca bersama induk, jarang berubahembedsatu query, tanpa biaya join
Data dipakai bersama banyak induk, sering berubah sendirireference + populatesatu tempat kebenaran, perubahan sekali jalan
Daftar yang bisa memanjang tak terbatasreferencedocument induk tidak membengkak
Perlu query/menghitung pada data itu sendirireferencecollection sendiri berarti index sendiri

Contoh kita memilih referensi karena kategori dipakai ratusan buku dan deskripsinya bisa berubah — embed akan menuntut ratusan pembaruan untuk satu perubahan nama.

Rangkuman #

  • Referensi = field ObjectId + ref yang menyebut nama model; collection menyimpan ObjectId saja.
  • .populate("path") mengganti ObjectId dengan document tujuan; argumen kedua membatasi field, dan _id selalu ikut.
  • Populate bersarang memakai { path, populate: { path } }; tiap tingkat menambah satu query $in.
  • Referensi yang menunjuk document terhapus tidak ditolak — hasil populate null; validasi keberadaan tetap di tangan aplikasi.
  • Embed vs populate adalah keputusan pemodelan data: embed untuk data kecil yang dibaca bersama, reference untuk data bersama yang sering berubah.