IlmHamroh
JavaScript Full-stack/30-qism. NestJS35/65-dars20 daqiqa
Mundarija (29)

NestJS tranzaksiyalar: @Transactional, TransactionHost va /toplam'dagi yarim yozuv xatosi

Qisqacha: Bir amal ikki jadvalga yozsa (vazifa + izoh), ikkalasi bitta tranzaksiyada bo'lishi kerak: ikkinchisi yiqilsa, birinchisi ham bekor. Nest'da tranzaksiyani xizmatlar orasida uzatishning ikki yo'li bor: tranzaksiya obyektini parametr qilib berish (Prisma $transaction(async (tx) => …), TypeORM dataSource.transaction(…)) yoki CLS orqali — @nestjs-cls/transactional ning @Transactional() dekoratori. Kanon tajribasida tranzaksiyasiz variant yarim yozuv qoldirdi. Xuddi shu tajriba vazifalar-api ning o'zida 25-qismdan beri yashirinib yotgan xatoni topdi: POST /toplam 409 qaytarib, oldingi elementlarni bazada qoldirardi.

Bu darsda

  • Tranzaksiyani xizmatlar orasida uzatishning ikki usulini (parametr va CLS) farqlaysiz.
  • @Transactional() va TransactionHost bilan ikki repository'ni bitta tranzaksiyaga o'raysiz va rollback'ni haqiqiy chiqishda ko'rasiz.
  • "Unit of work" g'oyasini va tranzaksiya ichidagi tipik xatolarni bilasiz.
  • vazifalar-api dagi /toplam xatosini o'zingiz takrorlaysiz, uni test bilan ushlaysiz va tuzatasiz.
  • "Hammasi yoki hech biri" (409) va "har element o'z natijasi" (207) orasidagi tanlovni asoslab bera olasiz.

Oldin bilishingiz kerak: Tranzaksiya asoslari, Prisma: nested write va tranzaksiyalar, Bulk va uzoq davom etuvchi operatsiyalar, So'rov konteksti: nestjs-cls, Drizzle va Kysely bilan NestJS.

To'rt darsda ORM'larni Nest'ga uladik. Endi ularning eng nozik joyi — tranzaksiya. Bu darsda tajriba bilan birga loyihada haqiqiy xato topiladi va tuzatiladi.

1. Nega bu kerak?

26-qismda tranzaksiyaning asosiy va'dasini o'rgandik: ichidagi amallar hammasi bajariladi yoki hech biri (Tranzaksiya asoslari). Bitta SQL so'rovida bu oson. Lekin Nest'da mantiq qatlamlarga bo'lingan: Xizmat → VazifaRepo va IzohRepo. Har repository o'z so'rovini yuboradi. Ular bitta tranzaksiyada bo'lishi uchun bitta ulanish va bitta tranzaksiya obyekti kerak — uni qanday qilib ikkala repository'ga yetkazamiz?

O'xshatish: bankdagi pul o'tkazmasi. Kassir sizning hisobingizdan 100 000 so'm yechadi, keyin do'stingiz hisobiga qo'shadi. Ikkinchi qadamda chiroq o'chsa — pul havoda qolmasligi kerak. Bank ikkala qadamni bitta "chipta" ostida qiladi: chipta yopilmaguncha hech narsa haqiqiy emas. Tranzaksiya — o'sha chipta, savol esa: chiptani ikkinchi kassirga qanday uzatamiz?

2. Avval SQL'da

Muammoni bazaning o'zida ko'ramiz (biz ishga tushirdik, PG 17). Uchta yozuv, uchinchisi takror:

sql
CREATE TABLE toplam_sinov (matn text UNIQUE);
INSERT INTO toplam_sinov VALUES ('A');
INSERT INTO toplam_sinov VALUES ('B');
INSERT INTO toplam_sinov VALUES ('A');
SELECT count(*) AS qoldi FROM toplam_sinov;
text
CREATE TABLE
INSERT 0 1
INSERT 0 1
ERROR:  duplicate key value violates unique constraint "toplam_sinov_matn_key"
DETAIL:  Key (matn)=(A) already exists.
 qoldi
-------
     2
(1 row)

Tranzaksiyasiz har INSERT — o'zi alohida tranzaksiya (autocommit). Uchinchisi yiqildi, lekin A va B qoldi. Endi o'sha amallar BEGIN … ROLLBACK ichida:

sql
TRUNCATE toplam_sinov;
BEGIN;
INSERT INTO toplam_sinov VALUES ('A');
INSERT INTO toplam_sinov VALUES ('B');
INSERT INTO toplam_sinov VALUES ('A');
INSERT INTO toplam_sinov VALUES ('C');
ROLLBACK;
SELECT count(*) AS qoldi FROM toplam_sinov;
DROP TABLE toplam_sinov;
text
TRUNCATE TABLE
BEGIN
INSERT 0 1
INSERT 0 1
ERROR:  duplicate key value violates unique constraint "toplam_sinov_matn_key"
DETAIL:  Key (matn)=(A) already exists.
ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK
 qoldi
