IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis25/65-dars18 daqiqa
Mundarija (26)

Prisma tranzaksiyalar: nested write, $transaction, isolationLevel va increment

Qisqacha: Bog'liq yozuvlarni bitta chaqiruvda yaratish — nested write (izohlar: { create: [...] }); u avtomatik tranzaksiyada bajariladi. Bir nechta mustaqil amal uchun — $transaction([...]) (massiv) yoki $transaction(async (tx) => …) (interaktiv: oraliq natijaga qarab qaror). Interaktiv tranzaksiya qisqa bo'lsin: timeout (sukut 5 s) o'tsa — P2028. Biz o'lchadik: 20 parallel "o'qib +1 yozish" — 1 (19 tasi yo'qoldi), increment: 1 — 20. "Tekshir va yoz" naqshida qulfsiz 10 dan 10 tasi limitdan o'tdi, FOR UPDATE bilan — aynan 5.

Bu darsda

  • create, connect, connectOrCreate bilan ichma-ich yozasiz va Prisma yuborgan SQL'ni o'qiysiz.
  • $transaction ning ikki shaklini farqlaysiz va to'g'risini tanlaysiz.
  • timeout, maxWait va isolationLevel sozlamalarini, P2028 va P2034 xatolarini tushunasiz.
  • Atomik increment bilan lost update'ni yo'qotasiz.
  • "Tekshir, keyin yoz" naqshini qulf bilan poygaga chidamli qilasiz.

Oldin bilishingiz kerak: Prisma: filtrlash va agregatlar, Tranzaksiya asoslari, Izolyatsiya darajalari, Qulflar, Deadlock va serialization failure.

1. Nega bu kerak?

Dilnoza mehmonlarga tayyorgarlik vazifasini yozmoqda va darhol ikki izoh qo'shmoqchi: "Non va meva", "Choy ham". 26-qismda buni uch INSERT bilan qilgan bo'lardik — va ular bitta tranzaksiyada bo'lishi kerak edi: ikkinchi izoh xato bersa, yarim-yorti vazifa qolmasin (Tranzaksiya asoslari).

Prisma'da ikki yo'l bor. Birinchisi — bitta create ichida bolalarni ham yozish. Ikkinchisi — bir nechta so'rovni $transaction bilan o'rash. Qaysi biri qachon, va tranzaksiya ichida nima xato ketishi mumkin — shu dars.

O'xshatish: bankomatdan pul yechish. "Hisobdan ayir" va "pulni ber" — ikkalasi ham bo'ladi yoki ikkalasi ham bo'lmaydi. Agar bankomat oraliqda o'chib qolsa, hisobingiz kamayib, pul chiqmay qolishi mumkin emas.

Chiqishlarni biz ishga tushirdik: PostgreSQL 17.6, holat 20 namunasi nusxasi, Prisma 7.10.1, Node 24.21.0. Prisma log'i BEGIN ni ko'rsatmaydi, faqat COMMIT/ROLLBACK ni — tranzaksiya chegarasini shu bo'yicha o'qing.

2. Nested write: bog'liq yozuvlar bitta chaqiruvda

2.1 create ichida create

ts
await db.vazifa.create({
  data: {
    egasiId: 2n,
    matn: "Mehmonlarga dasturxon",
    izohlar: {
      create: [
        { matn: "Non va meva", muallifId: 2n },
        { matn: "Choy ham", muallifId: 3n },
      ],
    },
  },
  select: {
    id: true, izohlarSoni: true, izohlar: { select: { matn: true } },
  },
});
text
{"id":"49n","izohlarSoni":2,"izohlar":[{"matn":"Non va meva"},{"matn":"Choy ham"}]}
SQL: INSERT INTO "vazifalar" (…) VALUES (…) RETURNING "vazifalar"."id"
SQL: INSERT INTO "izohlar" ("vazifa_id","muallif_id","matn","yaratilgan_vaqt") VALUES ($1,$2,$3,$4), ($5,$6,$7,$8)
SQL: SELECT "vazifalar"."id", "vazifalar"."izohlar_soni" FROM "vazifalar" WHERE "vazifalar"."id" = $1 LIMIT $2 OFFSET $3
SQL: SELECT "izohlar"."id", "izohlar"."matn", "izohlar"."vazifa_id" FROM "izohlar" WHERE "izohlar"."vazifa_id" = $1 OFFSET $2
SQL: COMMIT

Prisma ketma-ketlikni o'zi tuzdi: avval vazifa (yangi id kerak), keyin ikkala izoh bitta INSERT da (vazifa_id ni o'zi qo'ydi), keyin natijani qayta o'qidi. Oxiridagi COMMIT — hammasi bitta tranzaksiya.

izohlarSoni: 2 ga qarang: uni bazadagi AFTER INSERT trigger (counter cache) oshirgan. Prisma qatorni yozgandan keyin qayta o'qigani uchun trigger qiymati ko'rindi.

2.2 Xato bo'lsa — hammasi bekor

Ikkinchi izohning matni bo'sh — bazada izohlar_matn_check cheklovi buni taqiqlaydi:

text
PrismaClientKnownRequestError P2039 (asl 23514): Database error. Code: `23514`. Message: `new row for relation "izohlar" violates check constraint "izohlar_matn_check"`
vazifalar soni o'zgardimi: false
SQL: INSERT INTO "vazifalar" (…) RETURNING "vazifalar"."id"
SQL: ROLLBACK

Vazifa yozilgan edi, lekin izoh yiqilganda ROLLBACK uni ham qaytardi. Nested write — atomik.

2.3 connect — mavjud yozuvga ulash

Mavjud vazifaga izoh. FK maydonini (vazifaId: 16n) to'g'ridan yozsa ham bo'ladi; relation orqali — connect:

ts
await db.izoh.create({
  data: {
    matn: "Bajarildi",
    vazifa: { connect: { id: 16n } },
    muallif: { connect: { id: 2n } },
  },
  select: { id: true, vazifaId: true },
});
text
{"id":"18n","vazifaId":"16n"}
SQL: SELECT "foydalanuvchilar"."id" FROM "foydalanuvchilar" WHERE (…) LIMIT $2 OFFSET $3
SQL: SELECT "vazifalar"."id" FROM "vazifalar" WHERE (…) LIMIT $2 OFFSET $3
SQL: INSERT INTO "izohlar" (…) RETURNING "izohlar"."id"

connect ulanadigan yozuv borligini avval tekshiradi (ikki SELECT). Yo'q bo'lsa — FK xatosi emas, Prisma'ning aniq xabari:

text
PrismaClientKnownRequestError P2025: … No 'Vazifa' record (needed to inline the relation on 'Izoh' record(s)) was found for a nested connect on one-to-many relation 'IzohToVazifa'.

Narxi — har connect ga bitta qo'shimcha SELECT. Ko'p yozuvli import'da FK maydonini to'g'ridan yozish arzonroq; xatoni esa baza (FK, P2003) baribir ushlaydi.

2.4 connectOrCreate

"Teg bor bo'lsa — ula, yo'q bo'lsa — yarat". VazifaTegi (ko'prik jadval) yozuvini teg bilan birga yaratamiz:

ts
await db.vazifaTegi.create({
  data: {
    vazifa: { connect: { id: 14n } },
    teg: {
      connectOrCreate: {
        where: { egasiId_nom: { egasiId: 2n, nom } },
        create: { nom, egasi: { connect: { id: 2n } } },
      },
    },
  },
  select: { teg: { select: { id: true, nom: true } } },
});

#bozor (Dilnozada bor) va #xarid (yo'q) bilan:

text
{"teg":{"id":"9n","nom":"#bozor"}}
{"teg":{"id":"35n","nom":"#xarid"}}
SQL: SELECT "teglar"."id" FROM "teglar" WHERE (("teglar"."egasi_id" = $1 AND "teglar"."nom" = $2) …
SQL: SELECT "foydalanuvchilar"."id" FROM "foydalanuvchilar" WHERE (…)
SQL: INSERT INTO "teglar" ("egasi_id","nom") VALUES ($1,$2) RETURNING "teglar"."id"
…

Ikkinchi holatda Prisma tegni topmadi va yaratdi. upsert kabi bu ham SELECT + INSERT — parallel chaqiruvda P2002 bo'lishi mumkin (Prisma Client CRUD, poyga bo'limi).

Diqqat: Loyihamizda teglar bunday yozilmaydi: ularni bazadagi trigger vazifa matnidan (#bozor) o'zi oladi (Triggerlar). Bu misol faqat connectOrCreate ni ko'rsatish uchun; haqiqiy ilovada trigger va ilova bir xil jadvalga yozsa, ular bir-biriga xalal beradi.

Tekshirib ko'ring: Nested write ichidagi uchinchi bola xato bersa, birinchi ikki bola bazada qoladimi?

Javob

Yo'q. Nested write — bitta tranzaksiya: istalgan qadamdagi xato ROLLBACK qiladi, ota yozuv ham, bolalar ham qaytadi. «Xato bo'lsa — hammasi bekor» bo'limida vazifa soni o'zgarmadi.

3. $transaction: bir nechta amal birga

3.1 Massiv shakli

Amallar oldindan ma'lum va bir-biriga bog'liq bo'lmasa — massiv:

ts
const [n, yangi] = await db.$transaction([
  db.vazifa.count({ where: { egasiId: 2n } }),
  db.vazifa.create({
    data: { egasiId: 2n, matn: "Batch vazifa" },
    select: { id: true },
  }),
]);
console.log(n, yangi.id);
text
7 51n
SQL: SELECT COUNT(*) …
SQL: INSERT INTO "vazifalar" (…) RETURNING "vazifalar"."id"
SQL: COMMIT

So'rovlar ketma-ket bitta tranzaksiyada bajariladi, natijalar massivda shu tartibda keladi. Biri xato bersa — hammasi bekor (kanon ikkinchi create da CHECK buzib tekshirgan: birinchisi ham qaytgan).

Cheklov: ikkinchi so'rov birinchisining natijasini ishlata olmaydi — hammasi chaqiruvdan oldin yozilgan. db.vazifa.count(...) ning o'zi so'rovni bajarmaydi — PrismaPromise yasaydi; uni await qilmasdan massivga berasiz.

3.2 Interaktiv shakl

"Faol vazifalari 5 tadan kam bo'lsa qo'sh" — avval o'qish, keyin qaror. Buning uchun interaktiv tranzaksiya: funksiya tx klientini oladi, ichida oddiy await lar:

ts
const qosh = (egasiId: bigint, matn: string) =>
  db.$transaction(async (tx) => {
    const faol = await tx.vazifa.count({
      where: { egasiId, bajarildi: false, ochirilganVaqt: null },
    });
    if (faol >= 5) throw new Error(`faol vazifalar ko'p: ${faol}`);
    return tx.vazifa.create({
      data: { egasiId, matn }, select: { id: true },
    });
  });

Shahnoza (8) va Aziz (1) uchun:

text
qo'shildi 49
Error: faol vazifalar ko'p: 7
SQL: SELECT COUNT(*) … ; INSERT … ; COMMIT
SQL: SELECT COUNT(*) … ; ROLLBACK

Funksiya oddiy qiymat qaytarsa — COMMIT, istisno otsa — ROLLBACK va xato tashqariga chiqadi. Ichkarida faqat tx ishlating: db.vazifa… tranzaksiyadan tashqarida, boshqa ulanishda bajariladi.

Diqqat: bu kod hali poygaga chidamli emas — parallel chaqiruvlar ikkalasi ham "4 ta" ni ko'rib, ikkalasi ham qo'shishi mumkin. Buni «Tekshir va yoz: qulf» bo'limida o'lchaymiz.

sequenceDiagram
  participant K as Kod
  participant P as Prisma
  participant DB as Postgres
  K->>P: $transaction(async tx => …)
  P->>DB: BEGIN (ulanish band)
  K->>P: tx.vazifa.count()
  P->>DB: SELECT COUNT(*)
  K->>P: tx.vazifa.create()
  P->>DB: INSERT … RETURNING
  K-->>P: funksiya tugadi
  P->>DB: COMMIT (ulanish bo'shadi)

Diagrammaga qarang: BEGIN dan COMMIT gacha bitta ulanish shu tranzaksiyaga band. Funksiya ichida tarmoq so'rovi yoki uzoq hisob bo'lsa, ulanish shuncha vaqt hovuzga qaytmaydi.

3.3 timeout va maxWait

Interaktiv tranzaksiyaning ikki sozlamasi (Prisma 7 hujjati bo'yicha sukut: maxWait 2 000 ms, timeout 5 000 ms):

  • timeout — tranzaksiya boshidan oxirigacha eng ko'p vaqt.
  • maxWait — hovuzdan ulanish olishni eng ko'p kutish.

Tranzaksiya ichida 1.5 s "ish" va timeout: 1000:

ts
await db.$transaction(async (tx) => {
  await tx.vazifa.count();
  await kut(1500);
  return tx.vazifa.count();
}, { timeout: 1000 });
text
PrismaClientKnownRequestError P2028: Transaction API error: A query cannot be executed on an expired transaction. The timeout for this transaction was 1000 ms, however 1519 ms passed since the start of the transaction. Consider increasing the interactive transaction timeout or doing less work in the transaction.

Hovuzda 1 ta ulanish (max: 1) va birinchi tranzaksiya uni 800 ms ushlab turibdi; ikkinchisi maxWait: 200:

text
PrismaClientKnownRequestError P2028: Transaction API error: Unable to start a transaction in the given time.

Ikkalasi ham P2028. Xabar ma'nosi: birinchisi — "muddati o'tgan tranzaksiyada so'rov bajarib bo'lmaydi", ikkinchisi — "berilgan vaqtda tranzaksiyani boshlab bo'lmadi". Birinchisini timeout ni oshirib "tuzatish" — odatda xato yo'l: tranzaksiya ichidan sekin ishni (HTTP so'rov, fayl) olib chiqing.

3.4 isolationLevel va P2034

Sukut izolyatsiya — bazaniki (Postgres'da READ COMMITTED). Prisma'da tranzaksiya uchun alohida beriladi:

ts
await db.$transaction(async (tx) => { … }, {
  isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
});

Ikki parallel tranzaksiya: har biri Javohirning (5) bajarilmagan vazifalarini sanaydi, 200 ms kutadi va yangi vazifa qo'shadi:

text
o'tdi
PrismaClientKnownRequestError P2034 (asl 40001): Transaction failed due to a write conflict or a deadlock. Please retry your transaction

SERIALIZABLE poygani aniqladi: ikkinchisi "ketma-ket bajarilganida natija boshqacha bo'lardi" degan sabab bilan bekor qilindi (Izolyatsiya darajalari). Prisma buni P2034 ga aylantiradi; asl kod — 40001. Xabar matniga ko'ra deadlock (40P01) ham shu kod bilan keladi. To'g'ri javob — butun tranzaksiyani qayta urinish (Deadlock va serialization failure).

Diqqat — biz topgan muammo (Prisma 7.10.1, @prisma/adapter-pg 7.10.1): serializatsiya xatosi INSERT paytida emas, COMMIT paytida chiqsa (Postgres log'ida Canceled on identification as a pivot, during commit attempt), u $transaction dan xato bo'lib qaytmadi. Jarayonda ushlanmagan istisno (DriverAdapterError: TransactionWriteConflict) paydo bo'ldi va $transaction va'dasi hech qachon tugamadi. Biz buni "sana va qo'sh" naqshida 6 marta ketma-ket takrorladik — 6/6. Ya'ni Prisma 7.10 bilan SERIALIZABLE'ga tayanib qayta urinish yozish xavfli: jarayon yiqiladi. Kanon loyihasi boshqa yo'l tanlagan — READ COMMITTED + aniq qator qulfi (pastdagi bo'lim). Keyingi Prisma versiyalarida bu tuzatilgan bo'lishi mumkin — yangilaganda shu holatni qayta sinang.

4. Atomik increment

20 ta parallel so'rov foydalanuvchining vazifalarVersiyasi hisoblagichini +1 qiladi. Birinchi usul — o'qib, JavaScript'da qo'shib, yozish:

ts
const f = await db.foydalanuvchi.findUniqueOrThrow({
  where: { id: 6n }, select: { vazifalarVersiyasi: true },
});
await db.foydalanuvchi.update({
  where: { id: 6n },
  data: { vazifalarVersiyasi: f.vazifalarVersiyasi + 1n },
});

Ikkinchi usul — atomik operator:

ts
await db.foydalanuvchi.update({
  where: { id: 6n },
  data: { vazifalarVersiyasi: { increment: 1 } },
});

0 dan boshlab, har biri 20 marta parallel:

text
o'qib-yozish: 1n
increment: 20n
SQL: UPDATE "foydalanuvchilar" SET "vazifalar_versiyasi" = ("foydalanuvchilar"."vazifalar_versiyasi" + $1) WHERE (…) RETURNING …

Nima bo'ldi? Birinchi usulda 20 tadan 19 tasi yo'qoldi. Bu — klassik lost update (Izolyatsiya darajalari): hammasi "0" ni o'qidi va "1" ni yozdi. increment esa SQL'ning o'zida x = x + $1 — baza har yangilashni qatorni qulflab, oxirgi qiymat ustiga bajaradi. Shunday operatorlar: increment, decrement, multiply, divide, set.

Tekshirib ko'ring: O'qib-yozish usulini $transaction(async tx => …) ga o'rasak, 20 chiqadimi?

Javob

READ COMMITTED da — yo'q. Tranzaksiya faqat "hammasi yoki hech narsa"ni beradi; ikki tranzaksiya baribir bir vaqtda "0" ni o'qiydi. Kerak: increment (eng oddiy), yoki o'qishda qatorni qulflash (SELECT … FOR UPDATE), yoki optimistik qulf (WHERE versiya = …).

5. Tekshir va yoz: qulf

«Interaktiv shakl» bo'limidagi "5 tadan ko'p faol vazifa bo'lmasin" qoidasiga qaytamiz. 10 ta parallel qosh — vazifasi yo'q Shahnoza uchun. Ikkinchi variantda tranzaksiya boshida egasi qatorini qulflaymiz:

ts
await tx.$executeRaw`
  SELECT 1 FROM foydalanuvchilar WHERE id = ${egasiId} FOR UPDATE`;

Biz uch marta ishga tushirdik:

text
qulf yo'q: qo'shildi 10, bazada 10
qulf bor: qo'shildi 5, bazada 5
qulf yo'q: qo'shildi 10, bazada 10
qulf bor: qo'shildi 5, bazada 5
qulf yo'q: qo'shildi 9, bazada 9
qulf bor: qo'shildi 5, bazada 5

Nima bo'ldi? Qulfsiz qoida butunlay buzildi: 5 o'rniga 9–10. Hamma tranzaksiya bir vaqtda "0 ta faol" ni ko'rdi. Qulf bilan ikki tranzaksiya qanday navbatga turishini jadvalda ko'ring (Shahnozada 4 ta faol vazifa bor deb faraz qilamiz):

Qadam A tranzaksiya B tranzaksiya Natija
1 FOR UPDATE — egasi qatori qulflandi — A qulfni oldi
2 count → 4 FOR UPDATE — kutadi B to'xtab turibdi
3 create, COMMIT (hali kutyapti) faol: 5
4 — qulfni oldi, count → 5 B yangi sonni ko'rdi
5 — 5 >= LIMIT → "rad", COMMIT limit saqlandi

Prisma'da FOR UPDATE yo'q — u raw SQL bilan ($executeRaw, keyingi darsda batafsil).

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
P2028 expired transaction tranzaksiya ichida sekin ish sekin ishni tashqariga; tranzaksiya qisqa
P2028 Unable to start a transaction hovuz band (maxWait) qisqa tranzaksiyalar, hovuz hajmi
P2034 serializatsiya/deadlock butun tranzaksiyani qayta urinish
hisoblagich kam o'qib-yozish (lost update) increment
limit buzildi "tekshir va yoz" qulfsiz FOR UPDATE yoki noyoblik cheklovi
tranzaksiyadan keyin ham o'zgarish qoldi ichkarida db. ishlatilgan faqat tx.
jarayon yiqildi, TransactionWriteConflict 7.10.1: COMMIT'dagi 40001 SERIALIZABLE o'rniga qulf

7. Mashqlar

Mashqlar kurs/mashqlar/27/25-prisma-tranzaksiya/ da. 2-mashq yechimi Node 24 da tekshirilgan (bazasiz); 3-mashq — yuqoridagi o'lchov.

1-mashq (oson): qaysi biri?

  • Bolalarni ota bilan birga yaratish —
  • Ikkinchi so'rov birinchisining natijasiga bog'liq — $transaction ning shakli
  • Hisoblagichni +1 qilishning atomik yo'li — { : 1 }
  • Serializatsiya xatosining Prisma kodi —
Yechim
  • nested write — create ichida create/connect.
  • interaktiv — async (tx) => …; massiv shaklida natijani ishlatib bo'lmaydi.
  • increment — SQL'da x = x + $1.
  • P2034 — asl kod 40001 yoki 40P01.

2-mashq (o'rta): qayta urinish qarori

qaytaUrin(xato, urinish) funksiyasini yozing. U { qayta: boolean, kutish: number } qaytarsin. Qayta urinish faqat P2034 kodli xatoda (yoki meta.driverAdapterError.cause.originalCode 40001/40P01 bo'lsa) va urinish < 5 da. Kutish — eksponensial: 10 * 2 ** urinish ms (tekshirish oson bo'lsin deb tasodifiysiz).

Yechim
js
const QAYTA_KODLAR = new Set(["40001", "40P01"]);

function qaytaUrin(xato, urinish) {
  const asl = xato?.meta?.driverAdapterError?.cause?.originalCode;
  const nizo = xato?.code === "P2034" || QAYTA_KODLAR.has(asl);
  if (!nizo || urinish >= 5) return { qayta: false, kutish: 0 };
  return { qayta: true, kutish: 10 * 2 ** urinish };
}

const deadlock = {
  code: "P2010",
  meta: { driverAdapterError: { cause: { originalCode: "40P01" } } },
};
console.log(qaytaUrin({ code: "P2034" }, 1));
console.log(qaytaUrin(deadlock, 3));
console.log(qaytaUrin({ code: "P2034" }, 5));
console.log(qaytaUrin({ code: "P2002" }, 1));

Konsolda:

text
{ qayta: true, kutish: 20 }
{ qayta: true, kutish: 80 }
{ qayta: false, kutish: 0 }
{ qayta: false, kutish: 0 }

P2002 (noyoblik) — qayta urinish bilan tuzalmaydi, foydalanuvchiga 409. Chegara 5 — cheksiz sikl bo'lmasin. Haqiqiy kodda kutishga tasodifiy qism qo'shiladi (Math.random()), aks holda ikki nizolashgan tranzaksiya yana bir vaqtda urinadi. Kanondagi PrismaOmbori ham shunday qiladi («Loyiha qadami»).

3-mashq (qiyin): limitni qulf bilan saqlash

«Tekshir va yoz: qulf» bo'limidagi qosh(egasiId, matn, qulf) funksiyasini to'liq yozing va 10 parallel chaqiruv bilan ikkala rejimni solishtiring.

Yechim
ts
const LIMIT = 5;

async function qosh(egasiId: bigint, matn: string, qulf: boolean) {
  return db.$transaction(async (tx) => {
    if (qulf) {
      await tx.$executeRaw`
        SELECT 1 FROM foydalanuvchilar
        WHERE id = ${egasiId} FOR UPDATE`;
    }
    const faol = await tx.vazifa.count({
      where: { egasiId, bajarildi: false, ochirilganVaqt: null },
    });
    if (faol >= LIMIT) return "rad";
    await tx.vazifa.create({ data: { egasiId, matn } });
    return "qo'shildi";
  });
}

for (const qulf of [false, true]) {
  await db.vazifa.deleteMany({ where: { egasiId: 8n } });
  const r = await Promise.all(
    Array.from({ length: 10 }, (_, i) => qosh(8n, `Ish ${i}`, qulf)),
  );
  const faol = await db.vazifa.count({ where: { egasiId: 8n } });
  const n = r.filter((x) => x === "qo'shildi").length;
  const nom = qulf ? "bor" : "yo'q";
  console.log(`qulf ${nom}: qo'shildi ${n}, bazada ${faol}`);
}

Natija — «Tekshir va yoz: qulf» bo'limidagi chiqish (uch marta). Muhim: qulflanadigan qator — "umumiy resurs" (egasi), vazifalar emas: hali yaratilmagan qatorni qulflab bo'lmaydi.

8. Loyiha qadami: PrismaOmbori — yozuvlar tranzaksiyada

Holat 20 davom etadi — hali commit qilmang. Bu darsda hamma yozuv metodlari: yarat, yangila, ochir va ular ishlaydigan #yoz.

8.1 #yoz: qulf va qayta urinish

26-qismdagi PgOmbori har yozuvni bitta qoida bilan qilardi: tranzaksiya, egasi qatori FOR UPDATE, deadlock/serializatsiya bo'lsa — qayta urinish (Race condition'larni hal qilish). Prisma variantida ham aynan shu tartib — ikki ombor bir xil qulf tartibida yozadi:

ts
async #yoz<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {
  for (let urinish = 1; ; urinish++) {
    try {
      return await this.#db.$transaction(async (tx) => {
        await tx.$executeRaw`
          SELECT 1 FROM foydalanuvchilar
          WHERE id = ${this.#egasi} FOR UPDATE`;
        return fn(tx);
      }, { timeout: 10_000, maxWait: 10_000 });
    } catch (x) {
      const kod = sqlHolat(x);
      // P2034 — Prisma'ning "write conflict or deadlock" kodi
      const qayta = kod === "40P01" || kod === "40001"
        || (x instanceof Prisma.PrismaClientKnownRequestError
          && x.code === "P2034");
      if (!qayta || urinish >= QAYTA_URINISH) throw takrorXatosi(x);
      await new Promise((r) =>
        setTimeout(r, Math.random() * 10 * 2 ** urinish));
    }
  }
}

Tx — Prisma.TransactionClient turi. sqlHolat(x) — Postgres'ning asl kodini meta.driverAdapterError.cause.originalCode dan oladi (Prisma Client CRUD, xatolar bo'limi). takrorXatosi 23P01/23505 ni 409 ga aylantiradi. timeout va maxWait 10 s — yuklangan serverda qulf navbati uzoqroq bo'lishi mumkin.

8.2 #versiyaniOshir — increment

Har yozuvda foydalanuvchining ro'yxat versiyasi +1 (API'dagi ro'yxat ETag'i va frontend sinxroni uchun):

ts
async #versiyaniOshir(tx: Tx): Promise<number> {
  const f = await tx.foydalanuvchi.update({
    where: { id: this.#egasi },
    data: { vazifalarVersiyasi: { increment: 1 } },
    select: { vazifalarVersiyasi: true },
  });
  return son(f.vazifalarVersiyasi);
}

8.3 yarat va yangila

ts
async yarat(yangi: YangiVazifa): Promise<Vazifa> {
  return this.#yoz(async (tx) => {
    const bajarildi = yangi.bajarildi ?? false;
    // tartib berilmaydi — BEFORE INSERT trigger (013) qo'yadi
    const q = await tx.vazifa.create({
      data: {
        egasiId: this.#egasi, matn: yangi.matn, bajarildi,
        bajarilganVaqt: bajarildi ? new Date() : null,
      },
      select: TANLOV,
    });
    await this.#versiyaniOshir(tx);
    return vazifa(q);
  });
}

