IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL48/64-dars19 daqiqa
Mundarija (21)

Tranzaksiya asoslari: BEGIN, COMMIT, ROLLBACK, SAVEPOINT va Node'da to'g'ri naqsh

Qisqacha: Tranzaksiya — bir nechta o'zgarishni bitta bo'linmas ishga aylantirish: BEGIN … COMMIT — yo hammasi saqlanadi, yo xato bo'lsa ROLLBACK bilan hech biri. Node'da asosiy qoida: tranzaksiyaning hamma so'rovlari bitta ulanishda bo'lsin. pool.query("BEGIN") buni buzadi — bizning sinovimizda BEGIN va INSERT bitta ulanishda, ROLLBACK boshqasida bajarildi va birinchi ulanish "idle in transaction" holida osilib qoldi. To'g'ri naqsh: pool.connect() → BEGIN → COMMIT / catch da ROLLBACK → finally da release(). SAVEPOINT — tranzaksiya ichida qisman orqaga qaytish.

Bu darsda

  • BEGIN, COMMIT, ROLLBACK ni psql'da ishlatasiz va xatodan keyin tranzaksiya nima bo'lishini bilasiz.
  • Autocommit nimaligini va har so'rov o'z tranzaksiyasi ekanini tushuntirasiz.
  • SAVEPOINT bilan importda yomon qatorlarni o'tkazib, qolganini saqlaysiz.
  • pool.query("BEGIN") tuzog'ini taniysiz va tranzaksiyada(fn) yordamchisini yozasiz.
  • 25P02 xatosi va "idle in transaction" holatini aniqlaysiz.

Oldin bilishingiz kerak: ACID, BASE, CAP va PACELC, Connection pooling, node-postgres (pg) asoslari, UPDATE va DELETE xavfsiz.

1. Nega bu kerak?

25-qism API'sida "vazifa va birinchi izohni birga qo'shish" funksiyasi bor deylik. Ikki INSERT: avval vazifa, keyin unga izoh. Birinchisi o'tdi, ikkinchisi xato berdi (izoh bo'sh — CHECK cheklovi). Natija: bazada izohsiz vazifa qoldi. Foydalanuvchi "xato" xabarini ko'rib, yana bosadi — endi ikkita bir xil vazifa (yoki unique xatosi).

Yoki Savat: "o'chirilgan vazifani tiklash" — vazifaning ochirilgan_vaqt ini tozalash va tarixga yozish. Biri bajarilib, ikkinchisi bajarilmasa — ma'lumot o'zi bilan o'zi kelishmaydi.

ACID darsida bu muammoning nomini aytgan edik: A — atomiklik (atomicity). Bir nechta o'zgarish — bitta "atom": yo butunlay bo'ladi, yo umuman bo'lmaydi. Postgres buni tranzaksiya (transaction) bilan beradi.

O'xshatish: Click orqali do'konga pul o'tkazish. Sizning hisobingizdan yechish va do'kon hisobiga qo'shish — ikki amal. Birinchisidan keyin elektr o'chsa, pul "havoda" qolmasligi kerak. Bank uchun bu ikki amal — bitta tranzaksiya: yo ikkalasi, yo hech biri.

2. SQL'da tranzaksiya

2.1 BEGIN, COMMIT va xato

Shahnoza (8-foydalanuvchi) uchun vazifa va bo'sh izohni bitta tranzaksiyada qo'shamiz. Biz ishga tushirdik (PostgreSQL 17.6, holat 40, toza baza):

sql
BEGIN;
INSERT INTO vazifalar (egasi_id, matn)
  VALUES (8, 'Tranzaksiya sinovi') RETURNING id;
INSERT INTO izohlar (vazifa_id, muallif_id, matn)
  VALUES (currval('vazifalar_id_seq'), 8, '');
SELECT count(*) FROM vazifalar WHERE egasi_id = 8;
COMMIT;
SELECT count(*) FROM vazifalar WHERE egasi_id = 8;
text
BEGIN
 id 
----
 49
(1 row)

INSERT 0 1
ERROR:  new row for relation "izohlar" violates check constraint "izohlar_matn_check"
DETAIL:  Failing row contains (16, 49, 8, , 2026-10-24 10:54:46.266852+05).
ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK
 count 
-------
     0
(1 row)

