Mongoose bilan Aggregation Pipeline

~20 daq · MongoDB · Mongoose — Ilg'or

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 esa ObjectId
  • 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] $lookup da from — 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 statics iga (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