-------
     0
(1 row)

DROP TABLE

Xato bo'lgach Postgres tranzaksiyani "buzilgan" deb belgiladi: C umuman bajarilmadi (current transaction is aborted… — "joriy tranzaksiya bekor qilingan, blok oxirigacha buyruqlar e'tiborsiz"). ROLLBACK dan keyin — 0. Hech biri. Bu darsning qolgan qismi — shu xulqni Nest'dagi qatlamlar orqali olish.

3. Birinchi yo'l: tranzaksiyani parametr qilib berish

3.1 Prisma

27-qismdagi interaktiv tranzaksiya (Prisma: nested write va tranzaksiyalar):

ts
await prisma.$transaction(async (tx) => {
  const v = await tx.vazifa.create({ data: { matn, egasiId } });
  await tx.izoh.create({ data: { vazifaId: v.id, matn: izoh } });
});

tx — shu tranzaksiyaga bog'langan klient. Callback ichida xato tashlansa, Prisma ROLLBACK qiladi. Muammo — qatlamlar: tx ni VazifaRepo.yarat(tx, …) va IzohRepo.yoz(tx, …) ga parametr qilib berish kerak. Har repository metodi imzosi "tranzaksiya bo'lishi mumkin" degan qo'shimcha argument bilan to'ladi.

3.2 TypeORM

ts
await this.dataSource.transaction(async (manager) => {
  const v = await manager.save(VazifaEntity, { matn, egasiId });
  await manager.save(IzohEntity, { vazifaId: v.id, matn: izoh });
});

manager — tranzaksiyadagi EntityManager. Past darajada — QueryRunner: connect(), startTransaction(), commitTransaction() / rollbackTransaction(), release() — hammasi qo'lda, try/finally bilan. Bu ikki shakl — TypeORM hujjati bo'yicha; biz ularni sinamadik. Muammo o'sha: manager ni qatlamlar orqali uzatish.

Kanonda Prisma tranzaksiyasi PrismaOmbori ichida — ombor metodi o'zi tranzaksiya ochadi va hamma ishni bir metodda qiladi. Xizmat tranzaksiyani bilmaydi. Bu bitta ombor ichidagi amallar uchun yetarli.

4. Ikkinchi yo'l: CLS orqali — @Transactional()

4.1 G'oya

So'rov konteksti: nestjs-cls darsida CLS bilan so'rov logger'ini parametrsiz uzatgandik. Tranzaksiya ham xuddi shu: @Transactional() metod boshlanganda tranzaksiya ochadi va uni CLS kontekstiga qo'yadi. Repository'lar TransactionHost dan "joriy tranzaksiya" ni so'raydi. Imzolar o'zgarmaydi.

sequenceDiagram
  participant X as Xizmat (@Transactional)
  participant C as CLS konteksti
  participant V as VazifaRepo
  participant I as IzohRepo
  participant DB as Postgres
  X->>DB: BEGIN
  X->>C: tx ni qo'y
  X->>V: yarat(matn)
  V->>C: txHost.tx
  V->>DB: INSERT vazifa
  X->>I: yoz(id, izoh)
  I->>DB: INSERT izoh — xato
  X->>DB: ROLLBACK

Diagrammada VazifaRepo va IzohRepo bir-birini bilmaydi va tranzaksiyani parametr sifatida olmaydi. Ikkalasi CLS'dagi bitta tx ni oladi. Xato chiqqanda @Transactional hamma narsani bekor qiladi.

4.2 Kanon tajribasi

@nestjs-cls/transactional 4.0.1, Kysely adapteri 2.0.1 (kanon tajribasi kodi, telefon uchun bo'lindi; bazaga ulanish satri qisqartirildi):

ts
type DB = {
  vazifalar: { id: string; egasi_id: string; matn: string };
  izohlar: {
    id: string; vazifa_id: string; muallif_id: string; matn: string;
  };
};
const KYSELY = "KYSELY";
type Tx = TransactionalAdapterKysely<DB>;

@Injectable()
class VazifaRepo {
  constructor(private readonly tx: TransactionHost<Tx>) {}
  async yarat(matn: string) {
    const qator = await this.tx.tx.insertInto("vazifalar")
      .values({ egasi_id: "1", matn } as never)
      .returning("id").executeTakeFirstOrThrow();
    return qator.id;
  }
}

@Injectable()
class IzohRepo {
  constructor(private readonly tx: TransactionHost<Tx>) {}
  async yoz(vazifaId: string, matn: string) {
    if (matn === "") throw new Error("izoh bo'sh — xato (ataylab)");
    await this.tx.tx.insertInto("izohlar")
      .values({ vazifa_id: vazifaId, muallif_id: "1", matn } as never)
      .execute();
  }
}

this.tx.tx — TransactionHost ning tx xususiyati: tranzaksiya ochiq bo'lsa — tranzaksiyadagi Kysely, bo'lmasa — oddiy Kysely. Repository ikkala holatda ham bir xil kod bilan ishlaydi. as never — tajriba uchun tur tekshiruvini chetlab o'tish (to'liq DB turi yozilmagan: jadvalda boshqa majburiy ustunlar ham bor).

