Mongoose bilan Aggregation Pipeline
Muammodan boshlaylik
Sotuvchi paneli uchun API: "Mening oxirgi 30 kundagi savdolarim — kunlar bo'yicha, eng ko'p sotilgan mahsulotlar bilan".
18–25-darslarda aggregation ni mongosh da yozdik. Express + Mongoose da esa:
req.user.id— string, bazada esaObjectId- Natijada Mongoose document emas — oddiy obyektlar
- Schema dagi validatsiya, tur o'girish va middleware aggregation da qanday ishlaydi?
Hayotiy o'xshatma
Siz mongosh da tajriba o'tkazgan kimyogar edingiz — probirkalar, qo'lda aralashtirish. Endi zavodda ishlaysiz: retsept o'sha, lekin uskunalar boshqa — avtomatik dozator xom ashyoni o'zi o'lchamaydi, uni to'g'ri birlikda (ObjectId, Date) berishingiz kerak.
Model.aggregate()
const natija = await Buyurtma.aggregate([
{ $match: { holat: 'yetkazildi' } },
{ $group: { _id: '$shahar', jami: { $sum: '$summa' } } },
{ $sort: { jami: -1 } },
])
Sintaksis mongosh dagi bilan bir xil. Natija — oddiy JavaScript obyektlar massivi (Mongoose document emas): save(), virtual lar, metodlar yo'q.
⚠️ Tur o'girish YO'Q
find() da Mongoose string ni avtomatik ObjectId ga aylantiradi. aggregate() da — yo'q:
// ❌ — hech narsa topmaydi: "665f..." (string) ≠ ObjectId("665f...")
await Buyurtma.aggregate([
{ $match: { sotuvchi: req.user.id } },
])
// ✅
import mongoose from 'mongoose'
await Buyurtma.aggregate([
{ $match: { sotuvchi: new mongoose.Types.ObjectId(req.user.id) } },
])
Sanalar ham:
// ❌ — string bilan solishtirish
{ $match: { sana: { $gte: req.query.dan } } }
// ✅
{ $match: { sana: { $gte: new Date(req.query.dan) } } }
[!WARNING] Bu — Mongoose aggregation dagi eng ko'p uchraydigan xato: so'rov xatosiz ishlaydi, lekin bo'sh natija qaytaradi. Aggregation da ID va sanalarni har doim qo'lda o'giring.
Sotuvchi paneli — to'liq misol
// src/services/sotuvStatistikasi.js
import mongoose from 'mongoose'
import { Buyurtma } from '../models/Buyurtma.js'
export async function sotuvchiStatistikasi(sotuvchiId, kunlar = 30) {
const dan = new Date(Date.now() - kunlar * 24 * 60 * 60 * 1000)
const [natija] = await Buyurtma.aggregate([
{ $match: {
sotuvchi: new mongoose.Types.ObjectId(sotuvchiId),
holat: 'yetkazildi',
createdAt: { $gte: dan },
} },
{ $facet: {
umumiy: [
{ $group: { _id: null, tushum: { $sum: '$summa' }, soni: { $sum: 1 } } },
],
kunlik: [
{ $group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt', timezone: 'Asia/Tashkent' } },
tushum: { $sum: '$summa' },
} },
{ $sort: { _id: 1 } },
],
topMahsulotlar: [
{ $unwind: '$qatorlar' },
{ $group: {
_id: '$qatorlar.mahsulot',
sotildi: { $sum: '$qatorlar.soni' },
} },
{ $sort: { sotildi: -1 } },
{ $limit: 5 },
{ $lookup: {
from: 'mahsulotlar',
localField: '_id',
foreignField: '_id',
pipeline: [{ $project: { nomi: 1, rasm: 1 } }],
as: 'mahsulot',
} },
{ $set: { mahsulot: { $first: '$mahsulot' } } },
],
} },
])
return {
tushum: natija.umumiy[0]?.tushum ?? 0,
buyurtmalar: natija.umumiy[0]?.soni ?? 0,
kunlik: natija.kunlik,
topMahsulotlar: natija.topMahsulotlar,
}
}
router.get('/sotuvchi/statistika', auth, async (req, res) => {
res.json(await sotuvchiStatistikasi(req.user.id, 30))
})
[!INFO]
$lookupdafrom— collection nomi ('mahsulotlar'), model nomi ('Mahsulot') emas. Model nomidan olish:Mahsulot.collection.name.
Aggregate builder
Zanjir shaklida ham yozish mumkin:
const natija = await Buyurtma.aggregate()
.match({ holat: 'yetkazildi' })
.group({ _id: '$shahar', jami: { $sum: '$summa' } })
.sort({ jami: -1 })
.limit(5)
Pipeline ni shartli qurish uchun qulay:
const agg = Mahsulot.aggregate().match({ faol: true })
if (kategoriya) agg.match({ kategoriya })
agg.group({ _id: '$brend', soni: { $sum: 1 } })
const natija = await agg
Natijani Mongoose document ga aylantirish
Kamdan-kam kerak, lekin mumkin:
const xomlar = await Mahsulot.aggregate([{ $match: { ... } }, { $sample: { size: 5 } }])
const documentlar = xomlar.map((x) => Mahsulot.hydrate(x))
documentlar[0].toliqNomi // virtual lar ishlaydi
Middleware va aggregation
pre('find')hook lari (masalan, soft delete filtri — 46-dars) aggregation da ishlamaydi- Ular uchun alohida
pre('aggregate'):
mahsulotSchema.pre('aggregate', function () {
this.pipeline().unshift({ $match: { ochirilgan: { $ne: true } } })
})
[!TIP] Murakkab aggregation larni marshrutlarga emas — service funksiyalariga yoki model
staticsiga (47-dars) joylang. Ularni alohida test qilish va qayta ishlatish oson bo'ladi.
Tez-tez uchraydigan xatolar
String ID bilan $match
Bo'sh natija, xato yo'q. new mongoose.Types.ObjectId(id).
$lookup da model nomi
from: 'Mahsulot' — bunday collection yo'q, natija har doim [].
Natijada .id kutish
Aggregation natijasida faqat _id bor — id virtual i yo'q.
Xulosa
Model.aggregate([...])— mongosh dagi kabi pipeline; natija — oddiy obyektlar- Tur o'girish yo'q: ID lar —
new mongoose.Types.ObjectId(), sanalar —new Date() $lookup.from— collection nomi (Model.collection.name)- Builder:
.match().group().sort()— shartli qurish uchun Model.hydrate()— kerak bo'lsa document ga aylantirish- Find hook lari aggregation da ishlamaydi —
pre('aggregate') - Murakkab aggregation — service yoki statics da
Keyingi dars: 50-dars — Mongoose bilan Transaction — session