Middleware (Hooks) — pre, post, save, find, remove

~20 daq · MongoDB · Mongoose — Ilg'or

Muammodan boshlaylik

User yaratiladigan joylar: ro'yxatdan o'tish, admin panel, seed skript, Google orqali kirish. Har birida:

const xesh = await bcrypt.hash(parol, 12)
await User.create({ ...malumot, parol: xesh })

Bitta joyda unutildi — parol bazaga ochiq matnda tushdi. Yoki: mahsulot nomidan slug yasash, post o'chirilganda uning izohlarini ham o'chirish — bu qoidalar ham kod bo'ylab tarqalgan.

Bunday "har doim shunday bo'lishi kerak" qoidalari model ning o'zida bo'lishi kerak.


Middleware nima?

Middleware (hook) — Mongoose amalidan oldin (pre) yoki keyin (post) avtomatik bajariladigan funksiya.

userSchema.pre('save', async function () {
  // saqlashdan oldin
})

userSchema.post('save', function (doc) {
  // saqlangandan keyin
})

Hayotiy o'xshatma

Aeroportda yuk topshirish. Siz shunchaki chamadonni lentaga qo'yasiz. "Oldin" — avtomatik tarozi va rentgen (pre). "Keyin" — chamadonga birka yopishtiriladi va sizga SMS keladi (post). Siz har safar o'zingiz tarozi so'rashingiz shart emas — tizim o'zi bajaradi.


Parolni xeshlash — klassik misol

import bcrypt from 'bcrypt'

const userSchema = new Schema({
  email: { type: String, required: true, unique: true },
  parol: { type: String, required: true, minlength: 8, select: false },
})

userSchema.pre('save', async function () {
  if (!this.isModified('parol')) return     // parol o'zgarmagan — qayta xeshlamaslik
  this.parol = await bcrypt.hash(this.parol, 12)
})

userSchema.methods.parolTogrimi = function (kiritilgan) {
  return bcrypt.compare(kiritilgan, this.parol)
}
await User.create({ email: "lola@mail.uz", parol: "maxfiy123" })
// bazada: "$2b$12$Kx..." — avtomatik xeshlangan

isModified('parol') — juda muhim: user ismini o'zgartirib save() qilsangiz, allaqachon xeshlangan parol qayta xeshlanmasin (aks holda user kira olmaydi).


Middleware turlari

Tur Hook lar this
Document save, validate, deleteOne ({ document: true }) Document
Query find, findOne, updateOne, findOneAndUpdate, deleteMany... Query
Aggregate aggregate Aggregation obyekti
Model insertMany Model

[!WARNING] pre('save') faqat save() va create() da ishlaydi. updateOne, findByIdAndUpdate — ishlamaydi. Parolni findByIdAndUpdate(id, { parol }) bilan o'zgartirsangiz — u xeshlanmasdan saqlanadi! Parol o'zgarishini har doim find + save orqali qiling yoki pre('findOneAndUpdate') hook ini ham yozing.


Slug yasash

mahsulotSchema.pre('validate', function () {
  if (this.isModified('nomi') || !this.slug) {
    this.slug = this.nomi
      .toLowerCase()
      .replace(/[ʻʼ'`]/g, '')
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '')
  }
})

pre('validate') — validatsiyadan oldin, shuning uchun slug: { required: true } bo'lsa ham ishlaydi.


Query middleware

Avtomatik filtr

// o'chirilgan (soft delete) documentlar hech qachon ko'rinmasin
mahsulotSchema.pre(/^find/, function () {
  this.where({ ochirilgan: { $ne: true } })
})

/^find/ — regex: find, findOne, findOneAndUpdate... hammasi uchun. this — query.

Update da updatedAt yoki qo'shimcha maydon

postSchema.pre('findOneAndUpdate', function () {
  const update = this.getUpdate()
  if (update.sarlavha) {
    this.set({ slug: slugYasa(update.sarlavha) })
  }
})

Sekin so'rovlarni o'lchash

schema.pre(/^find/, function () {
  this._boshlandi = Date.now()
})
schema.post(/^find/, function () {
  const ms = Date.now() - this._boshlandi
  if (ms > 200) logger.warn({ ms, filtr: this.getFilter() }, 'Sekin so\'rov')
})

Bog'liq ma'lumotlarni o'chirish

postSchema.pre('deleteOne', { document: true, query: false }, async function () {
  await mongoose.model('Izoh').deleteMany({ post: this._id })
})
const post = await Post.findById(id)
await post.deleteOne()          // ✅ hook ishlaydi — izohlar ham o'chadi

await Post.deleteOne({ _id: id })  // ❌ document hook ishlamaydi (bu query)

Query uchun alohida:

postSchema.pre('deleteMany', async function () {
  const postlar = await this.model.find(this.getFilter()).select('_id').lean()
  await mongoose.model('Izoh').deleteMany({ post: { $in: postlar.map(p => p._id) } })
})

[!INFO] remove() metodi Mongoose 7 da olib tashlangan. Endi deleteOne() va deleteMany() hook laridan foydalaning; document hook i uchun { document: true, query: false } ko'rsating.


post — keyin

userSchema.post('save', function (doc) {
  logger.info({ userId: doc._id }, 'User saqlandi')
})

// dublikat xatosini chiroyli xabarga aylantirish
userSchema.post('save', function (err, doc, next) {
  if (err.name === 'MongoServerError' && err.code === 11000) {
    next(new Error("Bu email allaqachon ro'yxatdan o'tgan"))
  } else {
    next(err)
  }
})

Xato qayta ishlovchi post hook — 3 parametrli (err, doc, next).


Qachon middleware ishlatmaslik

  • Email yuborish, tashqi API — post('save') ichida emas. Saqlash transaction ichida bo'lsa va keyin bekor qilinsa — email baribir ketgan. Navbat (Express kursi, 71-dars) yoki service qatlami
  • Murakkab biznes mantiq — yashirin "sehr" kodni tushunishni qiyinlashtiradi. Middleware — ma'lumot yaxlitligi uchun: xeshlash, slug, vaqt belgilari, kaskad o'chirish

Tez-tez uchraydigan xatolar

Arrow function

// ❌ — this undefined
userSchema.pre('save', async () => { this.parol = ... })

isModified siz xeshlash

Har bir save() da parol qayta xeshlanadi va user kira olmay qoladi.

Update da save hook ini kutish

findByIdAndUpdate — pre('save') ni chaqirmaydi.


Xulosa

  • Middleware — amaldan oldin (pre) yoki keyin (post) avtomatik kod
  • Document hook lar (save, validate) — this = document; faqat save/create da
  • Query hook lar (find, findOneAndUpdate, deleteMany) — this = query; /^find/ regex
  • Parol xeshlash — pre('save') + isModified; update da ishlamasligini unutmang
  • Kaskad o'chirish — pre('deleteOne', { document: true }) va pre('deleteMany')
  • Xato hook — (err, doc, next); tashqi amallar — middleware da emas

Keyingi dars: 47-dars — Statics va Methods — model va instance metodlari