upsert, findOneAndUpdate, findOneAndDelete

~15 daq · MongoDB · CRUD

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:

  1. find — bor yoki yo'qligini tekshirish
  2. Bor bo'lsa update, yo'q bo'lsa insert

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 + update o'rniga — har doim findOneAndUpdate ishlating

Keyingi dars: 11-dars — deleteOne, deleteMany — xavfli holatlar