Mundarija (21)
- Bu darsda
- 1. Nega bu kerak?
- 2. SQL'da tranzaksiya
- 2.1 BEGIN, COMMIT va xato
- 2.2 ROLLBACK — qo'lda bekor qilish
- 2.3 Autocommit
- 3. SAVEPOINT: qisman orqaga qaytish
- 4. Node'da tranzaksiya
- 4.1 Tuzoq: pool.query("BEGIN")
- 4.2 To'g'ri naqsh: tranzaksiyada(fn)
- 4.3 SAVEPOINT Node'da
- 5. Tranzaksiya qoidalari
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): kodni tuzating
- 3-mashq (qiyin): vazifa izohi bilan
- 8. Node qadami
- 9. Real ishda
- Xulosa
- Manbalar
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'lsaROLLBACKbilan 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/catchdaROLLBACK→finallydarelease().SAVEPOINT— tranzaksiya ichida qisman orqaga qaytish.
Bu darsda
BEGIN,COMMIT,ROLLBACKni psql'da ishlatasiz va xatodan keyin tranzaksiya nima bo'lishini bilasiz.- Autocommit nimaligini va har so'rov o'z tranzaksiyasi ekanini tushuntirasiz.
SAVEPOINTbilan importda yomon qatorlarni o'tkazib, qolganini saqlaysiz.pool.query("BEGIN")tuzog'ini taniysiz vatranzaksiyada(fn)yordamchisini yozasiz.25P02xatosi 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):
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;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:
BEGIN— tranzaksiya boshlandi.- Vazifa qo'shildi,
id= 49. (currval('vazifalar_id_seq')— shu sessiyada oxirgi berilganid.) - Izoh — bo'sh matn, CHECK buzildi (CREATE TABLE va cheklovlar).
- Keyingi
SELECT— bajarilmadi: "joriy tranzaksiya buzilgan, tranzaksiya bloki tugaguncha buyruqlar e'tiborsiz qoldiriladi". Xatodan keyin tranzaksiya buzilgan holatda: undagi hech narsa endi saqlanmaydi. COMMITyozdik — PostgresROLLBACKdeb javob berdi! Buzilgan tranzaksiyani COMMIT qilib bo'lmaydi; Postgres jim orqaga qaytardi.- 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:
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;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:
txid'lar (autocommit): 61520, 61521Ikki 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
COMMITyozdingiz. 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:
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;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:
SAVEPOINTbepul 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:
// ❌ 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:
boshida: 0 | BEGIN / INSERT / ROLLBACK (pid): 4248 / 4248 / 21416 → ROLLBACK'dan keyin: 0
idle in transaction ulanishlar: 1
pool.end() dan keyin bazada: 0Pid'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 qoldiNima 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.queryoladi 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:
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;fnga aynan shuclientberiladi.catch— xato bo'lsaROLLBACK, keyin xatoni qayta tashlash: chaqiruvchi (API handler) bilsin va 400/409 qaytarsin.finally— muvaffaqiyatda ham, xatoda hamrelease()(Connection pooling dagi pool leak darsi).fnichida faqatclient.query—pool.queryemas!pool.queryboshqa ulanishga tushadi va tranzaksiyadan tashqarida bajariladi.
Ishlatish — vazifa va ikki izoh, ikkinchisi bo'sh:
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:
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
ROLLBACKning o'zi xato bersa (masalan ulanish uzilgan), bunday client'ni hovuzga qaytarmaslik kerak —client.release(xato)ga xatoni bersangiz,pguni hovuzdan chiqarib yopadi (node-postgres hujjati, Pool API). Kanon buni sinadi: tranzaksiya ichida server jarayoni o'ldirildi (pg_terminate_backend) varelease(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:
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:
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:
1-xato: 23514
keyingi so'rov: 25P02 current transaction is aborted, commands ignored until end of transaction block25P02 — 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:
tranzaksiyadaichidafnvazifani qo'shib, keyinpool.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
ROLLBACKbo'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 LOCALbilan.vazifa_tarixitrigger'i foydalanuvchiniilova.foydalanuvchi_iddan oladi — uni tranzaksiya boshidaSET LOCALyokiset_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 sozlamasiidle_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
COMMITyozilsa, 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/finallydarelease(). - SAVEPOINT — tranzaksiya davom etadi.
2-mashq (o'rta): kodni tuzating
Quyidagi kodda uchta xato bor. Toping va har birini bir gap bilan tushuntiring:
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
try/catchyo'q — xato bo'lsaROLLBACKchaqirilmaydi va tranzaksiya ochiq qoladi.release()yo'q — muvaffaqiyatda ham ulanish hovuzga qaytmaydi (pool leak); xatoda esa ochiq tranzaksiya bilan "yo'qoladi".pool.querytranzaksiya ichida — izoh boshqa ulanishda, tranzaksiyadan tashqarida yoziladi: UPDATE bekor bo'lsa ham izoh qoladi (vamax: 1da 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
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):
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.
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)—buzilganbo'sh bo'lsa, ulanish odatdagidek hovuzga qaytadi; xato bo'lsa,pguni yopadi («Maslahat» qutisidagi kanon sinovi).takrorXatosi(x)— Postgres xatosini API xatosiga o'giradi: takror matn (23505, cheklovvazifalar_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 kod23P01bo'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):
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:
ℹ tests 81
ℹ pass 81
ℹ fail 0
ℹ skipped 081 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:
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) => …), Drizzledb.transaction(…), postgres.jssql.begin(postgres.js) — hammasi ichkarida "bitta ulanish + BEGIN/COMMIT/ROLLBACK + release" qiladi. Ichkarida tashqidb/prismani ishlatish —pool.querytuzog'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 —
BEGINsiz 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, birinchisiidle in transactionqoldi (har safar emas — shuning uchun xavfli). - Naqsh:
pool.connect()→BEGIN→fn(client)→COMMIT;catch→ROLLBACK+ qayta tashlash;finally→release()(ROLLBACK yiqilsa —release(xato)); ichkarida faqatclient. - Loyihada:
PgOmborining#tranzaksiyadametodi — 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
- PostgreSQL 18 hujjati — Transactions (tutorial),
BEGIN,COMMIT,ROLLBACK,SAVEPOINT: https://www.postgresql.org/docs/18/tutorial-transactions.html, https://www.postgresql.org/docs/18/sql-savepoint.html - PostgreSQL 18 hujjati —
idle_in_transaction_session_timeout: https://www.postgresql.org/docs/18/runtime-config-client.html - node-postgres — Transactions (bitta client bilan naqsh,
pool.queryogohlantirishi): https://node-postgres.com/features/transactions - Kanon:
tajriba/t48-tranzaksiya.mjs,natijalar/tajriba/t48-tranzaksiya.txt(PostgreSQL 18.6, Node 24.21.0, pg 8.23.1, 2026-10-24);vazifalar-apiholat 48 —pg-ombor.ts,pg-ombor.test.ts,natijalar/tekshiruv-48.txt. psql misollari,pool.query("BEGIN")tuzog'i (5 urinish) va 3-mashq — biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, pg 8.23.1).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!