Pipeline aggregation bekerja atas document root. Namun kadang data yang paling berharga justru berada di nested document — ulasan di dalam array ulasan, atau hasil $lookup yang tersimpan sebagai array detail. Membaca field bertingkat seperti detail.0.harga di aplikasi itu merepotkan. Stage $replaceRoot mempromosikan document pilihan kita menjadi root baru; $replaceWith adalah nama aliasnya yang lebih ringkas. Chapter ini memakai keduanya untuk membongkar ulasan buku dan menuntaskan pola klasik “unpack hasil $lookup”.

Peta materi Chapter ini:

  • $replaceRoot dengan newRoot — syarat nilainya harus document.
  • $replaceWith, alias ringkas dengan perilaku identik.
  • Kombinasi dengan $mergeObjects — menggabungkan nested document dengan field induk.

Mempromosikan Ulasan Menjadi Root #

Ambil satu buku berjudul Bumi Manusia yang menyimpan tiga ulasan. $unwind memecah array ulasan menjadi tiga document, lalu $replaceRoot mengangkat isi setiap ulasan menjadi document utama:

js
use latihan_agregasi2
db.dropDatabase()
db.buku.insertMany([
  { judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "fiksi", terjual: 120, tanggal_terbit: ISODate("2017-02-01T00:00:00Z"), tag: ["fiksi", "sastra", "penghargaan"], ulasan: [ { pengulas: "Budi", bintang: 5 }, { pengulas: "Siti", bintang: 4 } ] },
  { judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "fiksi", terjual: 250, tanggal_terbit: ISODate("2014-09-02T00:00:00Z"), tag: ["fiksi", "remaja", "fantasi"], ulasan: [ { pengulas: "Dewi", bintang: 5 }, { pengulas: "Budi", bintang: 4 }, { pengulas: "Siti", bintang: 5 } ] },
  { judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "nonfiksi", terjual: 180, tanggal_terbit: ISODate("2018-07-01T00:00:00Z"), tag: ["filsafat", "pengembangan diri"], ulasan: [ { pengulas: "Siti", bintang: 5 } ] },
  { judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi", terjual: 310, tanggal_terbit: ISODate("2018-10-16T00:00:00Z"), tag: ["pengembangan diri", "terjemahan"], ulasan: [ { pengulas: "Budi", bintang: 5 }, { pengulas: "Dewi", bintang: 4 } ] },
  { judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi", terjual: 120, tanggal_terbit: ISODate("2016-06-15T00:00:00Z"), tag: ["fiksi", "remaja"], ulasan: [ { pengulas: "Dewi", bintang: 4 } ] },
  { judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", harga: 105000, kategori: "sastra", terjual: 150, tanggal_terbit: ISODate("1980-05-25T00:00:00Z"), tag: ["sastra", "sejarah"], ulasan: [ { pengulas: "Budi", bintang: 5 }, { pengulas: "Siti", bintang: 5 }, { pengulas: "Dewi", bintang: 4 } ] },
  { judul: "Hujan Bulan Juni", penulis: "Sapardi Djoko Damono", harga: 65000, kategori: "puisi", terjual: 75, tanggal_terbit: ISODate("1994-06-10T00:00:00Z"), tag: ["puisi", "sastra"] },
  { judul: "Ensiklopedia Sains Populer", penulis: "Tim Sains", harga: 250000, kategori: "referensi", terjual: 40, tanggal_terbit: ISODate("2020-03-05T00:00:00Z"), tag: ["referensi", "sains"], ulasan: [ { pengulas: "Dewi", bintang: 4 } ] }
])
db.transaksi.insertMany([
  { tanggal: ISODate("2026-11-28T10:20:00Z"), buku_judul: "Bumi", jumlah: 2 },
  { tanggal: ISODate("2026-11-29T14:45:00Z"), buku_judul: "Atomic Habits", jumlah: 3 },
  { tanggal: ISODate("2026-11-30T16:10:00Z"), buku_judul: "Filosofi Teras", jumlah: 1 },
  { tanggal: ISODate("2026-12-01T09:15:00Z"), buku_judul: "Bumi", jumlah: 2 },
  { tanggal: ISODate("2026-12-01T11:30:00Z"), buku_judul: "Atomic Habits", jumlah: 1 },
  { tanggal: ISODate("2026-12-01T15:45:00Z"), buku_judul: "Bumi", jumlah: 3 },
  { tanggal: ISODate("2026-12-02T10:05:00Z"), buku_judul: "Laut Bercerita", jumlah: 1 },
  { tanggal: ISODate("2026-12-02T13:20:00Z"), buku_judul: "Filosofi Teras", jumlah: 4 },
  { tanggal: ISODate("2026-12-02T16:40:00Z"), buku_judul: "Bumi", jumlah: 2 },
  { tanggal: ISODate("2026-12-03T09:50:00Z"), buku_judul: "Atomic Habits", jumlah: 2 },
  { tanggal: ISODate("2026-12-03T14:10:00Z"), buku_judul: "Bumi Manusia", jumlah: 1 },
  { tanggal: ISODate("2026-12-03T17:25:00Z"), buku_judul: "Bumi", jumlah: 1 },
  { tanggal: ISODate("2026-12-04T10:30:00Z"), buku_judul: "Rumah Kaca", jumlah: 2 },
  { tanggal: ISODate("2026-12-04T13:55:00Z"), buku_judul: "Bumi", jumlah: 2 },
  { tanggal: ISODate("2026-12-04T18:05:00Z"), buku_judul: "Hujan Bulan Juni", jumlah: 3 }
])

Output yang Dihasilkan:

text
switched to db latihan_agregasi2
{ ok: 1, dropped: 'latihan_agregasi2' }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3a50e8719faf949ca7bc2'),
    '1': ObjectId('6ad3a50e8719faf949ca7bc3'),
    '2': ObjectId('6ad3a50e8719faf949ca7bc4'),
    '3': ObjectId('6ad3a50e8719faf949ca7bc5'),
    '4': ObjectId('6ad3a50e8719faf949ca7bc6'),
    '5': ObjectId('6ad3a50e8719faf949ca7bc7'),
    '6': ObjectId('6ad3a50e8719faf949ca7bc8'),
    '7': ObjectId('6ad3a50e8719faf949ca7bc9')
  }
}
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3a50e8719faf949ca7bca'),
    '1': ObjectId('6ad3a50e8719faf949ca7bcb'),
    '2': ObjectId('6ad3a50e8719faf949ca7bcc'),
    '3': ObjectId('6ad3a50e8719faf949ca7bcd'),
    '4': ObjectId('6ad3a50e8719faf949ca7bce'),
    '5': ObjectId('6ad3a50e8719faf949ca7bcf'),
    '6': ObjectId('6ad3a50e8719faf949ca7bd0'),
    '7': ObjectId('6ad3a50e8719faf949ca7bd1'),
    '8': ObjectId('6ad3a50e8719faf949ca7bd2'),
    '9': ObjectId('6ad3a50e8719faf949ca7bd3'),
    '10': ObjectId('6ad3a50e8719faf949ca7bd4'),
    '11': ObjectId('6ad3a50e8719faf949ca7bd5'),
    '12': ObjectId('6ad3a50e8719faf949ca7bd6'),
    '13': ObjectId('6ad3a50e8719faf949ca7bd7'),
    '14': ObjectId('6ad3a50e8719faf949ca7bd8')
  }
}

