Real loyiha — to'liq schema loyihalash, Mongoose bilan yozish
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,
explainbilan 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 —
MongoMemoryReplSetbilan (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