Hasil pipeline jarang langsung siap dipakai: report butuh field tertentu saja, tampilan butuh nama field yang berbeda, dan kadang kita ingin field yang nilainya dihitung dari field lain. Tahap $project adalah meja kerja untuk semua itu — memilih field yang lolos, membuang yang tidak perlu, mengganti nama, membuat field bersarang, sampai menghitung field baru dari ekspresi. Jika $match menentukan document mana yang mengalir, $project menentukan bentuk apa yang mereka bawa.
Inclusion dan Exclusion #
Dua mode dasar $project menentukan arah penyaringan field: inclusion (sebutkan yang dipertahankan) atau exclusion (sebutkan yang dibuang). Kita pakai data toko buku yang sama:
use latihan_agregasi
db.dropDatabase()Output yang Dihasilkan:
switched to db latihan_agregasi
{ ok: 1, dropped: 'latihan_agregasi' }db.buku.insertMany([
{ _id: 1, judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "Fiksi", tag: ["sastra", "novel"], stok: 12, tahun_terbit: 2017, terjual: [ { bulan: "Januari", jumlah: 14 }, { bulan: "Februari", jumlah: 9 }, { bulan: "Maret", jumlah: 11 } ] },
{ _id: 2, judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "Fiksi", tag: ["novel", "fantasi"], stok: 20, tahun_terbit: 2014, terjual: [ { bulan: "Januari", jumlah: 21 }, { bulan: "Februari", jumlah: 17 }, { bulan: "Maret", jumlah: 25 } ] },
{ _id: 3, judul: "Supernova: Ksatria, Puteri, dan Bintang Jatuh", penulis: "Dee Lestari", harga: 95000, kategori: "Fiksi", tag: ["novel", "sains"], stok: 6, tahun_terbit: 2001, terjual: [ { bulan: "Januari", jumlah: 7 }, { bulan: "Februari", jumlah: 5 } ] },
{ _id: 4, judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "Nonfiksi", tag: ["filsafat", "pengembangan diri"], stok: 8, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 12 }, { bulan: "Februari", jumlah: 15 }, { bulan: "Maret", jumlah: 10 } ] },
{ _id: 5, judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "Nonfiksi", tag: ["pengembangan diri", "kebiasaan"], stok: 15, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 18 }, { bulan: "Februari", jumlah: 22 }, { bulan: "Maret", jumlah: 16 } ] },
{ _id: 6, judul: "Kamus Bahasa Indonesia", penulis: "Badan Bahasa", harga: 55000, kategori: "Referensi", tag: ["referensi", "bahasa"], stok: 30, tahun_terbit: 2016, terjual: [] }
])Output yang Dihasilkan:
{
acknowledged: true,
insertedIds: { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
}Mode inclusion memakai nilai 1; hanya field yang disebut (plus _id) yang bertahan:
db.buku.aggregate([ { $project: { judul: 1, harga: 1 } } ])Output yang Dihasilkan:
[
{ _id: 1, judul: 'Laut Bercerita', harga: 98000 },
{ _id: 2, judul: 'Bumi', harga: 89000 },
{
_id: 3,
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
harga: 95000
},
{ _id: 4, judul: 'Filosofi Teras', harga: 89000 },
{ _id: 5, judul: 'Atomic Habits', harga: 115000 },
{ _id: 6, judul: 'Kamus Bahasa Indonesia', harga: 55000 }
]_id selalu ikut secara bawaan — satu-satunya field yang boleh dikecualikan sambil memakai mode inclusion dengan _id: 0. Mode exclusion bekerja sebaliknya: nilai 0 membuang field yang disebut, sisanya dipertahankan:
db.buku.aggregate([ { $project: { _id: 0, terjual: 0, tag: 0 } } ])Output yang Dihasilkan:
[
{
judul: 'Laut Bercerita',
penulis: 'Leila S. Chudori',
harga: 98000,
kategori: 'Fiksi',
stok: 12,
tahun_terbit: 2017
},
{
judul: 'Bumi',
penulis: 'Tere Liye',
harga: 89000,
kategori: 'Fiksi',
stok: 20,
tahun_terbit: 2014
},
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
penulis: 'Dee Lestari',
harga: 95000,
kategori: 'Fiksi',
stok: 6,
tahun_terbit: 2001
},
{
judul: 'Filosofi Teras',
penulis: 'Henry Manampiring',
harga: 89000,
kategori: 'Nonfiksi',
stok: 8,
tahun_terbit: 2018
},
{
judul: 'Atomic Habits',
penulis: 'James Clear',
harga: 115000,
kategori: 'Nonfiksi',
stok: 15,
tahun_terbit: 2018
},
{
judul: 'Kamus Bahasa Indonesia',
penulis: 'Badan Bahasa',
harga: 55000,
kategori: 'Referensi',
stok: 30,
tahun_terbit: 2016
}
]Kedua mode ini tidak boleh dicampur dalam satu $project. Pencampuran langsung ditolak server dengan pesan yang jelas:
db.buku.aggregate([ { $project: { judul: 1, tag: 0 } } ])Output yang Dihasilkan:
Uncaught MongoServerError[Location31254]: Invalid $project :: caused by :: Cannot do exclusion on field tag in inclusion projectionPesan ini menegaskan aturannya: pilih satu arah per tahap. Butuh sebagian besar field? Gunakan exclusion. Butuh sedikit field atau field hasil hitungan? Gunakan inclusion.
Field Baru dari Ekspresi #
Inilah kemampuan yang membuat $project lebih dari sekadar penyaring field: nilai 1 bisa diganti ekspresi apa pun, dan hasilnya menjadi field baru. Nilai "$namaField" membaca field asli, operator seperti $multiply dan $concat mengolahnya:
db.buku.aggregate([
{ $project: {
_id: 0,
judul: 1,
hargaDiskon: { $multiply: ["$harga", 0.9] },
label: { $concat: ["$judul", " (", "$penulis", ")"] }
} }
])Output yang Dihasilkan:
[
{
judul: 'Laut Bercerita',
hargaDiskon: 88200,
label: 'Laut Bercerita (Leila S. Chudori)'
},
{ judul: 'Bumi', hargaDiskon: 80100, label: 'Bumi (Tere Liye)' },
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
hargaDiskon: 85500,
label: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh (Dee Lestari)'
},
{
judul: 'Filosofi Teras',
hargaDiskon: 80100,
label: 'Filosofi Teras (Henry Manampiring)'
},
{
judul: 'Atomic Habits',
hargaDiskon: 103500,
label: 'Atomic Habits (James Clear)'
},
{
judul: 'Kamus Bahasa Indonesia',
hargaDiskon: 49500,
label: 'Kamus Bahasa Indonesia (Badan Bahasa)'
}
]Mari kita bedah tiga bentuk nilai di dalam $project ini:
judul: 1: inclusion biasa — field asli diteruskan.hargaDiskon: { $multiply: ["$harga", 0.9] }: field baru bernamahargaDiskon, dihitung server darihargadikali 0.9.label: { $concat: ["$judul", " (", "$penulis", ")"] }: field baru berisi gabungan string;$concatmenerima daftar ekspresi dan merekatnya.
Perhatikan pola pentingnya: begitu satu field bernilai ekspresi, field lain tetap boleh bernilai 1. Kombinasi inclusion dan computed field sah — yang dilarang hanya mencampur 1 dengan 0. Chapter Operator Aritmatika dan Operator String nanti membedah kumpulan operator ini satu per satu.
Menyusun Struktur dengan Path Bertitik #
Nama field tujuan boleh berupa path bertitik, dan $project akan membangun document bersarang untuk menampungnya. Bentuk "$field" sebagai nilai sekaligus berfungsi mengganti nama:
db.buku.aggregate([
{ $project: {
_id: 0,
judul: 1,
"detail.harga": "$harga",
"detail.penulis": "$penulis"
} }
])Output yang Dihasilkan:
[
{
judul: 'Laut Bercerita',
detail: { harga: 98000, penulis: 'Leila S. Chudori' }
},
{ judul: 'Bumi', detail: { harga: 89000, penulis: 'Tere Liye' } },
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
detail: { harga: 95000, penulis: 'Dee Lestari' }
},
{
judul: 'Filosofi Teras',
detail: { harga: 89000, penulis: 'Henry Manampiring' }
},
{ judul: 'Atomic Habits', detail: { harga: 115000, penulis: 'James Clear' } },
{
judul: 'Kamus Bahasa Indonesia',
detail: { harga: 55000, penulis: 'Badan Bahasa' }
}
]Dua path bertitik (detail.harga, detail.penulis) dikumpulkan menjadi satu sub-document detail. Teknik ini menjawab dua kebutuhan sekaligus: mengelompokkan field terkait untuk hasil yang rapi, dan menyiapkan bentuk data yang cocok dengan struktur yang diharapkan aplikasi atau API di hilir.
Bedanya dengan Projection pada find() #
find() juga punya projection, dan sejak MongoDB 4.4 projection itu bahkan menerima ekspresi agregasi — perhatikan dua contoh berikut yang sama-sama berhasil:
db.buku.find({ _id: 1 }, { _id: 0, judul: 1, hargaDiskon: { $multiply: ["$harga", 0.9] } })
db.buku.find({ _id: 1 }, { _id: 0, judul: 1, "detail.harga": "$harga" })Output yang Dihasilkan:
[ { judul: 'Laut Bercerita', hargaDiskon: 88200 } ]
[ { judul: 'Laut Bercerita', detail: { harga: 98000 } } ]Jadi apa bedanya? Bukan pada kosakata ekspresinya, melainkan pada posisinya di dalam pipeline. Projection find() hanya bisa diterapkan pada document asli collection, pada saat query dijalankan. $project adalah tahap pipeline — ia bisa dipasang setelah $group, $unwind, atau $lookup, saat bentuk document sudah berubah total:
| Aspek | Projection find() | Tahap $project |
|---|---|---|
| Ekspresi agregasi | Didukung sejak 4.4 | Didukung penuh |
Bisa setelah $group/$unwind | ✖ | ✔ |
| Bisa dipakai berulang dalam satu pipeline | ✖ | ✔ |
| Cocok untuk | Query tampilan sederhana | Laporan dan transformasi berlapis |
Rangkuman #
$projectmenyaring field dalam dua mode: inclusion (1) atau exclusion (0); keduanya tidak boleh dicampur, kecuali_id: 0di mode inclusion.- Nilai ekspresi menghasilkan field baru —
"$field"membaca nilai, operator seperti$multiplydan$concatmengolahnya. - Path bertitik pada nama field tujuan membangun document bersarang; nama field baru sekaligus mengganti nama.
- Projection
find()dan$projectberbagi kosakata ekspresi, tetapi hanya$projectyang hidup di dalam pipeline — bisa bekerja pada document hasil$group,$unwind, atau$lookup.