Sekarang promosikan tiap ulasan menjadi document utama:

js
db.buku.aggregate([
  { $match: { judul: "Bumi Manusia" } },
  { $unwind: "$ulasan" },
  { $replaceRoot: { newRoot: { $mergeObjects: [ "$ulasan", { judul: "$judul" } ] } } }
])

Output yang Dihasilkan:

text
[
  { pengulas: 'Budi', bintang: 5, judul: 'Bumi Manusia' },
  { pengulas: 'Siti', bintang: 5, judul: 'Bumi Manusia' },
  { pengulas: 'Dewi', bintang: 4, judul: 'Bumi Manusia' }
]

Bedah pipelinenya:

  1. $match — memilih satu buku agar hasilnya ringkas; di pipeline sungguhan filter bisa sebesar apa pun.
  2. $unwind: "$ulasan" — memecah array tiga ulasan menjadi tiga document, masing-masing membawa satu ulasan.
  3. $replaceRoot dengan newRoot — ekspresi di dalam newRoot dievaluasi, lalu hasilnya menggantikan seluruh document. Di sini $mergeObjects menggabungkan $ulasan (field pengulas dan bintang) dengan document { judul: "$judul" } — field dari object yang disebut belakangan menimpa yang sama dari object sebelumnya.

Hasilnya: tiga document ulasan berdiri sendiri, lengkap dengan judul buku sebagai konteks. Semua field induk yang tidak ikut (harga, terjual, dan kawan-kawan) hilang — $replaceRoot benar-benar mengganti root, bukan menambah field.

$replaceWith: Alias yang Ringkas #

$replaceWith adalah $replaceRoot tanpa pembungkus newRoot — perilakunya identik, tinggal pilih selera penulisan:

js
db.buku.aggregate([
  { $match: { kategori: "puisi" } },
  { $replaceWith: { judul: "$judul", penulis: "$penulis", hargaDiskon: { $multiply: ["$harga", 0.9] } } }
])