Qatorma-qator:

  1. BEGIN — tranzaksiya boshlandi.
  2. Vazifa qo'shildi, id = 49. (currval('vazifalar_id_seq') — shu sessiyada oxirgi berilgan id.)
  3. Izoh — bo'sh matn, CHECK buzildi (CREATE TABLE va cheklovlar).
  4. Keyingi SELECT — bajarilmadi: "joriy tranzaksiya buzilgan, tranzaksiya bloki tugaguncha buyruqlar e'tiborsiz qoldiriladi". Xatodan keyin tranzaksiya buzilgan holatda: undagi hech narsa endi saqlanmaydi.
  5. COMMIT yozdik — Postgres ROLLBACK deb javob berdi! Buzilgan tranzaksiyani COMMIT qilib bo'lmaydi; Postgres jim orqaga qaytardi.
  6. Natija: Shahnozada 0 vazifa. 49-vazifa ham saqlanmadi.

Kichik detal: id 49 "yonib ketdi". Ketma-ketlik (sequence) tranzaksiya bilan orqaga qaytmaydi — keyingi vazifa 50 oladi. ID'lardagi bo'shliqlar normal holat, ularga tayanmang.

2.2 ROLLBACK — qo'lda bekor qilish

ROLLBACK ni o'zimiz ham chaqira olamiz. UPDATE va DELETE xavfsiz darsidagi odat: xavfli o'zgarishni avval tranzaksiyada sinash:

sql
BEGIN;
DELETE FROM vazifalar WHERE egasi_id = 1;
SELECT count(*) FROM vazifalar WHERE egasi_id = 1;
ROLLBACK;
SELECT count(*) FROM vazifalar WHERE egasi_id = 1;
text
BEGIN
DELETE 13
 count 
-------
     0
(1 row)

ROLLBACK
 count 
-------
    13
(1 row)

Azizning hamma 13 ta vazifasi (Savatdagisi bilan) tranzaksiya ichida o'chdi, count 0. ROLLBACK — hammasi qaytdi, 13. (Kaskad bilan o'chgan izohlar va teglar ham qaytdi.) Tranzaksiya ichidagi o'zgarishni faqat shu sessiya ko'radi; boshqalar uni COMMIT'dan keyin ko'radi (bu qanday ishlashi — MVCC darsida).

2.3 Autocommit

BEGIN siz yozilgan har buyruq ham tranzaksiya — faqat bitta buyruqdan iborat, o'zi boshlanib, o'zi COMMIT bo'ladi. Bu autocommit rejimi: psql ham, pg ham sukut bo'yicha shunday. Kanon ikkita alohida pool.query("SELECT txid_current()") ni solishtirdi. txid_current() — joriy tranzaksiyaning raqami (txid). Ikki raqamga qarang:

text
txid'lar (autocommit): 61520, 61521

Ikki so'rov — ikki xil tranzaksiya raqami. Demak ketma-ket ikkita pool.query — ikkita alohida tranzaksiya, ular orasida boshqa so'rovlar o'z o'zgarishlarini COMMIT qilib ulguradi.

Tekshirib ko'ring: Tranzaksiya ichida xato bo'ldi, siz uni sezmay COMMIT yozdingiz. Postgres nima qaytaradi va ma'lumot saqlanadimi?

Javob

ROLLBACK qaytaradi va hech narsa saqlanmaydi — hatto xatodan oldingi muvaffaqiyatli INSERT lar ham. Node'da client.query("COMMIT") xato tashlamaydi, natijaning command i esa "ROLLBACK" bo'ladi. Shuning uchun xatoni try/catch da ushlash va o'zingiz ROLLBACK qilish kerak — "COMMIT o'tdi" degani "saqlandi" degani emas.

3. SAVEPOINT: qisman orqaga qaytish

Ba'zan butun tranzaksiyani emas, faqat oxirgi qadamni bekor qilish kerak. Masalan CSV import: 4 qatordan 2 tasi yomon — yaxshilarini saqlash, yomonlarini hisobotga yozish. Xato butun tranzaksiyani buzadi — lekin SAVEPOINT (saqlash nuqtasi) bilan shu nuqtagacha qaytish mumkin:

sql
BEGIN;
INSERT INTO vazifalar (egasi_id, matn) VALUES (8, 'Import 1 #import');
SAVEPOINT qator;
INSERT INTO vazifalar (egasi_id, matn) VALUES (8, '');
ROLLBACK TO SAVEPOINT qator;
INSERT INTO vazifalar (egasi_id, matn) VALUES (8, 'Import 2 #import');
COMMIT;
SELECT id, matn FROM vazifalar WHERE egasi_id = 8 ORDER BY id;
text
BEGIN
INSERT 0 1
SAVEPOINT
ERROR:  new row for relation "vazifalar" violates check constraint "vazifalar_matn_check"
DETAIL:  Failing row contains (51, 8, , f, 2026-10-24 10:54:46.288535+05, orta, null, null, null, 0, 2026-10-24 10:54:46.288535+05, null, 1).
ROLLBACK
INSERT 0 1
COMMIT
 id |       matn       
