Mongoose bilan Transaction — session
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,fnmuvaffaqiyatli bo'lsa commit, xato bo'lsa abort, vaqtinchalik xatolarda qayta urinadi- Har bir operatsiyaga
{ session } fnqaytargan 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-serverningMongoMemoryReplSeti (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.allbilan 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.allemas
Keyingi dars: 51-dars — Discriminator — bir collection, bir nechta schema