Mengambil data dan menghitung sendiri di JavaScript hanya cocok untuk puluhan document; laporan penjualan nyata butuh penjumlahan, pengelompokan, dan penggabungan di sisi server. Mongoose tidak menerjemahkan aggregation ke abstraksi baru — pipeline yang kita kenal dari Section Aggregation ditulis apa adanya, tetapi dijalankan dari model sehingga schema tetap berperan di sekelilingnya. Bab ini menjalankan $group dan $lookup dari model, memeriksa tipe nilai _id pada hasil, lalu memutuskan kapan $lookup menggantikan populate().
Pipeline dari Model: aggregate().exec() #
Method Model.aggregate() menerima array tahap pipeline dan mengembalikan objek Aggregate yang bisa dirantai; .exec() mengirimkannya ke server dan menunggu hasilnya sebagai array. File berikut merangkum total quantity per produk, lalu menyusun laporan lengkap dengan nama produk:
// agregasi-penjualan.js — aggregate dari model: $group, ObjectId, dan $lookup
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_agregasi" })
await mongoose.connection.db.dropDatabase()
const produkSchema = new mongoose.Schema({ nama: String, kategori: String })
const Produk = mongoose.model("Produk", produkSchema) // collection "produks"
const pesananSchema = new mongoose.Schema({
produk: { type: mongoose.Schema.Types.ObjectId, ref: "Produk", required: true },
qty: { type: Number, required: true, min: 1 },
})
const Pesanan = mongoose.model("Pesanan", pesananSchema)
const kopi = await Produk.create({ nama: "Kopi Gayo", kategori: "minuman" })
const teh = await Produk.create({ nama: "Teh Melati", kategori: "minuman" })
const gula = await Produk.create({ nama: "Gula Aren", kategori: "bahan" })
await Pesanan.create([
{ produk: kopi._id, qty: 2 },
{ produk: kopi._id, qty: 3 },
{ produk: teh._id, qty: 1 },
{ produk: gula._id, qty: 4 },
])
// 1) $group murni: _id hasil group bertipe ObjectId
const perProduk = await Pesanan.aggregate([
{ $group: { _id: "$produk", totalQty: { $sum: "$qty" }, jumlahPesanan: { $sum: 1 } } },
{ $sort: { totalQty: -1 } },
]).exec()
console.log("1. tipe _id hasil $group:", perProduk[0]._id.constructor.name)
perProduk.forEach((d) => console.log(" totalQty:", d.totalQty, "| jumlahPesanan:", d.jumlahPesanan))
// 2) $lookup: penggabungan di server, satu pipeline utuh
const laporan = await Pesanan.aggregate([
{ $group: { _id: "$produk", totalQty: { $sum: "$qty" } } },
{
$lookup: {
from: "produks", // nama COLLECTION (bukan nama model)
localField: "_id",
foreignField: "_id",
as: "produk",
},
},
{ $unwind: "$produk" },
{ $project: { _id: 0, nama: "$produk.nama", kategori: "$produk.kategori", totalQty: 1 } },
{ $sort: { totalQty: -1 } },
]).exec()
laporan.forEach((d) => console.log("2.", d.nama, "|", d.kategori, "| totalQty:", d.totalQty))
await mongoose.disconnect()
}
main().catch((err) => {
console.error("GAGAL:", err.name, "-", err.message)
process.exit(1)
})cd /home/z/my-project/tutorial
MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node content/mongodb/mongodb-dengan-nodejs/aggregation-mongoose/agregasi-penjualan.jsOutput yang Dihasilkan:
1. tipe _id hasil $group: ObjectId
totalQty: 5 | jumlahPesanan: 2
totalQty: 4 | jumlahPesanan: 1
totalQty: 1 | jumlahPesanan: 1
2. Kopi Gayo | minuman | totalQty: 5
2. Gula Aren | bahan | totalQty: 4
2. Teh Melati | minuman | totalQty: 1Bedah per bagian: empat pesanan terkelompok menjadi tiga baris — $group menjumlahkan qty dan menghitung jumlah pesanan per produk, $sort mengurutkan hasilnya. Tahap $lookup kemudian menggabungkan document produk tepat di server: localField: "_id" (hasil group) dicocokkan dengan foreignField: "_id" di collection produks, $unwind meratakan array hasil join (hasil $lookup selalu array, sekalipun isinya satu document), dan $project merapikan kolom keluaran. Laporan akhir lahir dari satu perjalanan ke server.
Objek Aggregate yang dikembalikan sebelum .exec() juga bisa dirantai ala query builder — .match(), .group(), .sort(), .skip() — serta dua method penting lain: .option({ allowDiskUse: true }) bagi $sort atau $group yang memproses data melebihi batas memori 100 MB, dan .explain() untuk meminta rencana eksekusi dari server, alat utama saat men-tune pipeline. Tahap-tahapnya sendiri identik dengan yang kita latih di mongosh pada Section Aggregation — tidak ada dialek khusus Mongoose; yang berubah hanyalah tempat pipeline itu dipanggil dan tipe koneksi yang membidaninya.
Di aplikasi nyata, pipeline seperti ini dibungkus method bernama di lapisan model atau service — misalnya laporanPenjualan() pada model Pesanan — sehingga route hanya memanggil nama laporan, bukan merakit array tahap. Pipeline yang panjang juga sebaiknya diberi komentar per tahap atau dipecah variabel bernama (tahapGroup, tahapJoin); kesalahan paling umum saat merakit pipeline adalah tertukarnya urutan tahap, karena setiap tahap menerima keluaran tahap sebelumnya.
Hasil Polos, _id Tetap ObjectId #
Dua sifat hasil aggregate yang perlu dipegang: baris hasil adalah objek JavaScript polos (bukan instance model — tidak ada method save() atau jalur validasi di dalamnya), tetapi nilai _id hasil $group tetap berupa objek ObjectId, karena server mengembalikan nilai BSON sebagaimana adanya. Itulah mengapa baris pertama output melaporkan ObjectId, bukan string.
Sisi inputnya lebih mengejutkan: nilai yang kita kirim ke dalam pipeline tidak melewati casting schema.
// mdtest: skip — Pesanan dan kopi berasal dari file di atas
// $match dengan ObjectId -> 1 baris; dengan kopi._id.toString() -> 0 baris
await Pesanan.aggregate([{ $match: { produk: kopi._id } }]).exec()$lookup atau populate #
Keduanya menggabungkan data dari collection lain, tetapi bekerja di tempat berbeda dan tidak bisa dicampur di dalam satu pipeline:
| Aspek | populate() | $lookup |
|---|---|---|
| Lokasi kerja | aplikasi (query tambahan setelah query utama) | server (satu tahap pipeline) |
| Bisa difilter/di-group pada data gabungan | tidak langsung | ya — tahap berikutnya memakai hasil join |
| Kegunaan terbaik | memuat relasi untuk ditampilkan | laporan, penghitungan, penyaringan lintas collection |
Aturan praktisnya sederhana: bila hasil akhir berupa document beserta relasinya untuk ditampilkan, populate() lebih ringkas. Begitu pipeline perlu menghitung atau menyaring berdasarkan field collection tujuan (total per kategori, pelanggan dengan transaksi di atas ambang), gabungkan sejak awal dengan $lookup — mempopulate hasil $group setelahnya berarti query tambahan per baris, karena populate() tidak bisa menyentuh tahap-tahap di dalam pipeline. Perhatikan juga bedanya penamaan: ref: "Produk" menyebut nama model, sedangkan from: "produks" menyebut nama collection — model Produk kita otomatis memakai collection produks hasil pluralisasi. Tidak ada pemeriksaan bahwa keduanya cocok; salah tulis from tidak menghasilkan galat, hanya array kosong pada field as.
Rangkuman #
Model.aggregate([...]).exec()menjalankan pipeline persis seperti di mongosh; hasilnya array objek polos tanpa method document.- Objek
Aggregatebisa dirantai (.match(),.group(),.sort()), ditambah opsiallowDiskUse, dan diperiksa dengan.explain(). - Nilai
_idhasil$grouptetapObjectId; sementara nilai input pipeline tidak di-casting —$matchdengan string id tidak cocok dengan apa pun. $lookupmenggabungkan di server sehingga tahap$group,$sort, dan$matchbisa bekerja di atas data gabungan;populate()bekerja di aplikasi untuk memuat relasi hasil akhir.frompada$lookupmemakai nama collection, bukan nama model sepertiref; salah nama tidak melempar galat, hanya menghasilkan array kosong.