----+------------------
 50 | Import 1 #import
 52 | Import 2 #import
(2 rows)

Bo'sh matn xato berdi — tranzaksiya buzildi. ROLLBACK TO SAVEPOINT qator (psql uni qisqa ROLLBACK deb ko'rsatdi) — SAVEPOINT dan keyingi hamma narsani bekor qilib, tranzaksiyani tuzatdi. Keyingi INSERT ishladi, COMMIT — haqiqiy COMMIT. Natija: 2 qator saqlandi, yomoni — yo'q (id 51 yana yonib ketdi).

stateDiagram-v2
  [*] --> faol: BEGIN
  faol --> faol: so'rov muvaffaqiyatli
  faol --> buzilgan: xato
  buzilgan --> faol: ROLLBACK TO SAVEPOINT
  faol --> [*]: COMMIT (saqlandi)
  buzilgan --> [*]: COMMIT yoki ROLLBACK (bekor)

Diagrammaga qarang: "buzilgan" holatdan ikki yo'l bor — saqlash nuqtasiga qaytish (tranzaksiya davom etadi) yoki tugatish (hammasi bekor). Bu yerdan to'g'ridan "saqlandi" ga yo'l yo'q.

Diqqat: SAVEPOINT bepul emas: har biri serverda kichik tranzaksiya. Har qatorga savepoint — 1 000 qatorli importda 1 000 ta. Juda katta importlarda (COPY, batch va cursor) avval vaqtinchalik jadvalga yuklab, yomon qatorlarni SQL bilan ajratish tezroq.

4. Node'da tranzaksiya

4.1 Tuzoq: pool.query("BEGIN")

Yuqoridagi SQL'ni Node'ga "to'g'ridan" ko'chirsak:

js
// ❌ HECH QACHON shunday yozmang
await pool.query("BEGIN");
await pool.query("INSERT INTO vazifalar …");
await pool.query("ROLLBACK");

Connection pooling darsini eslang: pool.query har safar istalgan bo'sh ulanishni oladi va qaytaradi. Tranzaksiya esa ulanishga bog'liq: BEGIN qaysi ulanishda bo'lsa, tranzaksiya o'sha ulanishda. Kanon skripti (t48-tranzaksiya.mjs) bu tuzoqni ataylab ko'rsatadi: hovuzda 3 ta ulanish, BEGIN, parallel ikkita so'rov (biri INSERT), keyin ROLLBACK. pg_backend_pid() — so'rovni bajargan server jarayonining raqami (pid); har so'rov o'z pid'ini qaytaradi. Biz shu qismni 5 marta ishga tushirdik (PostgreSQL 17.6, holat 40) — besh marta ham bir xil manzara. Birinchi urinish:

text
boshida: 0 | BEGIN / INSERT / ROLLBACK (pid): 4248 / 4248 / 21416 → ROLLBACK'dan keyin: 0
idle in transaction ulanishlar: 1
pool.end() dan keyin bazada: 0

Pid'larni solishtiring: uchta so'rov — ikkita turli ulanish.

sequenceDiagram
  participant N as Node (pool.query)
  participant A as Ulanish 4248
  participant B as Ulanish 21416
  N->>A: BEGIN
  N->>A: INSERT (tranzaksiya ichida)
  N->>B: ROLLBACK
  Note over B: WARNING - tranzaksiya yo'q
  Note over A: idle in transaction, ochiq qoldi

Nima bo'ldi? BEGIN va INSERT 4248-ulanishga tushdi, ROLLBACK esa 21416 ga. 21416 da tranzaksiya yo'q edi — ROLLBACK hech narsa qilmadi. 4248 da esa tranzaksiya ochiq qoldi: INSERT na saqlangan, na bekor qilingan. Bu ulanish hovuzga ochiq tranzaksiya bilan qaytdi — holati idle in transaction ("tranzaksiya ichida bo'sh turibdi"). Faqat pool.end() ulanishni yopganda Postgres ochiq tranzaksiyani bekor qildi (oxirgi qator: 0).

