Real loyiha — to'liq schema loyihalash, Mongoose bilan yozish

~40 daq · MongoDB · Amaliy va Professional

Muammodan boshlaylik

Kurs davomida alohida mavzularni o'rgandik: schema dizayn, aggregation, index lar, transaction, Mongoose, kesh, pagination. Endi ularni bitta loyihada birlashtiramiz — kichik internet do'kon backend i:

  • Foydalanuvchilar (ro'yxatdan o'tish, kirish)
  • Kategoriyalar va mahsulotlar (katalog, qidiruv, filtr)
  • Savat va buyurtma (qoldiqni kamaytirish)
  • Sharhlar va reyting
  • Sotuvchi uchun statistika

1. So'rovlardan boshlash

MongoDB da schema ma'lumotdan emas, qanday o'qilishidan kelib chiqib loyihalanadi. Avval asosiy so'rovlar:

So'rov Chastota
Katalog: kategoriya + filtr + saralash + sahifalash Juda ko'p
Mahsulot sahifasi: ma'lumot + oxirgi sharhlar + reyting Juda ko'p
Qidiruv Ko'p
Buyurtma berish O'rtacha
"Mening buyurtmalarim" O'rtacha
Sotuvchi statistikasi Kam

Hayotiy o'xshatma

Do'kon qurilishi. Avval xaridorlar qanday yurishini o'ylaysiz: kirish → kategoriyalar → javon → kassa. Javonlar, kassalar va omborni shunga qarab joylashtirasiz. Chiroyli, lekin xaridor yo'liga mos bo'lmagan do'kon — noqulay do'kon.


2. Collection lar va bog'lanishlar

users ──────────┐
   │            │
   │ 1:N        │ 1:N
   ▼            ▼
buyurtmalar   sharhlar ──▶ mahsulotlar ──▶ kategoriyalar
(qatorlar —                  │
 embedded)                   └── reyting (hisoblangan, saqlangan)
Qaror Sabab
Buyurtma qatorlari — embedded Har doim buyurtma bilan birga o'qiladi; buyurtma paytidagi narx va nom o'zgarmasligi kerak
Sharhlar — alohida collection Cheksiz o'sishi mumkin (16 MB chegarasi); sahifalab o'qiladi
Mahsulotda reyting va sharhlarSoni — saqlangan Katalogda saralash va filtr uchun; har sharhda yangilanadi
Mahsulotda kategoriya — ref + nomi nusxasi Katalogda populate siz ko'rsatish

3. Modellar

User

// src/models/User.js
import mongoose from 'mongoose'
import bcrypt from 'bcrypt'

const userSchema = new mongoose.Schema({
  ism: { type: String, required: true, trim: true, maxlength: 60 },
  email: { type: String, required: true, lowercase: true, trim: true, match: /^\S+@\S+\.\S+$/ },
  parol: { type: String, required: true, minlength: 8, select: false },
  rol: { type: String, enum: ['xaridor', 'sotuvchi', 'admin'], default: 'xaridor' },
}, { timestamps: true, collection: 'users' })

userSchema.index({ email: 1 }, { unique: true })

userSchema.pre('save', async function () {
  if (this.isModified('parol')) this.parol = await bcrypt.hash(this.parol, 12)
})

userSchema.methods.parolTogrimi = function (p) {
  return bcrypt.compare(p, this.parol)
}

export const User = mongoose.models.User ?? mongoose.model('User', userSchema)

Mahsulot

// src/models/Mahsulot.js
import mongoose from 'mongoose'
const { Schema } = mongoose

const mahsulotSchema = new Schema({
  nomi: { type: String, required: true, trim: true, maxlength: 200 },
  slug: { type: String, required: true, lowercase: true },
  tavsif: { type: String, maxlength: 5000 },
  narx: { type: Number, required: true, min: 0, validate: Number.isInteger },
  qoldiq: { type: Number, default: 0, min: 0 },
  kategoriya: {
    _id: { type: Schema.Types.ObjectId, ref: 'Kategoriya', required: true },
    nomi: { type: String, required: true },
  },
  sotuvchi: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  xususiyatlar: { type: Map, of: String },
  rasmlar: [{ type: String }],
  reyting: { type: Number, default: 0, min: 0, max: 5 },
  sharhlarSoni: { type: Number, default: 0 },
  faol: { type: Boolean, default: true },
}, { timestamps: true, collection: 'mahsulotlar' })

