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:

js
use latihan_agregasi
db.dropDatabase()

Output yang Dihasilkan:

text
switched to db latihan_agregasi
{ ok: 1, dropped: 'latihan_agregasi' }
js
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:

text
{
  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:

js
db.buku.aggregate([ { $project: { judul: 1, harga: 1 } } ])

Output yang Dihasilkan:

text
[
  { _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:

js
db.buku.aggregate([ { $project: { _id: 0, terjual: 0, tag: 0 } } ])

Output yang Dihasilkan:

text
[
  {
    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:

js
db.buku.aggregate([ { $project: { judul: 1, tag: 0 } } ])

Output yang Dihasilkan:

text
Uncaught MongoServerError[Location31254]: Invalid $project :: caused by :: Cannot do exclusion on field tag in inclusion projection

Pesan 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:

js
db.buku.aggregate([
  { $project: {
      _id: 0,
      judul: 1,
      hargaDiskon: { $multiply: ["$harga", 0.9] },
      label: { $concat: ["$judul", " (", "$penulis", ")"] }
  } }
])

Output yang Dihasilkan:

text
[
  {
    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:

  1. judul: 1: inclusion biasa — field asli diteruskan.
  2. hargaDiskon: { $multiply: ["$harga", 0.9] }: field baru bernama hargaDiskon, dihitung server dari harga dikali 0.9.
  3. label: { $concat: ["$judul", " (", "$penulis", ")"] }: field baru berisi gabungan string; $concat menerima 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:

js
db.buku.aggregate([
  { $project: {
      _id: 0,
      judul: 1,
      "detail.harga": "$harga",
      "detail.penulis": "$penulis"
  } }
])

Output yang Dihasilkan:

text
[
  {
    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:

js
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:

text
[ { 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:

AspekProjection find()Tahap $project
Ekspresi agregasiDidukung sejak 4.4Didukung penuh
Bisa setelah $group/$unwind✖✔
Bisa dipakai berulang dalam satu pipeline✖✔
Cocok untukQuery tampilan sederhanaLaporan dan transformasi berlapis

Rangkuman #

  • $project menyaring field dalam dua mode: inclusion (1) atau exclusion (0); keduanya tidak boleh dicampur, kecuali _id: 0 di mode inclusion.
  • Nilai ekspresi menghasilkan field baru — "$field" membaca nilai, operator seperti $multiply dan $concat mengolahnya.
  • Path bertitik pada nama field tujuan membangun document bersarang; nama field baru sekaligus mengganti nama.
  • Projection find() dan $project berbagi kosakata ekspresi, tetapi hanya $project yang hidup di dalam pipeline — bisa bekerja pada document hasil $group, $unwind, atau $lookup.