Database Transaction — nima, qachon kerak, qanday yoziladi
Muammodan boshlaylik
Foydalanuvchi bank orqali pul o'tkazmoqda. Ikki operatsiya bo'lishi kerak:
- Ali ning hisobidan 1000 so'm yechish
- Valining hisobiga 1000 so'm qo'shish
Kod:
await pool.query('UPDATE accounts SET balance = balance - 1000 WHERE id = 1');
await pool.query('UPDATE accounts SET balance = balance + 1000 WHERE id = 2');
Birinchi so'rov bajarildi. Ikkinchi so'rov bajarilayotganda server to'xtab qoldi — elektr o'chdi, xato chiqdi.
Natija: Ali ning hisobidan 1000 so'm yechildi, lekin Valining hisobiga qo'shilmadi. 1000 so'm yo'qoldi.
Bu — ma'lumotlar yaxlitligi muammosi. Transaction bu muammoni hal qiladi.
Transaction nima?
Transaction — bir nechta operatsiyani bitta butun sifatida bajarish. Yoki hammasi bajariladi, yoki hech biri.
Hayotiy o'xshatma: siz do'konda to'lov qilasiz. Kartangizdan pul yechildi, lekin kassa cheki chiqmadi. Do'kon sizdan pul oldi, lekin siz tovar olmadingiz. Bu adolatsiz. To'g'ri tizimda: pul yechildi va chek chiqdi — ikkalasi birga. Biri bajarilmasa — pul qaytariladi.
Transaction ning to'rtta xususiyati bor — ACID deyiladi:
Atomicity — hammasi yoki hech biri. Yarim holat bo'lmaydi.
Consistency — operatsiyadan oldin va keyin ma'lumotlar qoidalarga mos bo'ladi.
Isolation — bir vaqtda bajarilayotgan transaksiyalar bir-biriga ta'sir qilmaydi.
Durability — bajarilgan transaksiya server to'xtasa ham saqlanib qoladi.
PostgreSQL da transaction
Transaction uchta buyruq bilan boshqariladi:
BEGIN — transaksiyani boshlash. Bundan keyin barcha o'zgarishlar vaqtinchalik.
COMMIT — o'zgarishlarni tasdiqlash. Endi ular doimiy.
ROLLBACK — o'zgarishlarni bekor qilish. Hammasi boshlang'ich holatga qaytadi.
const pool = require('../db');
async function transferMoney(fromId, toId, amount) {
const client = await pool.connect(); // alohida ulanish olish
try {
await client.query('BEGIN');
// 1. Yechish
const from = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING balance',
[amount, fromId]
);
// Balans yetarlimi?
if (from.rows[0].balance < 0) {
throw new Error('Balans yetarli emas');
}
// 2. Qo'shish
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT'); // hammasi bajarildi — tasdiqlash
return { success: true };
} catch (err) {
await client.query('ROLLBACK'); // xato — hammasi bekor
throw err;
} finally {
client.release(); // ulanishni poolga qaytarish
}
}
Nima uchun pool.connect() — pool.query() emas? pool.query() har so'rovda boshqa ulanish ishlatishi mumkin. Transaction esa bir ulanishda bo'lishi kerak — BEGIN, COMMIT, ROLLBACK bir xil ulanishda.
finally bloki — xato bo'lsa ham, bo'lmasa ham client.release() chaqiriladi. Ulanish poolga qaytarilmasa — pool to'lib qoladi, yangi so'rovlar kutib qoladi.
Qachon transaction kerak?
Har bir so'rov uchun transaction kerak emas — bu ortiqcha. Transaction quyidagi hollarda kerak:
Bir nechta jadval o'zgarganda — buyurtma yaratilganda ham orders jadvaliga yoziladi, ham inventory (ombor) kamayadi. Biri bajarilmasa — ikkinchisi ham bekor bo'lishi kerak.
Pul operatsiyalarida — yuqoridagi misol.
Bog'liq ma'lumotlar yaratilganda — foydalanuvchi ro'yxatdan o'tganda users va profiles jadvallariga bir vaqtda yoziladi.
async function createOrder(userId, items) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. Buyurtma yaratish
const orderResult = await client.query(
'INSERT INTO orders (user_id, status) VALUES ($1, $2) RETURNING id',
[userId, 'pending']
);
const orderId = orderResult.rows[0].id;
// 2. Har bir mahsulot uchun
for (const item of items) {
// Buyurtma elementini qo'shish
await client.query(
'INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)',
[orderId, item.productId, item.quantity]
);
// Ombordan kamaytirish
const stock = await client.query(
'UPDATE products SET stock = stock - $1 WHERE id = $2 AND stock >= $1 RETURNING stock',
[item.quantity, item.productId]
);
if (stock.rows.length === 0) {
throw new Error(`Mahsulot ${item.productId} omborda yetarli emas`);
}
}
await client.query('COMMIT');
return { orderId };
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
Agar biror mahsulot omborda yetarli bo'lmasa — ROLLBACK chaqiriladi. Buyurtma ham, boshqa mahsulotlar ham o'zgartirilmaydi. Hamma narsa boshlang'ich holatda qoladi.
Tez-tez uchraydigan xatolar
client.release() unutish:
const client = await pool.connect();
try {
// ...
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
}
// release() yo'q — ulanish poolga qaytmaydi
// Pool to'lib qoladi, yangi so'rovlar qotib qoladi
finally blokida client.release() — har doim.
ROLLBACK da xato:
} catch (err) {
await client.query('ROLLBACK'); // bu ham xato berishi mumkin
throw err;
}
Agar ROLLBACK ham xato bersa — asl xato yo'qoladi. Yaxshisi:
} catch (err) {
try { await client.query('ROLLBACK'); } catch {}
throw err;
}
Xulosa
Transaction — bir nechta operatsiyani bitta butun sifatida bajarish. Yoki hammasi, yoki hech biri — yarim holat bo'lmaydi.
BEGIN → operatsiyalar → COMMIT (muvaffaqiyatli) yoki ROLLBACK (xato).
pool.connect() — alohida ulanish, transaction uchun kerak. finally da client.release() — har doim.
Pul operatsiyalari, bir nechta jadval o'zgarganda, bog'liq ma'lumotlar yaratilganda — transaction kerak.
Keyingi dars
19-dars: Connection Pool — nima uchun kerak, qanday sozlanadi