mahsulotSchema.index({ slug: 1 }, { unique: true })
mahsulotSchema.index({ 'kategoriya._id': 1, faol: 1, narx: 1, _id: 1 })
mahsulotSchema.index({ 'kategoriya._id': 1, faol: 1, reyting: -1, _id: 1 })
mahsulotSchema.index({ sotuvchi: 1, createdAt: -1 })

export const Mahsulot = mongoose.models.Mahsulot ?? mongoose.model('Mahsulot', mahsulotSchema)

Index lar — katalogning asosiy so'rovlari uchun ESR bo'yicha (29-dars): kategoriya va faol — tenglik, narx/reyting — saralash, _id — cursor pagination uchun (56-dars).

Buyurtma

// src/models/Buyurtma.js
import mongoose from 'mongoose'
const { Schema } = mongoose

const qatorSchema = new Schema({
  mahsulot: { type: Schema.Types.ObjectId, ref: 'Mahsulot', required: true },
  nomi: { type: String, required: true },
  narx: { type: Number, required: true },
  soni: { type: Number, required: true, min: 1 },
  sotuvchi: { type: Schema.Types.ObjectId, ref: 'User', required: true },
}, { _id: false })

const buyurtmaSchema = new Schema({
  mijoz: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  qatorlar: { type: [qatorSchema], validate: (v) => v.length > 0 },
  summa: { type: Number, required: true, min: 0 },
  holat: {
    type: String,
    enum: ['yangi', 'tolandi', 'yolda', 'yetkazildi', 'bekor'],
    default: 'yangi',
  },
  manzil: {
    shahar: { type: String, required: true },
    kocha: { type: String, required: true },
  },
}, { timestamps: true, collection: 'buyurtmalar' })

buyurtmaSchema.index({ mijoz: 1, createdAt: -1 })
buyurtmaSchema.index({ 'qatorlar.sotuvchi': 1, holat: 1, createdAt: -1 })

export const Buyurtma = mongoose.models.Buyurtma ?? mongoose.model('Buyurtma', buyurtmaSchema)

Qatorlarda nomi va narx nusxalanadi — mahsulot narxi keyin o'zgarsa ham, buyurtma o'sha paytdagi narxni saqlaydi.

Sharh

// src/models/Sharh.js
import mongoose from 'mongoose'
const { Schema } = mongoose

const sharhSchema = new Schema({
  mahsulot: { type: Schema.Types.ObjectId, ref: 'Mahsulot', required: true },
  muallif: {
    _id: { type: Schema.Types.ObjectId, ref: 'User', required: true },
    ism: { type: String, required: true },
  },
  baho: { type: Number, required: true, min: 1, max: 5, validate: Number.isInteger },
  matn: { type: String, maxlength: 2000 },
}, { timestamps: true, collection: 'sharhlar' })

sharhSchema.index({ mahsulot: 1, createdAt: -1 })
sharhSchema.index({ mahsulot: 1, 'muallif._id': 1 }, { unique: true })   // bitta mahsulotga — bitta sharh

export const Sharh = mongoose.models.Sharh ?? mongoose.model('Sharh', sharhSchema)

4. Katalog — filtr, saralash, cursor pagination

// src/services/katalog.js
import mongoose from 'mongoose'
import { Mahsulot } from '../models/Mahsulot.js'

const SARALASH = {
  arzon: { narx: 1, _id: 1 },
  qimmat: { narx: -1, _id: -1 },
  reyting: { reyting: -1, _id: -1 },
}

