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:
$replaceRootdengannewRoot— 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:
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:
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:
db.buku.aggregate([
{ $match: { judul: "Bumi Manusia" } },
{ $unwind: "$ulasan" },
{ $replaceRoot: { newRoot: { $mergeObjects: [ "$ulasan", { judul: "$judul" } ] } } }
])Output yang Dihasilkan:
[
{ pengulas: 'Budi', bintang: 5, judul: 'Bumi Manusia' },
{ pengulas: 'Siti', bintang: 5, judul: 'Bumi Manusia' },
{ pengulas: 'Dewi', bintang: 4, judul: 'Bumi Manusia' }
]Bedah pipelinenya:
$match— memilih satu buku agar hasilnya ringkas; di pipeline sungguhan filter bisa sebesar apa pun.$unwind: "$ulasan"— memecah array tiga ulasan menjadi tiga document, masing-masing membawa satu ulasan.$replaceRootdengannewRoot— ekspresi di dalamnewRootdievaluasi, lalu hasilnya menggantikan seluruh document. Di sini$mergeObjectsmenggabungkan$ulasan(fieldpengulasdanbintang) 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:
db.buku.aggregate([
{ $match: { kategori: "puisi" } },
{ $replaceWith: { judul: "$judul", penulis: "$penulis", hargaDiskon: { $multiply: ["$harga", 0.9] } } }
])Output yang Dihasilkan:
[
{
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:
db.buku.aggregate([
{ $match: { judul: "Bumi" } },
{ $replaceRoot: { newRoot: "$judul" } }
])Output yang Dihasilkan:
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:
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:
[
{ 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”:
$lookupmenghasilkan arraydetail— walaupun isinya satu document, tetap berbentuk array.$arrayElemAt: ["$detail", 0]mengambil element pertama array tersebut, lalu$mergeObjectsmenggabungkannya dengan{ totalJumlah }dari hasil$group; fieldtotalJumlahdiletakkan terakhir agar tidak tertimpa field bernama sama dari document buku.$replaceRootmengangkat gabungan itu menjadi root, dan$projectmerapikan 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
$ifNullbila datanya bisa kosong. $mergeObjectsdi dalamnewRootmenggabungkan beberapa document; object belakangan menimpa field bernama sama dari object depan.- Pola
$lookup→$arrayElemAt→$mergeObjects→$replaceRootadalah 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.