Mongoose bilan Transaction — session

~20 daq · MongoDB · Mongoose — Ilg'or

Muammodan boshlaylik

Buyurtma berish:

for (const q of qatorlar) {
  await Mahsulot.updateOne({ _id: q.mahsulot }, { $inc: { qoldiq: -q.soni } })
}
const buyurtma = await Buyurtma.create({ mijoz, qatorlar, summa })
await User.updateOne({ _id: mijoz }, { $inc: { bonus: Math.floor(summa / 100) } })

Uchinchi mahsulotda qoldiq yetmadi va xato tashlandi. Birinchi ikki mahsulot qoldig'i allaqachon kamaygan, buyurtma esa yo'q. Ombor hisobi buzildi.

31-darsda transaction ni driver bilan ko'rdik. Mongoose da u qanday ishlaydi?


Hayotiy o'xshatma

Do'kondagi kassa. Kassir mahsulotlarni birma-bir skanerlaydi, lekin to'lov o'tmaguncha hech narsa "sotilgan" hisoblanmaydi. Karta rad etilsa — butun chek bekor qilinadi, hech bir mahsulot ombordan hisobdan chiqmaydi.


connection.transaction()

Mongoose da eng qulay usul:

import mongoose from 'mongoose'

export async function buyurtmaBer(mijozId, qatorlar) {
  return mongoose.connection.transaction(async (session) => {
    let summa = 0

    for (const q of qatorlar) {
      const m = await Mahsulot.findOneAndUpdate(
        { _id: q.mahsulot, qoldiq: { $gte: q.soni } },
        { $inc: { qoldiq: -q.soni } },
        { new: true, session }
      )
      if (!m) throw new Error(`Mahsulot yetarli emas: ${q.mahsulot}`)
      summa += m.narx * q.soni
    }

    const [buyurtma] = await Buyurtma.create(
      [{ mijoz: mijozId, qatorlar, summa }],
      { session }
    )

    await User.updateOne(
      { _id: mijozId },
      { $inc: { bonus: Math.floor(summa / 100) } },
      { session }
    )

    return buyurtma
  })
}
  • connection.transaction(fn) — sessiya yaratadi, transaction boshlaydi, fn muvaffaqiyatli bo'lsa commit, xato bo'lsa abort, vaqtinchalik xatolarda qayta urinadi
  • Har bir operatsiyaga { session }
  • fn qaytargan qiymat — natija

[!WARNING] Model.create() ga session uzatish uchun documentlar massiv ichida bo'lishi kerak: Model.create([doc], { session }). Model.create(doc, { session }) — ikkinchi argument ikkinchi document deb tushuniladi va session e'tiborsiz qoladi.


Session ni uzatish usullari

// query opsiyasi
await Mahsulot.find({ ... }, null, { session })
await Mahsulot.updateOne(filtr, update, { session })

// query metodi
await Mahsulot.findById(id).session(session)

// document — save
const m = await Mahsulot.findById(id).session(session)
m.qoldiq -= 1
await m.save()        // findById dagi session avtomatik ishlatiladi

session bilan yuklangan document ning save() i shu sessiyada bajariladi.

Avtomatik session — AsyncLocalStorage

Har bir chaqiruvga { session } yozishni unutish oson. Mongoose 8+ da:

mongoose.set('transactionAsyncLocalStorage', true)

await mongoose.connection.transaction(async () => {
  await Mahsulot.updateOne(filtr, update)    // session avtomatik
  await Buyurtma.create([doc])
})

Xatoni qayta ishlash

router.post('/buyurtmalar', auth, async (req, res) => {
  try {
    const buyurtma = await buyurtmaBer(req.user.id, req.body.qatorlar)
    res.status(201).json(buyurtma)
  } catch (err) {
    if (err.message.startsWith('Mahsulot yetarli emas')) {
      return res.status(409).json({ xato: err.message })
    }
    throw err
  }
})

Transaction bekor bo'lganda barcha o'zgarishlar qaytariladi — qoldiqlar ham, buyurtma ham, bonus ham.


Document holati — muhim nozik joy

Transaction bekor bo'lganda baza qaytariladi, lekin xotiradagi Mongoose documentlar emas:

const user = await User.findById(id)
try {
  await mongoose.connection.transaction(async (session) => {
    user.bonus += 100
    await user.save({ session })
    throw new Error('xato')
  })
} catch {}

user.bonus   // xotirada +100, bazada esa o'zgarmagan

connection.transaction() buni hisobga oladi va abort da document holatini tiklashga harakat qiladi, lekin eng ishonchli yo'l — transaction ichida ishlatiladigan documentlarni transaction ichida yuklash.


Talablar va cheklovlar

  • Replica set yoki sharded cluster kerak (35-dars); Atlas — tayyor
  • Development: bitta a'zoli replica set yoki mongodb-memory-server ning MongoMemoryReplSet i (testlar uchun)
  • 60 soniya chegarasi; transaction ichida email, tashqi API yo'q (31-dars)
  • Transaction ichida collection va index yaratmang — ularni oldindan yarating (Model.createCollection(), syncIndexes())
// testlar uchun
import { MongoMemoryReplSet } from 'mongodb-memory-server'

const replSet = await MongoMemoryReplSet.create({ replSet: { count: 1 } })
await mongoose.connect(replSet.getUri())

[!TIP] Transaction ichidagi operatsiyalar ketma-ket bo'lsin — Promise.all bilan bitta sessiyada parallel so'rovlar yubormang. Bitta sessiyada bir vaqtda faqat bitta operatsiya bajarilishi kerak.


Tez-tez uchraydigan xatolar

{ session } ni bitta joyda unutish

O'sha operatsiya transaction dan tashqarida bajariladi va abort da qaytarilmaydi.

create(doc, { session })

Massiv kerak: create([doc], { session }).

Transaction ichida Promise.all

// ❌
await Promise.all(qatorlar.map((q) => Mahsulot.updateOne(..., { session })))

Ketma-ket for...of bilan.


Xulosa

  • mongoose.connection.transaction(async (session) => {...}) — commit, abort va qayta urinish avtomatik
  • Har bir operatsiyaga { session } yoki .session(session); transactionAsyncLocalStorage — avtomatik
  • Model.create([doc], { session }) — massiv shart
  • Abort da baza qaytariladi; documentlarni transaction ichida yuklang
  • Replica set kerak; testlar uchun MongoMemoryReplSet
  • Ketma-ket operatsiyalar, Promise.all emas

Keyingi dars: 51-dars — Discriminator — bir collection, bir nechta schema