export async function katalog({ kategoriyaId, min, max, saralash = 'reyting', cursor, hajm = 20 }) {
  const sort = SARALASH[saralash] ?? SARALASH.reyting
  const [maydon] = Object.keys(sort)
  const yonalish = sort[maydon]

  const filtr = { 'kategoriya._id': new mongoose.Types.ObjectId(kategoriyaId), faol: true }
  if (min != null || max != null) {
    filtr.narx = {}
    if (min != null) filtr.narx.$gte = Number(min)
    if (max != null) filtr.narx.$lte = Number(max)
  }

  if (cursor) {
    const { q, id } = JSON.parse(Buffer.from(cursor, 'base64url').toString())
    const op = yonalish === 1 ? '$gt' : '$lt'
    filtr.$or = [
      { [maydon]: { [op]: q } },
      { [maydon]: q, _id: { [op]: new mongoose.Types.ObjectId(id) } },
    ]
  }

  const royxat = await Mahsulot.find(filtr)
    .select('nomi slug narx rasmlar reyting sharhlarSoni')
    .sort(sort)
    .limit(hajm + 1)
    .lean()

  const keyingisiBor = royxat.length > hajm
  if (keyingisiBor) royxat.pop()
  const oxirgi = royxat.at(-1)

  return {
    royxat,
    keyingiCursor: keyingisiBor
      ? Buffer.from(JSON.stringify({ q: oxirgi[maydon], id: oxirgi._id })).toString('base64url')
      : null,
  }
}

5. Buyurtma — transaction

// src/services/buyurtma.js
import mongoose from 'mongoose'
import { Mahsulot } from '../models/Mahsulot.js'
import { Buyurtma } from '../models/Buyurtma.js'

export class BiznesXato extends Error {}

export function buyurtmaBer(mijozId, savat, manzil) {
  return mongoose.connection.transaction(async (session) => {
    const qatorlar = []
    let summa = 0

    for (const { mahsulotId, soni } of savat) {
      const m = await Mahsulot.findOneAndUpdate(
        { _id: mahsulotId, faol: true, qoldiq: { $gte: soni } },
        { $inc: { qoldiq: -soni } },
        { new: true, session, projection: 'nomi narx sotuvchi' }
      )
      if (!m) throw new BiznesXato(`Mahsulot mavjud emas yoki yetarli emas: ${mahsulotId}`)

      qatorlar.push({ mahsulot: m._id, nomi: m.nomi, narx: m.narx, soni, sotuvchi: m.sotuvchi })
      summa += m.narx * soni
    }

    const [buyurtma] = await Buyurtma.create([{ mijoz: mijozId, qatorlar, summa, manzil }], { session })
    return buyurtma
  })
}

Qoldiq tekshiruvi va kamaytirish — bitta atomar findOneAndUpdate (31-dars); butun buyurtma — bitta transaction (50-dars).


6. Sharh va reyting

// src/services/sharh.js
import mongoose from 'mongoose'
import { Sharh } from '../models/Sharh.js'
import { Mahsulot } from '../models/Mahsulot.js'

export function sharhQoldir(user, mahsulotId, baho, matn) {
  return mongoose.connection.transaction(async (session) => {
    const [sharh] = await Sharh.create([{
      mahsulot: mahsulotId,
      muallif: { _id: user._id, ism: user.ism },
      baho,
      matn,
    }], { session })

    const [stat] = await Sharh.aggregate([
      { $match: { mahsulot: new mongoose.Types.ObjectId(mahsulotId) } },
      { $group: { _id: null, ortacha: { $avg: '$baho' }, soni: { $sum: 1 } } },
    ]).session(session)

    await Mahsulot.updateOne(
      { _id: mahsulotId },
      { $set: { reyting: Math.round(stat.ortacha * 10) / 10, sharhlarSoni: stat.soni } },
      { session }
    )

    return sharh
  })
}

Reyting saqlangan — katalogda sort({ reyting: -1 }) index bilan tez ishlaydi. Har bir yangi sharhda qayta hisoblanadi.


7. Sotuvchi statistikasi — aggregation