Kanonning o'zida esa skript ikkinchi marta ishga tushirilganda (2026-10-24, 19:09) uchala so'rov bitta ulanishga tushdi (1329 / 1329 / 1329) va idle in transaction 0 chiqdi. Ya'ni tuzoq har safar ham ko'rinmaydi — qaysi ulanish tushishi tasodifga bog'liq.

Bu nimaga olib keladi:

  • Ulanishni keyingi pool.query oladi va bilmagan holda o'sha tranzaksiya ichida ishlaydi. Uning o'zgarishlari ham hech qachon COMMIT bo'lmaydi.
  • Ochiq tranzaksiya qatorlarga qo'ygan qulflarni ushlab turadi (Qulflar) — boshqa so'rovlar ularni kutib qotadi.
  • Postgres eski qator versiyalarini tozalay olmaydi (MVCC) — jadval shishadi.

ROLLBACK'dan keyin: 0 aldamchi: INSERT "yo'qolgandek" ko'rinadi, aslida u 4248 dagi ochiq tranzaksiyada kutib turibdi. Kichik sinovda hammasi ishlayotgandek ko'rinishi — bu tuzoqni ayniqsa xavfli qiladi: u faqat yuklama ostida, parallel so'rovlarda chiqadi.

4.2 To'g'ri naqsh: tranzaksiyada(fn)

Tranzaksiya uchun hovuzdan bitta client olamiz va hamma so'rovni shu client bilan bajaramiz. Kanon yordamchisi:

js
async function tranzaksiyada(fn) {
  const client = await pool.connect();   // bitta ulanish
  try {
    await client.query("BEGIN");
    const natija = await fn(client);       // hammasi shu client
    await client.query("COMMIT");
    return natija;
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;                               // xatoni yashirmaymiz
  } finally {
    client.release();                      // doim hovuzga qaytadi
  }
}

To'rt qism, har biri muhim:

  • pool.connect() — ulanishni qo'lda olish; fn ga aynan shu client beriladi.
  • catch — xato bo'lsa ROLLBACK, keyin xatoni qayta tashlash: chaqiruvchi (API handler) bilsin va 400/409 qaytarsin.
  • finally — muvaffaqiyatda ham, xatoda ham release() (Connection pooling dagi pool leak darsi).
  • fn ichida faqat client.query — pool.query emas! pool.query boshqa ulanishga tushadi va tranzaksiyadan tashqarida bajariladi.

Ishlatish — vazifa va ikki izoh, ikkinchisi bo'sh:

js
try {
  await tranzaksiyada(async (c) => {
    const { rows: [v] } = await c.query(
      "INSERT INTO vazifalar (egasi_id, matn) " +
        "VALUES (8, 'Tranzaksiya sinovi') RETURNING id",
    );
    await c.query(
      "INSERT INTO izohlar (vazifa_id, muallif_id, matn) " +
        "VALUES ($1, 8, 'birinchi')",
      [v.id],
    );
    await c.query(
      "INSERT INTO izohlar (vazifa_id, muallif_id, matn) " +
        "VALUES ($1, 8, '')",
      [v.id],
    );
  });
} catch (e) {
  console.log("tranzaksiya bekor:", e.code, e.constraint);
}

Kanon chiqishi:

text
tranzaksiya bekor: 23514 izohlar_matn_check
'Tranzaksiya sinovi' bazada: 0 | pool: { total: 1, idle: 1 }

Vazifa va birinchi izoh ham saqlanmadi — hammasi yoki hech narsa. Hovuz toza: bitta ulanish, u ham bo'sh — ochiq tranzaksiya yo'q.

Maslahat: Agar ROLLBACK ning o'zi xato bersa (masalan ulanish uzilgan), bunday client'ni hovuzga qaytarmaslik kerak — client.release(xato) ga xatoni bersangiz, pg uni hovuzdan chiqarib yopadi (node-postgres hujjati, Pool API). Kanon buni sinadi: tranzaksiya ichida server jarayoni o'ldirildi (pg_terminate_backend) va release(xato) dan keyin hovuzda buzilgan ulanish qolmadi: uzilgan ulanish: terminating connection due to administrator command | pool: { total: 1, idle: 1 }, keyingi so'rov sog' ulanishda { ok: 1 } qaytardi. Loyihadagi yakuniy shakli — pastdagi «Node qadami».

4.3 SAVEPOINT Node'da

