Database Transaction — nima, qachon kerak, qanday yoziladi

~20 daq · Express · Ma'lumotlar Bazasi

Muammodan boshlaylik

Foydalanuvchi bank orqali pul o'tkazmoqda. Ikki operatsiya bo'lishi kerak:

  1. Ali ning hisobidan 1000 so'm yechish
  2. 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