export async function sotuvchiStatistikasi(sotuvchiId) {
  const id = new mongoose.Types.ObjectId(sotuvchiId)
  const dan = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)

  return Buyurtma.aggregate([
    { $match: { 'qatorlar.sotuvchi': id, holat: { $ne: 'bekor' }, createdAt: { $gte: dan } } },
    { $unwind: '$qatorlar' },
    { $match: { 'qatorlar.sotuvchi': id } },
    { $group: {
        _id: '$qatorlar.mahsulot',
        nomi: { $first: '$qatorlar.nomi' },
        sotildi: { $sum: '$qatorlar.soni' },
        tushum: { $sum: { $multiply: ['$qatorlar.narx', '$qatorlar.soni'] } },
    } },
    { $sort: { tushum: -1 } },
    { $limit: 10 },
  ])
}

Birinchi $match index dan foydalanadi, $unwind dan keyingi ikkinchi $match — faqat shu sotuvchining qatorlarini qoldiradi (bitta buyurtmada bir nechta sotuvchi bo'lishi mumkin).


8. Yakuniy ro'yxat

  • Har bir asosiy so'rov uchun index, explain bilan tekshirilgan (28-dars)
  • Production da autoIndex: false, index lar migratsiya orqali (59-dars)
  • Mahsulot sahifasi va bosh sahifa — Redis kesh, o'zgarishda tozalash (58-dars)
  • O'chirish — mahsulotlar uchun soft delete (57-dars)
  • Atlas da: alohida user, IP Access List, zaxira, alert lar (52, 55-darslar)
  • Validatsiya: API qatlamida (Zod) va Mongoose da (41-dars)
  • Integration testlar — MongoMemoryReplSet bilan (50-dars)

[!TIP] Bu loyihani o'zingiz noldan yozib chiqing va keyin kengaytiring: sevimlilar ro'yxati, aksiyalar, Atlas Search bilan qidiruv (53-dars), yangi buyurtmada Telegram bildirishnoma (54-dars). Har bir qo'shimcha — kursdagi bir mavzuni amalda mustahkamlash.


Tez-tez uchraydigan xatolar

Jadval kabi loyihalash

Har bir narsa alohida collection va har sahifada 5 ta populate — bu SQL ni MongoDB da yozish. Birga o'qiladigan narsani birga saqlang.

Hamma narsani embed qilish

Mahsulot ichida barcha sharhlar — 16 MB chegarasi va har sharhda butun mahsulotni qayta yozish. Cheksiz o'sadigan narsa — alohida collection.

Nusxalangan ma'lumotni yangilamaslik

kategoriya.nomi nusxasi — kategoriya nomi o'zgarganda mahsulotlarda ham updateMany. Nusxalash — faqat kam o'zgaradigan ma'lumot uchun.


Xulosa

  • Schema — asosiy so'rovlardan kelib chiqib loyihalanadi
  • Embed — birga o'qiladigan va chegaralangan (buyurtma qatorlari); alohida — cheksiz o'sadigan (sharhlar)
  • Tarixiy qiymatlar (buyurtmadagi narx) — nusxalanadi
  • Hisoblangan maydonlar (reyting) — saqlanadi va yangilanadi
  • Index lar — ESR bo'yicha, asosiy so'rovlar uchun
  • Buyurtma va sharh — transaction; qoldiq — atomar $inc + shart
  • Statistika — aggregation; katalog — cursor pagination

Kurs yakunlandi

Tabriklaymiz — MongoDB kursini tugatdingiz! Siz CRUD va schema dizayndan boshlab aggregation pipeline, index lar va optimizatsiya, transaction lar, xavfsizlik, replica set va sharding, Mongoose, Atlas va real loyiha arxitekturasigacha bo'lgan yo'lni bosib o'tdingiz.

Keyingi qadamlar:

  • O'z loyihangizni yarating va Atlas ga joylang
  • Express kursi — MongoDB ustida to'liq API yozish
  • NestJS kursi — Mongoose bilan tuzilmali backend