Import misoli: to'rtta matn, ikkitasi yomon (bo'sh va 101 belgi — matn 100 belgigacha). Har qator oldidan SAVEPOINT, xatoda ROLLBACK TO SAVEPOINT:

js
const natija = await tranzaksiyada(async (c) => {
  const qabul = [], rad = [];
  for (const matn of ["Import 1 #import", "", "Import 2 #import",
    "x".repeat(101)]) {
    await c.query("SAVEPOINT qator");
    try {
      await c.query(
        "INSERT INTO vazifalar (egasi_id, matn) VALUES (8, $1)",
        [matn],
      );
      await c.query("RELEASE SAVEPOINT qator");
      qabul.push(matn);
    } catch (e) {
      await c.query("ROLLBACK TO SAVEPOINT qator");
      const bosh = JSON.stringify(matn.slice(0, 10));
      rad.push(`${bosh}: ${e.constraint}`);
    }
  }
  return { qabul, rad };
});

Kanon chiqishi:

text
SAVEPOINT bilan import: {
  qabul: [ 'Import 1 #import', 'Import 2 #import' ],
  rad: [ '"": vazifalar_matn_check', '"xxxxxxxxxx": vazifalar_matn_check' ]
}

RELEASE SAVEPOINT — muvaffaqiyatda saqlash nuqtasini "unutish" (ortiqcha nuqtalar to'planmasin). SAVEPOINT'siz nima bo'lishini kanon alohida ko'rsatdi:

text
1-xato: 23514
keyingi so'rov: 25P02 current transaction is aborted, commands ignored until end of transaction block

25P02 — in_failed_sql_transaction: psql'dagi o'sha xabar, endi Node'da. Bu xatoni ko'rsangiz — undan oldin boshqa xato bo'lgan va kimdir uni ushlab, tranzaksiyani tuzatmay davom etgan. Haqiqiy sababni log'dan oldingi xatoda qidiring.

Tekshirib ko'ring: tranzaksiyada ichida fn vazifani qo'shib, keyin pool.query("SELECT count(*) FROM vazifalar WHERE egasi_id = 8") bilan sanadi. Yangi vazifa sanoqqa kiradimi?

Javob

Yo'q. pool.query boshqa ulanishni oldi — u tranzaksiyadan tashqarida va COMMIT bo'lmagan o'zgarishni ko'rmaydi. Ustiga, hovuzda bo'sh ulanish bo'lmasa (max: 1), pool.query fn tugashini kutadi, fn esa pool.query ni kutadi — abadiy kutish (deadlock). Qoida: tranzaksiya ichida faqat berilgan client.

5. Tranzaksiya qoidalari

  • Qisqa bo'lsin. Tranzaksiya ichida HTTP so'rov, email yuborish, foydalanuvchini kutish — yo'q. Ular soniyalab davom etadi, tranzaksiya esa qulflarni ushlab turadi. Avval tashqi ishni qiling, keyin qisqa tranzaksiyada yozing (yoki aksincha — yozib, keyin navbat orqali yuboring, SKIP LOCKED navbat).
  • Tashqi yon ta'sirlar orqaga qaytmaydi. Tranzaksiya ichida email yuborilsa va keyin ROLLBACK bo'lsa — email baribir ketdi. Postgres faqat o'z ma'lumotini qaytaradi. (Xabar yuborishning bazaga mos usuli — LISTEN/NOTIFY: u faqat COMMIT'da yuboriladi.)
  • "Kim" ni SET LOCAL bilan. vazifa_tarixi trigger'i foydalanuvchini ilova.foydalanuvchi_id dan oladi — uni tranzaksiya boshida SET LOCAL yoki set_config(…, true) bilan qo'ying (PgBouncer darsi).
  • Osilib qolganlarni kuzating. SELECT pid, state, xact_start FROM pg_stat_activity WHERE state = 'idle in transaction' — tuzoq belgisi. Postgres sozlamasi idle_in_transaction_session_timeout (sukut 0 — o'chiq) shunday sessiyalarni ma'lum vaqtdan keyin uzadi.

Parallel tranzaksiyalar bir-birini qanday ko'rishi (izolyatsiya darajalari) — Read phenomena va izolyatsiya darajalari darsida. Bu darsdagi hamma misol sukut daraja — READ COMMITTED da.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
ROLLBACK qildim, lekin keyin g'alati xatolar pool.query("BEGIN") — so'rovlar turli ulanishda tranzaksiyada(fn), bitta client
25P02 current transaction is aborted oldingi xato ushlanib, tranzaksiya tuzatilmagan ROLLBACK yoki ROLLBACK TO SAVEPOINT
COMMIT → ROLLBACK tranzaksiya oldinroq buzilgan xatoni catch da ushlang
Qisman saqlangan ma'lumot bog'liq so'rovlar alohida autocommit'da bitta tranzaksiyaga oling
pg_stat_activity da idle in transaction COMMIT/ROLLBACK va release yo'q yo'l bor try/catch/finally
ID'lar orasida bo'shliq ROLLBACK'da sequence qaytmaydi normal, tayanmang

7. Mashqlar

Mashqlar kurs/mashqlar/26/48-tranzaksiya/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Tranzaksiyani boshlash —
  • Xatodan keyin COMMIT yozilsa, Postgres javobi —
  • Buzilgan tranzaksiyadagi so'rov xatosi kodi —
  • Tranzaksiya uchun hovuzdan ulanish olish — pool.()
  • Saqlash nuqtasiga qaytish — ROLLBACK TO qator
Yechim
  • BEGIN (yoki START TRANSACTION).
  • ROLLBACK — buzilgan tranzaksiya saqlanmaydi.
  • 25P02 — in_failed_sql_transaction; asl xato undan oldin.
  • connect — keyin try/finally da release().
  • SAVEPOINT — tranzaksiya davom etadi.

2-mashq (o'rta): kodni tuzating

Quyidagi kodda uchta xato bor. Toping va har birini bir gap bilan tushuntiring:

js
async function savatdanTikla(id) {
  const client = await pool.connect();
  await client.query("BEGIN");
  await client.query(
    "UPDATE vazifalar SET ochirilgan_vaqt = NULL WHERE id = $1",
    [id],
  );
  await pool.query(
    "INSERT INTO izohlar (vazifa_id, matn) VALUES ($1, 'tiklandi')",
    [id],
  );
  await client.query("COMMIT");
}
Yechim
  1. try/catch yo'q — xato bo'lsa ROLLBACK chaqirilmaydi va tranzaksiya ochiq qoladi.
  2. release() yo'q — muvaffaqiyatda ham ulanish hovuzga qaytmaydi (pool leak); xatoda esa ochiq tranzaksiya bilan "yo'qoladi".
  3. pool.query tranzaksiya ichida — izoh boshqa ulanishda, tranzaksiyadan tashqarida yoziladi: UPDATE bekor bo'lsa ham izoh qoladi (va max: 1 da abadiy kutish).

To'g'risi — tranzaksiyada(async (c) => { … }) ichida ikkala so'rov ham c.query.

3-mashq (qiyin): vazifa izohi bilan

tranzaksiyada yordamchisi asosida vazifaIzohBilan(egasi, matn, izoh) funksiyasini yozing: vazifani qo'shadi, RETURNING id bilan uning id'siga izoh qo'shadi va id'ni qaytaradi. Shahnoza (8) uchun sinang: (a) to'g'ri izoh — vazifalar soni va yangi vazifaning izohlar_soni ni chiqaring (trigger uni yangilaydi — Denormalizatsiya); (b) bo'sh izoh (" ") — xato kodi va vazifalar sonini chiqaring. Oxirida (a) dagi vazifani o'chiring.

Yechim
js
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});

async function tranzaksiyada(fn) {
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    const natija = await fn(client);
    await client.query("COMMIT");
    return natija;
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  } finally {
    client.release();
  }
}

function vazifaIzohBilan(egasi, matn, izoh) {
  return tranzaksiyada(async (c) => {
    const { rows } = await c.query(
      "INSERT INTO vazifalar (egasi_id, matn) VALUES ($1, $2) " +
        "RETURNING id",
      [egasi, matn],
    );
    await c.query(
      "INSERT INTO izohlar (vazifa_id, muallif_id, matn) " +
        "VALUES ($1, $2, $3)",
      [rows[0].id, egasi, izoh],
    );
    return rows[0].id;
  });
}

const soni = async () => {
  const r = await pool.query(
    "SELECT count(*)::int AS n FROM vazifalar WHERE egasi_id = 8",
  );
  return r.rows[0].n;
};

console.log("boshida:", await soni());
const id = await vazifaIzohBilan(
  8, "Kutubxonaga kitob qaytarish", "ertaga",
);
const { rows } = await pool.query(
  "SELECT izohlar_soni FROM vazifalar WHERE id = $1",
  [id],
);
console.log(
  "a) qo'shildi:", await soni(),
  "| izohlar_soni:", rows[0].izohlar_soni,
);
try {
  await vazifaIzohBilan(8, "Sport zaliga yozilish", "   ");
} catch (e) {
  console.log("b) bekor:", e.code, e.constraint, "|", await soni());
}
await pool.query("DELETE FROM vazifalar WHERE id = $1", [id]);
console.log(
  "tozalandi:", await soni(),
  "| pool:", pool.totalCount, pool.idleCount,
);
await pool.end();

Biz ishga tushirdik (holat 40, PostgreSQL 17.6, Node 24.21.0, pg 8.23.1):

text
boshida: 0
a) qo'shildi: 1 | izohlar_soni: 1
b) bekor: 23514 izohlar_matn_check | 1
tozalandi: 0 | pool: 1 1