Xizmat:

ts
@Injectable()
class Xizmat {
  constructor(
    private readonly v: VazifaRepo,
    private readonly i: IzohRepo,
    private readonly tx: TransactionHost<Tx>,
  ) {}
  // @Transactional — metod ichidagi hamma tx.tx chaqiruvi BITTA
  // tranzaksiyada (CLS orqali uzatiladi — parametr sifatida emas)
  @Transactional()
  async vazifaVaIzoh(matn: string, izoh: string) {
    const id = await this.v.yarat(matn);
    await this.i.yoz(id, izoh);
    return id;
  }
  async tranzaksiyasiz(matn: string, izoh: string) {
    const id = await this.v.yarat(matn);
    await this.i.yoz(id, izoh);
    return id;
  }
}

Ikki metod — so'zma-so'z bir xil, farqi faqat dekoratorda. Modulni ulash:

ts
@Module({
  imports: [
    ClsModule.forRoot({
      global: true,
      plugins: [
        new ClsPluginTransactional({
          imports: [DbModule],
          adapter: new TransactionalAdapterKysely({
            kyselyInstanceToken: KYSELY,
          }),
        }),
      ],
    }),
  ],
  providers: [VazifaRepo, IzohRepo, Xizmat],
})
class M {}

ClsPluginTransactional — nestjs-cls plagini. adapter — qaysi kutubxona bilan (Kysely, Prisma, TypeORM, Drizzle uchun adapterlar bor). kyselyInstanceToken — DbModule dagi Kysely provider'ining tokeni (Drizzle va Kysely darsidagi naqsh).

4.3 Natija

