upsert, findOneAndUpdate, findOneAndDelete
Muammodan boshlaylik
Foydalanuvchi tizimga kirdi. Siz uning "so'nggi faollik" ma'lumotini saqlashingiz kerak. Agar u avval ham kirgan bo'lsa — yangilash, aks holda — yangi yozuv yaratish. Buning uchun odatda ikki qadam qilinadi:
find— bor yoki yo'qligini tekshirish- Bor bo'lsa
update, yo'q bo'lsainsert
Bu yondashuv muammo tug'diradi: ikkita alohida operatsiya — ikki marta server so'rovi, race condition xavfi (ikki so'rov bir vaqtda kelsa, ikkalasi ham insert qilishi mumkin).
MongoDB da buning uchun upsert va findOneAndUpdate bor.
upsert — update + insert
upsert = update + insert
Agar filter bo'yicha document topilsa — yangilaydi. Topilmasa — yangi document yaratadi. Hammasi bitta atomik operatsiyada.
db.users.updateOne(
{ email: "jasur@gmail.com" }, // filter
{ $set: { so_nggi_kirish: new Date() } }, // update
{ upsert: true } // ← shu parametr
)
Holat 1 — document topildi:
email: "jasur@gmail.com" → so'nggi_kirish yangilandi
Holat 2 — document topilmadi:
Yangi document yaratildi:
{ email: "jasur@gmail.com", so'nggi_kirish: <hozir> }
Qaytarilgan natija
let natija = db.users.updateOne(
{ email: "jasur@gmail.com" },
{ $set: { so_nggi_kirish: new Date() } },
{ upsert: true }
)
// Yangilangan bo'lsa:
{ matchedCount: 1, modifiedCount: 1, upsertedId: null }
// Yangi yaratilgan bo'lsa:
{ matchedCount: 0, modifiedCount: 0, upsertedId: ObjectId("...") }
// ↑
// yaratilgan documentning _id si
upsertedId — yangi document yaratilganmi yo'qmi bilish uchun.
upsert — $setOnInsert
Ba'zan faqat yangi document yaratilganda qo'shiladigan maydonlar kerak bo'ladi. Masalan: yaratilgan_vaqt — yangilashda o'zgarmasligi kerak, lekin yaratilganda qo'shilishi kerak.
db.users.updateOne(
{ email: "jasur@gmail.com" },
{
$set: { so_nggi_kirish: new Date() }, // har doim yangilanadi
$setOnInsert: { yaratilgan: new Date(), rol: "foydalanuvchi" }
// ↑ faqat yangi document yaratilganda qo'shiladi
},
{ upsert: true }
)
Yangilashda: so'nggi_kirish yangilandi, yaratilgan va rol o'zgarmadi
Yaratishda: so'nggi_kirish, yaratilgan, rol — hammasi qo'shildi
Real loyihalarda $setOnInsert — yaratilgan_vaqt, initial_rol, invite_code kabi bir marta yoziladigan maydonlar uchun ishlatiladi.
findOneAndUpdate — yangilangan documentni qaytarish
Oddiy updateOne yangilangan documentni qaytarmaydi — faqat statistika (matchedCount, modifiedCount) qaytadi. Lekin ko'pincha yangilangan documentning o'zini ko'rish kerak.
Tasavvur qiling: foydalanuvchi "like" bosdi. Siz like_soni ni oshirdingiz — ammo yangilangan like_soni ni response ga qaytarishingiz kerak.
// ❌ Noqulay usul
await db.postlar.updateOne({ _id: id }, { $inc: { like: 1 } })
let post = await db.postlar.findOne({ _id: id }) // ikkinchi so'rov
// ✅ findOneAndUpdate — bitta so'rovda
let post = await db.postlar.findOneAndUpdate(
{ _id: id },
{ $inc: { like: 1 } },
{ returnDocument: "after" } // ← yangilangandan KEYIN gi holatni ber
)
returnDocument parametri
// returnDocument: "before" — yangilashdan OLDIN gi holat (default)
// returnDocument: "after" — yangilashdan KEYIN gi holat
let oldin = await db.users.findOneAndUpdate(
{ ism: "Jasur" },
{ $inc: { yosh: 1 } },
{ returnDocument: "before" }
)
// oldin.yosh → 24 (yangilashdan avvalgi qiymat)
let keyin = await db.users.findOneAndUpdate(
{ ism: "Jasur" },
{ $inc: { yosh: 1 } },
{ returnDocument: "after" }
)
// keyin.yosh → 25 (yangilangan qiymat)
findOneAndUpdate — upsert bilan
// Topilmasa yaratadi va yangi documentni qaytaradi
let doc = await db.sessiyalar.findOneAndUpdate(
{ foydalanuvchi_id: userId },
{
$set: { so_nggi_faollik: new Date() },
$setOnInsert: { yaratilgan: new Date() }
},
{ upsert: true, returnDocument: "after" }
)
Projection — faqat kerakli maydonlar
// Faqat ism va email ni qaytarish
let foydalanuvchi = await db.users.findOneAndUpdate(
{ _id: userId },
{ $set: { faol: true } },
{
returnDocument: "after",
projection: { ism: 1, email: 1 } // ← faqat bu maydonlar
}
)
findOneAndDelete — topib o'chirish
O'chirilgan documentning o'zini qaytaradi. "Queue" (navbat) tizimlari uchun juda foydali — taskni navbatdan olib, o'sha haqda ish qilasiz.
// Navbatdagi birinchi ishni olish va o'chirish (atomik)
let task = await db.tasks.findOneAndDelete(
{ status: "kutilmoqda" },
{ sort: { yaratilgan: 1 } } // ← eng eski task birinchi
)
if (task) {
await taskniBajar(task) // task o'chirildi, ishni bajar
}
Agar bu ikkita qadam bo'lganida:
1. find() → task topildi
2. delete() → o'chirildi
Muammo: ikkala server bir vaqtda bir xil taskni topishi mumkin!
findOneAndDelete — atomik, bu muammo yo'q.
findOneAndDelete — projection bilan
// O'chirilgan documentdan faqat kerakli maydonlarni olish
let o_chirilgan = await db.vaqtinchalik_kodlar.findOneAndDelete(
{ kod: userKirganKod, muddati: { $gt: new Date() } },
{ projection: { foydalanuvchi_id: 1 } }
)
if (!o_chirilgan) {
// kod noto'g'ri yoki muddati o'tgan
}
Bu pattern — OTP (bir martalik kod) tekshirishda juda keng ishlatiladi: kod to'g'ri bo'lsa — o'chirib, foydalanuvchi id sini olasiz (bir vaqtda ikki marta ishlatilmaydi).
findOneAndReplace — topib almashtirish
Butun documentni almashtiradi ($set emas — to'liq replacement):
let eski = await db.config.findOneAndReplace(
{ kalit: "sozlamalar" },
{ kalit: "sozlamalar", qiymat: yangiQiymat, yangilangan: new Date() },
{ returnDocument: "before" }
)
Taqqoslash — qaysi metoddan foydalanish
┌──────────────────────┬────────────────────────────────────────────┐
│ Metod │ Qachon ishlatish │
├──────────────────────┼────────────────────────────────────────────┤
│ updateOne + upsert │ Document kerak emas, faqat yangilash/yaratish│
│ findOneAndUpdate │ Yangilangan documentni qaytarish kerak │
│ findOneAndDelete │ O'chirib, o'chirilgan documentni ishlatish │
│ findOneAndReplace │ Butun documentni almashtirish │
└──────────────────────┴────────────────────────────────────────────┘
Tez-tez uchraydigan xatolar
returnDocument yo'q — eski qiymat keladi
// ❌ Yangilangan qiymat kerak — lekin eski keladi
let doc = await db.users.findOneAndUpdate(
{ _id: id },
{ $inc: { ball: 10 } }
// returnDocument ko'rsatilmagan → default "before"
)
// doc.ball → 90 (eski), aslida 100 bo'ldi
// ✅
let doc = await db.users.findOneAndUpdate(
{ _id: id },
{ $inc: { ball: 10 } },
{ returnDocument: "after" } // ← majburiy
)
// doc.ball → 100
upsert da filter maydoni documentga tushmaydi
// ❌ Yangi document yaratilganda email bo'lmaydi!
db.users.updateOne(
{ email: "jasur@gmail.com" },
{ $set: { ism: "Jasur" } },
{ upsert: true }
)
// Yaratilgan: { _id: ..., ism: "Jasur" } ← email qani?
Sababi: $set faqat berilgan maydonlarni yozadi.
Filter da kelgan email $set ichida bo'lmasa — document ga tushmaydi.
✅ To'g'ri:
db.users.updateOne(
{ email: "jasur@gmail.com" },
{ $set: { email: "jasur@gmail.com", ism: "Jasur" } },
{ upsert: true }
)
// Yoki $setOnInsert:
db.users.updateOne(
{ email: "jasur@gmail.com" },
{
$set: { ism: "Jasur" },
$setOnInsert: { email: "jasur@gmail.com" }
},
{ upsert: true }
)
findOneAndUpdate — null qaytarsa
let doc = await db.users.findOneAndUpdate(
{ _id: "mavjudemas" },
{ $set: { faol: true } }
)
// ❌ doc null — tekshirilmadi
console.log(doc.ism) // TypeError: Cannot read properties of null
// ✅ Tekshirish
if (!doc) {
throw new Error("Foydalanuvchi topilmadi")
}
Xulosa
- upsert — bor bo'lsa yangilaydi, yo'q bo'lsa yaratadi — bitta atomik operatsiya
- $setOnInsert — faqat yangi document yaratilganda qo'shiladigan maydonlar uchun
- findOneAndUpdate — yangilangan documentni qaytarish kerak bo'lganda,
returnDocument: "after"bilan - findOneAndDelete — o'chirib, o'chirilgan document bilan ish qilish (navbat, OTP) uchun
- Ikkita alohida
find + updateo'rniga — har doim findOneAndUpdate ishlating
Keyingi dars: 11-dars — deleteOne, deleteMany — xavfli holatlar