(a) — ikki INSERT bitta tranzaksiyada; izohlar_soni = 1 — counter cache trigger'i ham o'sha tranzaksiya ichida ishladi. (b) — " " (faqat bo'shliq) btrim dan keyin bo'sh, CHECK buzildi; vazifa ham saqlanmadi, son 1 da qoldi. Oxirida hovuzda 1 ta bo'sh ulanish — hamma release() ishladi.

8. Node qadami

Endi shu naqsh vazifalar-api ga kiradi. Postgres ombori src/vazifalar/pg-ombor.ts (node-postgres asoslari darsida boshlangan PgOmbori klassi) tranzaksiyani xususiy metod #tranzaksiyada bilan ochadi. Uning birinchi varianti (holat 44) yuqoridagi tranzaksiyada bilan bir xil edi. Bu qadamda (holat 48) bitta o'zgarish: ROLLBACK ning o'zi yiqilsa, ulanish hovuzga qaytmaydi.

ts
async #tranzaksiyada<T>(
  fn: (c: PoolClient) => Promise<T>,
): Promise<T> {
  const c = await this.#pool.connect();
  let buzilgan: Error | undefined;
  try {
    await c.query("BEGIN");
    const natija = await fn(c);
    await c.query("COMMIT");
    return natija;
  } catch (x) {
    try {
      await c.query("ROLLBACK");
    } catch (r) {
      buzilgan = r instanceof Error ? r : new Error(String(r));
    }
    throw takrorXatosi(x);   // 23505 → 409 "takror-matn"
  } finally {
    c.release(buzilgan);     // undefined — hovuzga; xato — yopiladi
  }
}