Kanon chiqishi (kanon o'lchovi, IlmHamroh, 2026-10, NestJS 12.2.0):

text
boshida: 0
@Transactional, ikkalasi muvaffaqiyatli → vazifalar: 1
@Transactional, izoh xato (izoh bo'sh — xato (ataylab)) → vazifalar: ...
  ... rollback'dan keyin: 1 (o'zgarmadi)
tranzaksiyasiz, izoh xato → vazifalar: 2 (YARIM yozuv qoldi)

Biz xuddi shu kodni o'z PG 17 bazamizda yurgizdik (biz ishga tushirdik: @nestjs-cls/transactional 4.0.1, kysely 0.29.6) — chiqish harfma-harf bir xil.

  • Ikkinchi qator — ikkala yozuv muvaffaqiyatli: 1 vazifa.
  • Uchinchi-to'rtinchi — izoh bo'sh, IzohRepo xato tashladi. @Transactional ROLLBACK qildi: VazifaRepo allaqachon INSERT qilgan vazifa ham bekor — son 1 da qoldi.
  • Beshinchi — o'sha holat dekoratorsiz: vazifa yozildi, izoh yo'q. Son 2: yarim yozuv. Izohsiz vazifa — «Avval SQL'da» bo'limidagi birinchi blokda qolgan A va B ning o'zi.

Prisma adapteri (@nestjs-cls/transactional-adapter-prisma) ham bor; kanon uni sinamadi — Kysely bilan ko'rsatildi.

Tekshirib ko'ring: vazifaVaIzoh ichida IzohRepo this.tx.tx o'rniga to'g'ridan-to'g'ri @Inject(KYSELY) db bilan yozsa (tranzaksiyasiz Kysely). Izoh xato bersa, vazifa bekor bo'ladimi?

Javob

Vazifa bekor bo'ladi (u this.tx.tx orqali, tranzaksiyada yozilgan). Lekin agar xato vazifa emas, izoh muvaffaqiyatli yozilgandan keyin chiqsa — izoh qoladi: u tranzaksiyadan tashqarida, boshqa ulanishda yozilgan. Qoida: tranzaksiyali kodda hamma so'rov TransactionHost orqali bo'lsin; to'g'ridan-to'g'ri ulanish — jim teshik.

5. Unit of work

Bu naqsh kitoblarda unit of work (ish birligi) deyiladi: bitta biznes amali davomidagi hamma o'zgarishlar bitta birlik sifatida yoziladi yoki bekor qilinadi. Kim tranzaksiyani boshlaydi? Qoida: amalni boshlagan qatlam — odatda xizmat. Repository o'zi tranzaksiya ochmaydi (u kontekstni bilmaydi), controller ham emas (u HTTP'ni biladi, biznesni emas).

Qatlam Tranzaksiyadagi roli
Controller bilmaydi
Xizmat chegarani belgilaydi (@Transactional)
Repository joriy tranzaksiyadan foydalanadi (TransactionHost)
Baza BEGIN / COMMIT / ROLLBACK

6. Tajribada topilgan haqiqiy xato: /toplam

6.1 Xato

Kanon tranzaksiya tajribasini yozayotib vazifalar-api ning bulk endpoint'ini ham tekshirdi. POST /v1/vazifalar/toplam — bir so'rovda bir nechta vazifa yaratish (25-qismda qurilgan, Bulk va uzoq davom etuvchi operatsiyalar). So'rov: [A, B, A-takror] — uchinchisi birinchisining katta harfli nusxasi (bazada registrga befarq noyob). Kanon xom chiqishi (kanon o'lchovi, IlmHamroh, 2026-10; javob qisqartirilgan):

text
oldin: 12
POST /toplam [A, B, A(takror)] → 409 {"type":"https://vazifalar-api.example/xatolar/takror-matn","title":"Takror vazifa","status":409,"detail":"Bu vazifa ro'yxatda allaqachon bor","instance":"/v1/vazifalar/toplam","kod":"takror-matn","so
keyin: 14

Biz ham takrorladik — loyihaning o'tgan darsdagi holatida, o'z PG 17 bazamizda (biz ishga tushirdik):

text
oldin: 12
POST /v1/vazifalar/toplam → 409
{"type":"https://vazifalar-api.example/xatolar/takror-matn","title":"Takror vazifa","status":409,"detail":"Bu vazifa ro'yxatda allaqachon bor","instance":"/v1/vazifalar/toplam","kod":"takror-matn","sorovId":"…"}
keyin: 14

Mijoz 409 oldi — "xato, hech narsa bo'lmadi" deb tushunadi. Aslida ikkita vazifa yaratildi. Mijoz so'rovni tuzatib qayta yuborsa — A va B endi takror bo'ladi va yana 409. Foydalanuvchi uchun bu tushunarsiz aylanma.

6.2 Sabab

Xizmatdagi kod (25-qismdan beri, Express'da ham aynan shu):

ts
async toplamYarat(
  yangilar: readonly YangiVazifa[],
): Promise<Vazifa[]> {
  // ... chegarani tekshirish
  const yaratilgan: Vazifa[] = [];
  for (const y of yangilar) {
    yaratilgan.push(await this.ombor.yarat(y));
  }
  return yaratilgan;
}

Har ombor.yarat — alohida yozuv (autocommit — «Avval SQL'da» bo'limidagi birinchi blok). Uchinchisi KonfliktXatosi tashladi va u for siklini, butun metodni va butun so'rovni yiqitdi. Oldingi ikkitasi esa allaqachon bazada edi.

Halol aytamiz: bu xato 25-qismdagi darsimizda ham bor. U yerda /toplam validatsiya xatolarini (422) to'g'ri ishlagan — noto'g'ri element o'tkazib yuborilgan, qolganlari yaratilgan. Lekin bazadan kelgan xato (takror matn) sinalmagan. 25–27-qism testlari ham buni ko'rmagan. Kodni Nest'ga ko'chirish uni tuzatmadi va buzmadi ham — xizmat mantiqi o'zgarmay ko'chgan edi. Topilishiga tranzaksiya tajribasi sabab bo'ldi: "yarim yozuv qayerda bo'lishi mumkin?" degan savol.

Diqqat: "Kod ishlaydi va testlar o'tadi" — "xato yo'q" degani emas. Testlar faqat siz o'ylagan holatlarni tekshiradi. Bu xato "bulk so'rovning o'rtasida baza xatosi" holatida yashiringan edi — hech kim bunday test yozmagan.

6.3 Ikki to'g'ri javob

A: hammasi yoki hech biri B: har element o'z natijasi
Qanday bitta tranzaksiya (@Transactional) har element alohida, xato ushlanadi
[A, B, A-takror] 409, bazada hech narsa 207: [201, 201, 409]
Mijozga qayta yuborish oson har elementni o'qish kerak
Mos keladi elementlar bog'liq (buyurtma + to'lov) elementlar mustaqil (vazifalar ro'yxati)

Ikkalasi ham to'g'ri — "yarim yozuv + 409" dan tashqari hamma narsa to'g'ri. Kanon B ni tanladi. Sabab: bu endpoint allaqachon qisman muvaffaqiyat bilan ishlaydi. 25-qismdan beri u yaroqsiz elementni 422 bilan belgilab, qolganlarini yaratadi va 207 Multi-Status qaytaradi. Takror matn ham — bitta elementning muammosi, xuddi validatsiya xatosi kabi. Bir endpoint ichida ikki xil falsafa (validatsiyada — qisman, bazada — hammasi yoki hech biri) mijozni chalkashtirardi.

A variant qachon kerak? Elementlar bir-biriga bog'liq bo'lsa: buyurtma va uning qatorlari, pul o'tkazmasining ikki tomoni. U holda yarim natija ma'nosiz — @Transactional() bilan bitta tranzaksiya.

7. Loyiha qadami: /toplam tuzatildi

7.1 Avval test — xato ko'rinsin

To'g'ri tartib: avval xatoni ushlaydigan test, keyin tuzatish. test/ombor-modul.test.ts dagi Postgres to'plamiga (describe("OmborModule.forRootAsync (postgres)")) yangi test, importlarga import request from "supertest";:

ts
    // regressiya: avval [A, B, A-takror] → 409, lekin A va B bazada
    // qolardi (yarim yozuv)
    test("POST /toplam takror matn bilan — 207, har element o'z " +
      "statusi (201, 201, 409)", async () => {

(Testning to'liq matni pastda.) Uni tuzatishdan oldingi kodda yurgizdik (biz ishga tushirdik, PG 17 namuna bazasi):

text
 FAIL  test/ombor-modul.test.ts > OmborModule.forRootAsync (postgres) > POST /toplam takror matn bilan — 207, har element o'z statusi (201, 201, 409)
AssertionError: Expected values to be strictly equal:

409 !== 207

Test xatoni ko'rsatdi: kutilgan 207, kelgan 409. Yiqilgan test o'zi ham ikki yetim vazifa qoldirdi — xatoning yana bir isboti; ularni qo'lda o'chirdik.

7.2 Xizmat

src/vazifalar/vazifalar.service.ts — importga IlovaXatosi qo'shiladi, MAKS_VAZIFA dan keyin natija turi:

ts
export type ToplamNatija =
  | { ok: true; vazifa: Vazifa }
  | { ok: false; xato: IlovaXatosi };

Discriminated union: ok maydoni bo'yicha TypeScript qaysi variant ekanini biladi. Metod (kanon, formatlangan, izoh qisqartirildi):

ts
  /**
   * Bulk yaratish: har element o'z natijasi bilan (207 — qisman
   * muvaffaqiyat). 25–27 dan qolgan xato tuzatildi: avval birinchi
   * kutilgan xato (takror matn — 409) butun so'rovni yiqitardi, lekin
   * undan OLDINGI elementlar bazada QOLARDI. Kutilmagan xato
   * (IlovaXatosi emas) — avvalgidek butun so'rov 500
   */
  async toplamYarat(
    yangilar: readonly YangiVazifa[],
  ): Promise<ToplamNatija[]> {
    if ((await this.ombor.soni({})) + yangilar.length > MAKS_VAZIFA) {
      throw new KonfliktXatosi(
        `Ro'yxat to'ladi (${String(MAKS_VAZIFA)})`,
        { kod: "royxat-toldi", sarlavha: "Vazifalar soni chegarada" },
      );
    }
    const natijalar: ToplamNatija[] = [];
    for (const y of yangilar) {
      try {
        const vazifa = await this.ombor.yarat(y);
        natijalar.push({ ok: true, vazifa });
      } catch (x) {
        if (!(x instanceof IlovaXatosi)) throw x;
        natijalar.push({ ok: false, xato: x });
      }
    }
    return natijalar;
  }

Eng muhim qator — if (!(x instanceof IlovaXatosi)) throw x;. Faqat kutilgan xato (takror matn, 409) element natijasiga aylanadi. Kutilmagan xato — baza uzildi, kodda TypeError — avvalgidek butun so'rovni 500 bilan yiqitadi (Xato turlari va custom error iyerarxiyasi). Aks holda baza o'chganda mijoz "har element 500" degan 207 olardi — bu yolg'on muvaffaqiyat.

7.3 Controller

src/vazifalar/vazifalar.controller.ts dagi toplam metodida natijalarni yig'ish qismi:

ts
    const yaratilgan = await this.xizmat.toplamYarat(yaroqli);
    let k = 0;
    const natijalar = tekshirilgan.map((t, indeks): Natija => {
      const joy = `body.vazifalar[${String(indeks)}]`;
      if (!t.success) {
        const xabar = t.error.issues.map((i) => i.message).join("; ");
        return { indeks, holat: 422, xato: { joy, xabar } };
      }
      const n = yaratilgan[k++];
      if (n === undefined) return { indeks, holat: 500 };
      return n.ok
        ? { indeks, holat: 201, vazifa: n.vazifa }
        : {
          indeks,
          holat: n.xato.holat,
          xato: { joy, xabar: n.xato.message },
        };
    });

Kanonda o'zgargani — oxirgi uch qator: avval vazifa === undefined ? 500 : 201 edi, endi n.ok bo'yicha 201 yoki xatoning o'z statusi (409). Qolganini telefon uchun bo'ldik (joy va xabar o'zgaruvchilari), ma'no aynan. Keyingi qator — res.status(natijalar.every((n) => n.holat === 201) ? 201 : 207) — o'zgarmadi: hammasi yaratilsa 201, aks holda 207.

7.4 Test to'liq

ts
    // regressiya: avval [A, B, A-takror] → 409, lekin A va B bazada
    // qolardi (yarim yozuv)
    test("POST /toplam takror matn bilan — 207, har element o'z " +
      "statusi (201, 201, 409)", async () => {
      const app = await ilovaYarat({
        config: configYukla({
          SAQLASH: "postgres", DATABASE_URL: URL, EGASI_ID: "1",
          LOG_LEVEL: "silent",
        }),
      });
      const http = app.getHttpServer();
      const tag = `t36-${String(Date.now())}`;
      const r = await request(http)
        .post("/v1/vazifalar/toplam")
        .send({
          vazifalar: [
            { matn: `${tag} A` },
            { matn: `${tag} B` },
            { matn: `${tag.toUpperCase()} A` },
          ],
        });
      assert.equal(r.status, 207);
      type N = { holat: number; vazifa?: { id: number } };
      const natijalar = (r.body as { natijalar: N[] }).natijalar;
      const holatlar = natijalar.map((n) => n.holat);
      assert.deepEqual(holatlar, [201, 201, 409]);
      for (const n of natijalar) {
        if (n.vazifa === undefined) continue;
        await request(http)
          .delete(`/v1/vazifalar/${String(n.vazifa.id)}`)
          .expect(204);
      }
      await app.close();
    });

tag — Date.now() bilan noyob: test har yugurishda yangi matnlar bilan ishlaydi va boshqa testlarning ma'lumotiga tegmaydi. Oxirida yaratilganlar o'chiriladi — namuna bazasi toza qoladi. tag.toUpperCase() — takror, lekin boshqa registrda: bazadagi noyoblik registrga befarq ekanini ham tekshiradi.

7.5 Natija va tekshiruv

Tuzatilgan kod bilan o'sha so'rov (biz ishga tushirdik, PG 17; yaratildi/yangilandi vaqtlari va id lar har safar boshqa):

text
oldin: 12
POST /v1/vazifalar/toplam → 207
{"natijalar":[{"indeks":0,"holat":201,"vazifa":{"id":52,"matn":"Toplam sinovi A","bajarildi":false,"teglar":[],"yaratildi":"2026-11-02T07:01:11.653Z","yangilandi":"2026-11-02T07:01:11.653Z","tartib":14,"versiya":1}},{"indeks":1,"holat":201,"vazifa":{"id":53,"matn":"Toplam sinovi B","bajarildi":false,"teglar":[],"yaratildi":"2026-11-02T07:01:11.676Z","yangilandi":"2026-11-02T07:01:11.676Z","tartib":15,"versiya":1}},{"indeks":2,"holat":409,"xato":{"joy":"body.vazifalar[2]","xabar":"Bu vazifa ro'yxatda allaqachon bor"}}]}
keyin: 14

keyin: 14 — endi bu to'g'ri: mijozga aynan shuni aytdik — ikkitasi yaratildi (201), uchinchisi takror (409). Javob va baza bir xil gapiradi.

bash
npm run check

Biz o'z nusxamizda yurgizdik: tip 0, lint 0; ombor-modul.test.ts PG 17 bazamiz bilan — 5 passed. Kanon to'liq muhitida (kanon o'lchovi, IlmHamroh, 2026-10): Vitest 118 passed | 1 skipped (119) (+1 test), build, Testcontainers 10/10, jonli 9 so'rov uch saqlash rejimida.

Commit:

bash
git add -A
M="fix: /toplam — kutilgan xato (takror matn) butun so'rovni"
M+=" yiqitmaydi: har element o'z statusi (207);"
M+=" avval 409 qaytib, yaratilganlar bazada qolardi"
git commit -m "$M"

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Rollback'dan keyin ham yozuv qoldi repository TransactionHost emas, to'g'ridan ulanishdan foydalangan hamma so'rov this.tx.tx orqali
Tranzaksiya commit bo'ldi, xato bo'lsa ham @Transactional metod ichida xato catch qilinib yutilgan xatoni qayta tashlang yoki ichki amalni alohida qiling
Bazada qulf uzoq ushlanadi tranzaksiya ichida tashqi HTTP (SMS, to'lov) tashqi chaqiruv — tranzaksiyadan keyin (outbox)
Yozuv tranzaksiyadan "qochib" ketdi ichkarida await siz Promise har chaqiruvni await qiling
409, lekin yozuvlar qoldi bulk'da kutilgan xato butun so'rovni yiqitdi «Ikki to'g'ri javob» bo'limidagi A yoki B variant

Uchinchi qator alohida eslatma: tranzaksiya ichida 3 soniyalik tashqi so'rov — 3 soniya qulf. 29-qismdagi transactional outbox shu muammo uchun (Transactional outbox naqshi).

9. Mashqlar

Mashqlar kurs/mashqlar/30/35-tranzaksiyalar/ da.

1-mashq (oson): kim nima qiladi?

To'ldiring:

  • Tranzaksiya chegarasini belgilaydigan dekorator — @ ()
  • Repository joriy tranzaksiyani qayerdan oladi? TransactionHost ning xususiyati.
  • Kanon /toplam uchun qaysi status tanladi: hammasi muvaffaqiyatli bo'lmasa —
  • Kutilmagan xato (baza uzildi) bulk so'rovda —
Yechim
  • Transactional — xizmat metodida.
  • tx — this.tx.tx: tranzaksiya bo'lsa — tranzaksiyadagi, bo'lmasa — oddiy klient.
  • 207 — qisman muvaffaqiyat; hammasi yaratilsa — 201.
  • 500 — faqat IlovaXatosi element natijasiga aylanadi.

2-mashq (o'rta): ikki strategiya modeli

Xotiradagi kichik "baza" yozing: baza.yoz(matn) — registrga befarq takror bo'lsa KonfliktXatosi (IlovaXatosi dan meros, holat = 409) tashlasin. Ikki funksiya: hammasiYokiHechBiri(matnlar) — boshida bazaning nusxasini oladi, xato bo'lsa nusxani qaytaradi va 409 qaytaradi; harBiriOzi(matnlar) — 207/201 va har element holati. Har biridan keyin bazadagi sonni chiqaring. Ishora: Sayoz va chuqur nusxalash, instanceof.

Yechim
js
class IlovaXatosi extends Error {
  constructor(xabar, holat) { super(xabar); this.holat = holat; }
}
class KonfliktXatosi extends IlovaXatosi {
  constructor(xabar) { super(xabar, 409); }
}

const baza = {
  qatorlar: ["Non olish"],
  yoz(matn) {
    const bor = this.qatorlar
      .some((q) => q.toLowerCase() === matn.toLowerCase());
    if (bor) throw new KonfliktXatosi("Bu vazifa allaqachon bor");
    this.qatorlar.push(matn);
  },
};

function hammasiYokiHechBiri(matnlar) {
  const nusxa = [...baza.qatorlar]; // BEGIN
  try {
    for (const m of matnlar) baza.yoz(m);
    return 201; // COMMIT
  } catch (x) {
    baza.qatorlar = nusxa; // ROLLBACK
    if (x instanceof IlovaXatosi) return x.holat;
    throw x;
  }
}

function harBiriOzi(matnlar) {
  const holatlar = matnlar.map((m) => {
    try {
      baza.yoz(m);
      return 201;
    } catch (x) {
      if (!(x instanceof IlovaXatosi)) throw x;
      return x.holat;
    }
  });
  const umumiy = holatlar.every((h) => h === 201) ? 201 : 207;
  return { umumiy, holatlar };
}

const sorov = ["Sut", "Olma", "SUT"];
console.log(hammasiYokiHechBiri(sorov), baza.qatorlar.length);
console.log(harBiriOzi(sorov), baza.qatorlar.length);

Konsolda:

text
409 1
{ umumiy: 207, holatlar: [ 201, 201, 409 ] } 3

Birinchi strategiya: 409 va bazada faqat boshidagi bitta qator — Sut va Olma bekor bo'ldi. Ikkinchisi: 207, [201, 201, 409], bazada 3 qator. Ikkalasi ham izchil: javob bazadagi holatni aniq tasvirlaydi. Kanondagi eski xato — uchinchi, noizchil variant edi: 409 + 3 qator.

3-mashq (qiyin): @Transactional modeli — AsyncLocalStorage bilan

Node'da tranzaksiyani CLS orqali uzatish modelini yozing. tranzaksiyada(fn) — yangi tranzaksiya obyektini ({ id, yozuvlar: [] }) AsyncLocalStorage ga qo'yib fn ni bajarsin; muvaffaqiyatda yozuvlarni baza ga qo'shsin (COMMIT), xatoda tashlab yuborsin (ROLLBACK) va xatoni qayta tashlasin. joriyTx() — joriy tranzaksiyani qaytarsin. Ikki repository (vazifaYoz, izohYoz) tranzaksiyani parametr sifatida OLMASIN — joriyTx() dan foydalansin; tranzaksiya bo'lmasa — to'g'ridan bazaga yozsin. Ishora: AsyncLocalStorage.

Yechim
js
import { AsyncLocalStorage } from "node:async_hooks";

const baza = [];
const als = new AsyncLocalStorage();
let keyingi = 0;

const joriyTx = () => als.getStore();

async function tranzaksiyada(fn) {
  const tx = { id: ++keyingi, yozuvlar: [] };
  try {
    const natija = await als.run(tx, fn);
    baza.push(...tx.yozuvlar); // COMMIT
    return natija;
  } catch (x) {
    const n = tx.yozuvlar.length;
    console.log(`tx ${tx.id}: ROLLBACK (${n} yozuv)`);
    throw x;
  }
}

function yoz(qator) {
  const tx = joriyTx();
  if (tx) tx.yozuvlar.push(qator);
  else baza.push(qator); // tranzaksiyasiz — darhol
}

const vazifaYoz = async (matn) => yoz(`vazifa: ${matn}`);
async function izohYoz(matn) {
  if (matn === "") throw new Error("izoh bo'sh");
  yoz(`izoh: ${matn}`);
}

async function vazifaVaIzoh(matn, izoh) {
  await vazifaYoz(matn);
  await izohYoz(izoh);
}

await tranzaksiyada(() => vazifaVaIzoh("Non olish", "ertalab"));
console.log("1:", baza.length);
await tranzaksiyada(() => vazifaVaIzoh("Sut olish", ""))
  .catch((e) => console.log("xato:", e.message));
console.log("2:", baza.length);
await vazifaVaIzoh("Olma olish", "").catch(() => {});
console.log("3:", baza.length, baza.at(-1));

Konsolda:

text
1: 2
tx 2: ROLLBACK (1 yozuv)
xato: izoh bo'sh
2: 2
3: 3 vazifa: Olma olish

Kanon tajribasining uchala qadami modelda: tranzaksiyada ikkalasi (2 yozuv), tranzaksiyada xato — vazifa ham tashlandi (son o'zgarmadi), tranzaksiyasiz xato — yetim vazifa qoldi (vazifa: Olma olish). Repository'lar tranzaksiyani parametr qilib olmadi — joriyTx() uni kontekstdan topdi. @nestjs-cls/transactional ham shunday, faqat "yozuvlar" o'rnida haqiqiy BEGIN/COMMIT va ulanish.

10. Real ishda

  • @Transactional() — Nest loyihalarida nestjs-cls bilan birga keng tarqalgan; Spring (Java) dan kelgan dasturchilarga tanish nom.
  • Bulk endpoint'lar — yarim yozuv xatolari eng ko'p shu yerda. API dizayn review'ida birinchi savol: "o'rtadagi element yiqilsa nima bo'ladi?".
  • Regressiya testi — topilgan har xato uchun avval yiqiladigan test, keyin tuzatish. Shu darsdagi tartib — kasbiy odat.
  • Intervyu: "Nest'da bir nechta xizmat chaqiruvini qanday bitta tranzaksiyaga o'raysiz?", "unit of work nima?", "207 Multi-Status qachon kerak?".

Xulosa

  • Bir amal bir nechta yozuv bo'lsa — bitta tranzaksiya; aks holda yarim yozuv.
  • Tranzaksiyani uzatish: parametr (Prisma tx, TypeORM manager) yoki CLS (@Transactional + TransactionHost).
  • Chegara — xizmatda; repository joriy tranzaksiyadan (this.tx.tx) foydalanadi; tashqi chaqiruvlar — tranzaksiyadan tashqarida.
  • Kanon tajribasi: @Transactional bilan rollback — 1 → 1, tranzaksiyasiz — 1 → 2 (yarim yozuv).
  • vazifalar-api dagi /toplam 25-qismdan beri 409 qaytarib, yozuvlarni qoldirardi; endi har element o'z statusi (207).
  • Bulk'da ikki izchil javob bor — hammasi yoki hech biri (409) va har element o'zi (207); noizchil uchinchisi — xato.

Keyingi dars: Repository naqshi va qatlamlar — VazifaOmbori interfeysi, token bilan almashtirish va ORM turlarini xizmatdan yashirish: kanon nega shunday qurilganini yig'amiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS tranzaksiyalar: @Transactional, TransactionHost va /toplam'dagi yarim yozuv xatosi — IlmHamroh