Soft Delete — deletedAt, mongoose-delete plugin
Muammodan boshlaylik
Admin xato bilan mahsulotni o'chirdi:
await Mahsulot.findByIdAndDelete(id)
- Mahsulot — butunlay yo'q. Tiklash — faqat zaxiradan (34-dars), va zaxiradan keyingi o'zgarishlar yo'qoladi
- Eski buyurtmalarda shu mahsulotga havola bor — populate
nullqaytaradi, buyurtma tarixi "singan" - "Kim, qachon o'chirdi?" — hech qanday iz yo'q
Soft delete nima?
Soft delete — documentni haqiqatan o'chirmasdan, uni o'chirilgan deb belgilash:
// hard delete
db.mahsulotlar.deleteOne({ _id })
// soft delete
db.mahsulotlar.updateOne({ _id }, { $set: { deletedAt: new Date(), deletedBy: adminId } })
Oddiy so'rovlarda o'chirilganlar ko'rinmaydi, lekin ma'lumot bazada qoladi — tiklash, audit va tarixiy hisobotlar uchun.
Hayotiy o'xshatma
Kompyuterdagi "Korzina". Faylni o'chirsangiz — u yo'qolmaydi, korzinaga tushadi. Papkada ko'rinmaydi, lekin kerak bo'lsa — qaytarasiz. Korzinani vaqti-vaqti bilan tozalaysiz — shunda fayl butunlay o'chadi.
Qo'lda amalga oshirish
const mahsulotSchema = new Schema({
nomi: String,
narx: Number,
deletedAt: { type: Date, default: null, index: true },
deletedBy: { type: Schema.Types.ObjectId, ref: 'User' },
})
// o'chirish va tiklash
mahsulotSchema.methods.softDelete = function (userId) {
this.deletedAt = new Date()
this.deletedBy = userId
return this.save()
}
mahsulotSchema.methods.tikla = function () {
this.deletedAt = null
this.deletedBy = undefined
return this.save()
}
// barcha find so'rovlarida avtomatik filtr
mahsulotSchema.pre(/^find|^count/, function () {
if (this.getOptions().ochirilganlarBilan) return
this.where({ deletedAt: null })
})
mahsulotSchema.pre('aggregate', function () {
if (this.options.ochirilganlarBilan) return
this.pipeline().unshift({ $match: { deletedAt: null } })
})
const m = await Mahsulot.findById(id)
await m.softDelete(req.user.id)
await Mahsulot.find() // o'chirilganlar yo'q
await Mahsulot.findById(id) // null
// admin "korzina" sahifasi
await Mahsulot.find({ deletedAt: { $ne: null } }).setOptions({ ochirilganlarBilan: true })
setOptions — query ga o'z opsiyangizni berish; middleware uni getOptions() bilan o'qiydi.
Plugin sifatida
Bir nechta model uchun — plugin (48-dars):
// src/plugins/softDelete.js
export function softDeletePlugin(schema) {
schema.add({
deletedAt: { type: Date, default: null, index: true },
deletedBy: { type: schema.base.Schema.Types.ObjectId, ref: 'User' },
})
schema.methods.softDelete = function (userId) {
this.deletedAt = new Date()
this.deletedBy = userId
return this.save()
}
schema.methods.tikla = function () {
this.deletedAt = null
this.deletedBy = undefined
return this.save()
}
schema.pre(/^find|^count/, function () {
if (!this.getOptions().ochirilganlarBilan) this.where({ deletedAt: null })
})
}
mahsulotSchema.plugin(softDeletePlugin)
postSchema.plugin(softDeletePlugin)
mongoose-delete
Tayyor plagin:
npm install mongoose-delete
import mongooseDelete from 'mongoose-delete'
mahsulotSchema.plugin(mongooseDelete, {
deletedAt: true,
deletedBy: true,
overrideMethods: 'all',
})
await Mahsulot.deleteById(id, req.user.id) // soft delete
await Mahsulot.restore({ _id: id }) // tiklash
await Mahsulot.find() // o'chirilmaganlar
await Mahsulot.findDeleted() // faqat o'chirilganlar
await Mahsulot.findWithDeleted() // hammasi
overrideMethods: 'all' — find, count, update va boshqalar avtomatik o'chirilganlarni chiqarib tashlaydi. Plaginning Mongoose versiyangizga mosligini tekshiring.
Noyob maydonlar muammosi
email: { type: String, unique: true }
User o'chirildi (soft), keyin xuddi shu email bilan qayta ro'yxatdan o'tmoqchi — E11000 duplicate key: eski document hali bazada.
Yechim — partial unique index (27-dars): noyoblik faqat o'chirilmaganlar orasida:
userSchema.index(
{ email: 1 },
{ unique: true, partialFilterExpression: { deletedAt: null } }
)
Butunlay tozalash
Soft delete qilingan ma'lumotlar abadiy saqlanmasin — xotira va qonuniy talablar (shaxsiy ma'lumotlarni o'chirish huquqi):
// 90 kundan keyin avtomatik — TTL index
mahsulotSchema.index({ deletedAt: 1 }, { expireAfterSeconds: 90 * 24 * 60 * 60 })
deletedAt: null bo'lgan documentlarga TTL ta'sir qilmaydi — faqat sana qo'yilganlarga.
[!WARNING] Soft delete — shaxsiy ma'lumotlarni o'chirish talabini bajarmaydi: ma'lumot bazada qoladi. Foydalanuvchi "hisobimni o'chiring" desa — shaxsiy maydonlarni (ism, email, telefon) haqiqatan o'chiring yoki anonimlashtiring, faqat statistika uchun kerakli qismini qoldiring.
Qachon soft delete
✅ Mos:
- Xato bilan o'chirish ehtimoli bor (admin panel)
- Boshqa documentlar unga havola qiladi (buyurtmadagi mahsulot)
- Audit kerak: kim, qachon o'chirdi
❌ Mos emas:
- Vaqtinchalik ma'lumotlar (sessiyalar, kodlar) — hard delete yoki TTL
- Juda katta, tez o'zgaruvchan collection lar — "o'lik" documentlar index va so'rovlarni og'irlashtiradi
Tez-tez uchraydigan xatolar
Filtrni unutish
Middleware siz — har bir so'rovda deletedAt: null ni qo'lda yozish kerak va bir joyda albatta unutiladi. Avtomatik filtr (middleware/plagin).
Aggregation va $lookup da filtr
Find middleware aggregation da ishlamaydi (49-dars); $lookup ichida ham o'chirilganlar ko'rinadi — pipeline: [{ $match: { deletedAt: null } }].
Unique index
Partial unique index siz — o'chirilgan qiymatni qayta ishlatib bo'lmaydi.
Xulosa
- Soft delete —
deletedAtbilan belgilash, haqiqatan o'chirmaslik; tiklash va audit - Avtomatik filtr —
pre(/^find|^count/)vapre('aggregate');setOptionsbilan istisno - Plugin sifatida qayta ishlatish yoki
mongoose-delete - Noyob maydonlar —
partialFilterExpression: { deletedAt: null } - TTL index — N kundan keyin butunlay o'chirish
- Shaxsiy ma'lumotlarni o'chirish talabi — haqiqiy o'chirish yoki anonimlashtirish
Keyingi dars: 58-dars — Caching — Redis bilan MongoDB natijalarini keshlash