Ikki yangi narsa:

  • release(buzilgan) — buzilgan bo'sh bo'lsa, ulanish odatdagidek hovuzga qaytadi; xato bo'lsa, pg uni yopadi («Maslahat» qutisidagi kanon sinovi).
  • takrorXatosi(x) — Postgres xatosini API xatosiga o'giradi: takror matn (23505, cheklov vazifalar_egasi_id_matn_key) → KonfliktXatosi → HTTP 409. Boshqa xatolar o'zgarmay o'tadi. (Bu holat 48 dagi sxema. Keyinroq noyoblik exclusion constraint'ga aylanadi va kod 23P01 bo'ladi — u ham 409; sababi Deadlock va serialization failure darsining «Node qadami» da.)

Bu metodni eng ko'p ishlatadigan joy — PUT /v1/vazifalar (almashtirHammasi): butun ro'yxat yangisi bilan almashadi. Egasi qatorini qulflash, ro'yxatda yo'qlarini Savatga tushirish, mavjudlarini yangilash, yangilarini qo'shish va ro'yxat versiyasini oshirish — hammasi bitta tranzaksiyada. Ikki so'rovli unnest qismi — COPY, batch va cursor darsida.

Kafolatni test bilan tekshiramiz (test/pg-ombor.test.ts, holat 48 da qo'shildi; qisqartirildi):

ts
test("almashtirHammasi atomik (#48) …", async () => {
  await tozala(pool);
  const o = new PgOmbori(pool, 1);
  await o.yarat({ matn: "Non" });
  await o.yarat({ matn: "Sut" });
  // hammasi — filtrsiz ro'yxat so'rovi (limit 10)
  const oldin = await o.royxat(hammasi);
  await assert.rejects(
    o.almashtirHammasi([
      { matn: "Choy", bajarildi: false },
      { matn: "choy", bajarildi: true },   // registrsiz takror
    ]),
    (x) => x instanceof KonfliktXatosi);
  const keyin = await o.royxat(hammasi);
  const matnlar = keyin.vazifalar.map((v) => v.matn);
  assert.deepEqual(matnlar, ["Non", "Sut"]);
  assert.equal(keyin.versiya, oldin.versiya);
});

"Choy" va "choy" — bir xil vazifa (noyoblik registrga befarq). Ikkinchisi 23505 beradi — va butun PUT bekor bo'ladi: eski "Non", "Sut" joyida, ro'yxat versiyasi o'zgarmagan, yarim yozilgan qator yo'q. Ikkinchi yangi test 5 ta yiqilgan tranzaksiyadan keyin hovuzni tekshiradi: kutayotgan yo'q, hamma ulanish bo'sh, pg_stat_activity da idle in transaction — 0.

Kanon tekshiruvi (npm test, PostgreSQL 18.6, Node 24.21.0), oxirgi qatorlar:

text
ℹ tests 81
ℹ pass 81
ℹ fail 0
ℹ skipped 0

81 testdan 14 tasi Postgres'ga xos (holat 44 da 79/79 edi; +2 shu qadamniki). Bazasiz kompyuterda Postgres testlari SKIP bo'ladi, qolganlari ishlaydi.

Holat 48 uch darsning ishini yakunlaydi (COPY, batch va cursor, LISTEN/NOTIFY va shu dars) — commit shu yerda:

bash
git add src/vazifalar/pg-ombor.ts test/pg-ombor.test.ts
git commit -m "perf: PUT /v1/vazifalar — unnest bilan ikki so'rov" \
  -m "Tranzaksiya yordamchisi ROLLBACK xatosida ulanishni yopadi."

Keyinroq bu metodga yana bitta himoya qo'shiladi: tranzaksiya paytida ulanish uzilsa, olingan client "error" hodisasini chiqaradi va u pool.on("error") ga bormaydi — tinglovchisiz Node jarayoni yiqiladi (kanon t48 ni qayta ishga tushirganda topildi). Tuzatish — c.on("error", …) tinglovchisi; u Keyset pagination darsidagi yakuniy holatda.

9. Real ishda

  • Har jiddiy backend amali tranzaksiya: buyurtma + to'lov yozuvi + ombor qoldig'i; ro'yxatdan o'tish + profil + boshlang'ich sozlamalar.
  • ORM'larda xuddi shu naqsh boshqa nom bilan: Prisma $transaction(async (tx) => …), Drizzle db.transaction(…), postgres.js sql.begin (postgres.js) — hammasi ichkarida "bitta ulanish + BEGIN/COMMIT/ROLLBACK + release" qiladi. Ichkarida tashqi db/prisma ni ishlatish — pool.query tuzog'ining aynan o'zi.
  • Kod review savoli: "bu ikki so'rov orasida xato bo'lsa, baza qanday holatda qoladi?"
  • Intervyuda: "Tranzaksiya nima va ACID'ning qaysi harfi?", "SAVEPOINT nima uchun?", "Node'da tranzaksiyani qanday to'g'ri yozasiz?"

Xulosa

  • Tranzaksiya — BEGIN … COMMIT: yo hammasi, yo hech biri; xatodan keyin tranzaksiya buzilgan, COMMIT → ROLLBACK.
  • Autocommit — BEGIN siz har so'rov o'z tranzaksiyasi (kanon: ketma-ket txid 61520, 61521).
  • SAVEPOINT / ROLLBACK TO SAVEPOINT — xatodan keyin tranzaksiyani tuzatib, davom etish; RELEASE SAVEPOINT — muvaffaqiyatda.
  • Node: tranzaksiya — bitta ulanishda; pool.query("BEGIN") — bizning sinovda ROLLBACK boshqa ulanishga tushdi, birinchisi idle in transaction qoldi (har safar emas — shuning uchun xavfli).
  • Naqsh: pool.connect() → BEGIN → fn(client) → COMMIT; catch → ROLLBACK + qayta tashlash; finally → release() (ROLLBACK yiqilsa — release(xato)); ichkarida faqat client.
  • Loyihada: PgOmbori ning #tranzaksiyada metodi — PUT butun ro'yxatni atomik almashtiradi; test buni isbotlaydi.

Keyingi dars: Read phenomena va izolyatsiya darajalari — ikki tranzaksiya bir vaqtda ishlasa: kanonda bir hisob 130 o'rniga 120 bo'lib qoldi va bu xato hech qanday xabarsiz yuz berdi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Tranzaksiya asoslari: BEGIN, COMMIT, ROLLBACK, SAVEPOINT va Node'da to'g'ri naqsh — IlmHamroh