yangila optimistik qulfni (If-Match → shart.versiya) ikki joyda tekshiradi: avval o'qilgan qatorda, keyin updateMany ning where ida (Optimistic concurrency):

ts
const n = await tx.vazifa.updateMany({
  where: { id: joriy.id, versiya: joriy.versiya },
  data: {
    ...(ozgarish.matn === undefined ? {} : { matn: ozgarish.matn }),
    bajarildi,
    bajarilganVaqt: bajarildi
      ? (joriy.bajarilganVaqt ?? new Date()) : null,
  },
});
if (n.count !== 1) return { tur: "konflikt", joriy: vazifa(joriy) };
const q = await tx.vazifa.findUniqueOrThrow({
  where: { id: joriy.id }, select: TANLOV,
});

Nega update emas, updateMany? update ning where iga versiya qo'shsak, mos kelmaganda P2025 otadi; updateMany esa jim count: 0 qaytaradi — "konflikt" holatini istisnosiz ifodalash qulayroq. Narxi — keyin yangilangan qatorni alohida o'qish. Kanon o'lchovi buni ko'rsatadi: 200 ketma-ket yangila — pg 2 842 ms, prisma 4 051 ms (bir yozuvga uch so'rov).

ochir — xuddi shu naqsh: updateMany({ where: { id, egasiId, ochirilganVaqt: null, versiya? }, data: { ochirilganVaqt: new Date() } }), count !== 1 bo'lsa — "topilmadi" yoki "konflikt" ni aniqlash uchun qatorni qayta o'qish.

Tekshiruv: npm run tip hali teglar va almashtirHammasi yo'qligi uchun xato beradi — ular raw SQL talab qiladi, keyingi darsda yoziladi va holat 20 o'sha yerda yakunlanadi.

9. Real ishda

  • To'lov, ombor qoldig'i, chegirma limiti — har "tekshir va yoz" joyi shu darsdagi poyga bilan buziladi. Code review'da birinchi savol: "bu ikki parallel so'rovda nima bo'ladi?"
  • NestJS'da tranzaksiyani bir nechta servisga uzatish — @nestjs-cls/transactional (Tranzaksiyalar NestJS'da, 30-qism).
  • Intervyuda: "$transaction ning ikki shakli farqi?", "Lost update qanday oldini olinadi?", "Interaktiv tranzaksiya ichida HTTP so'rov yuborsa nima bo'ladi?"

Xulosa

  • Nested write (create, connect, connectOrCreate) — bitta tranzaksiya; xato bo'lsa hammasi ROLLBACK.
  • $transaction([...]) — oldindan ma'lum, mustaqil so'rovlar; $transaction(async tx => …) — oraliq natijaga qarab qaror, ichkarida faqat tx.
  • timeout/maxWait → P2028; serializatsiya/deadlock → P2034 — qayta urinish.
  • increment lost update'ni yo'qotadi (1 o'rniga 20); "tekshir va yoz" — FOR UPDATE bilan (10 o'rniga 5).
  • Prisma 7.10.1: COMMIT paytidagi 40001 jarayonni yiqitdi — SERIALIZABLE o'rniga aniq qulf.

Keyingi dars: Prisma: raw SQL va TypedSQL — $queryRaw, Prisma.sql, injection eshigi $queryRawUnsafe va SQL fayldan tipli funksiya.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Prisma tranzaksiyalar: nested write, $transaction, isolationLevel va increment — IlmHamroh