Output yang Dihasilkan:

text
[
  {
    judul: 'Hujan Bulan Juni',
    penulis: 'Sapardi Djoko Damono',
    hargaDiskon: 58500
  }
]

Contoh ini menunjukkan sisi lain stage ini: membangun root baru dari nol. Document hasil hanya berisi judul, penulis, dan hargaDiskon hasil hitung — bentuk yang langsung siap dikonsumsi aplikasi tanpa projection tambahan.

Kita bisa saksikan penolakan tersebut langsung — mencoba menjadikan "$judul" (sebuah string) sebagai root:

js
db.buku.aggregate([
  { $match: { judul: "Bumi" } },
  { $replaceRoot: { newRoot: "$judul" } }
])

Output yang Dihasilkan:

text
Uncaught 
MongoServerError[Location40228]: Executor error during aggregate command on namespace: latihan_agregasi2.buku :: caused by :: 'newRoot' expression  must evaluate to an object, but resulting value was: "Bumi". Type of resulting value: 'string'. Input document: {judul: "Bumi"}

Pola Klasik: Unpack Hasil $lookup #

Penggunaan $replaceRoot yang paling sering ditemui adalah merapikan hasil $lookup. Collection transaksi mencatat penjualan per judul; kita ingin laporan jumlah terjual per buku lengkap dengan penulis dan harga dari collection buku:

js
db.transaksi.aggregate([
  { $group: { _id: "$buku_judul", totalJumlah: { $sum: "$jumlah" } } },
  { $sort: { totalJumlah: -1, _id: 1 } },
  { $lookup: { from: "buku", localField: "_id", foreignField: "judul", as: "detail" } },
  { $replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: ["$detail", 0] }, { totalJumlah: "$totalJumlah" } ] } } },
  { $project: { _id: 0, judul: 1, penulis: 1, harga: 1, totalJumlah: 1 } }
])

Output yang Dihasilkan:

text
[
  { judul: 'Bumi', penulis: 'Tere Liye', harga: 89000, totalJumlah: 12 },
  {
    judul: 'Atomic Habits',
    penulis: 'James Clear',
    harga: 115000,
    totalJumlah: 6
  },
  {
    judul: 'Filosofi Teras',
    penulis: 'Henry Manampiring',
    harga: 89000,
    totalJumlah: 5
  },
  {
    judul: 'Hujan Bulan Juni',
    penulis: 'Sapardi Djoko Damono',
    harga: 65000,
    totalJumlah: 3
  },
  {
    judul: 'Rumah Kaca',
    penulis: 'Tere Liye',
    harga: 72000,
    totalJumlah: 2
  },
  {
    judul: 'Bumi Manusia',
    penulis: 'Pramoedya A. Toer',
    harga: 105000,
    totalJumlah: 1
  },
  {
    judul: 'Laut Bercerita',
    penulis: 'Leila S. Chudori',
    harga: 98000,
    totalJumlah: 1
  }
]

Tiga langkah terakhirnya adalah pola baku “unpack lookup”:

  1. $lookup menghasilkan array detail — walaupun isinya satu document, tetap berbentuk array.
  2. $arrayElemAt: ["$detail", 0] mengambil element pertama array tersebut, lalu $mergeObjects menggabungkannya dengan { totalJumlah } dari hasil $group; field totalJumlah diletakkan terakhir agar tidak tertimpa field bernama sama dari document buku.
  3. $replaceRoot mengangkat gabungan itu menjadi root, dan $project merapikan field yang ditampilkan.

Tanpa pola ini, aplikasi harus mengakses hasil lewat elemen.detail[0].harga; dengan pola ini, setiap document hasil sudah datar dan siap dipakai.

Rangkuman #

  • $replaceRoot: { newRoot: <expr> } mengganti document root dengan hasil ekspresi; $replaceWith: <expr> adalah alias ringkasnya.
  • Ekspresi root wajib menghasilkan document — string, angka, array, atau field hilang semuanya memicu error; lindungi dengan $ifNull bila datanya bisa kosong.
  • $mergeObjects di dalam newRoot menggabungkan beberapa document; object belakangan menimpa field bernama sama dari object depan.
  • Pola $lookup → $arrayElemAt → $mergeObjects → $replaceRoot adalah cara standar merapikan hasil join menjadi document datar.

Laporan di atas menampilkan totalJumlah mentah. Agar tampilannya mengikuti kondisi data — misalnya label “terlaris” hanya untuk yang laku di atas 200 — pipeline butuh logika if-then-else. Itu materi Chapter Conditional Expressions berikutnya.