Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Avval SQL'da
- 3. Birinchi yo'l: tranzaksiyani parametr qilib berish
- 3.1 Prisma
- 3.2 TypeORM
- 4. Ikkinchi yo'l: CLS orqali — @Transactional()
- 4.1 G'oya
- 4.2 Kanon tajribasi
- 4.3 Natija
- 5. Unit of work
- 6. Tajribada topilgan haqiqiy xato: /toplam
- 6.1 Xato
- 6.2 Sabab
- 6.3 Ikki to'g'ri javob
- 7. Loyiha qadami: /toplam tuzatildi
- 7.1 Avval test — xato ko'rinsin
- 7.2 Xizmat
- 7.3 Controller
- 7.4 Test to'liq
- 7.5 Natija va tekshiruv
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): kim nima qiladi?
- 2-mashq (o'rta): ikki strategiya modeli
- 3-mashq (qiyin): @Transactional modeli — AsyncLocalStorage bilan
- 10. Real ishda
- Xulosa
- Manbalar
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) => …), TypeORMdataSource.transaction(…)) yoki CLS orqali —@nestjs-cls/transactionalning@Transactional()dekoratori. Kanon tajribasida tranzaksiyasiz variant yarim yozuv qoldirdi. Xuddi shu tajribavazifalar-apining o'zida 25-qismdan beri yashirinib yotgan xatoni topdi:POST /toplam409 qaytarib, oldingi elementlarni bazada qoldirardi.
Bu darsda
- Tranzaksiyani xizmatlar orasida uzatishning ikki usulini (parametr va CLS) farqlaysiz.
@Transactional()vaTransactionHostbilan 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-apidagi/toplamxatosini 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:
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;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:
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;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 TABLEXato 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):
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
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: ROLLBACKDiagrammada 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):
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:
@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:
@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):
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,
IzohRepoxato tashladi.@TransactionalROLLBACKqildi:VazifaRepoallaqachonINSERTqilgan 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
AvaBning o'zi.
Prisma adapteri (@nestjs-cls/transactional-adapter-prisma) ham bor; kanon uni sinamadi — Kysely bilan ko'rsatildi.
Tekshirib ko'ring:
vazifaVaIzohichidaIzohRepothis.tx.txo'rniga to'g'ridan-to'g'ri@Inject(KYSELY) dbbilan 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):
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: 14Biz ham takrorladik — loyihaning o'tgan darsdagi holatida, o'z PG 17 bazamizda (biz ishga tushirdik):
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: 14Mijoz 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):
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";:
// 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):
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 !== 207Test 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:
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):
/**
* 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:
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
// 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):
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: 14keyin: 14 — endi bu to'g'ri: mijozga aynan shuni aytdik — ikkitasi yaratildi (201), uchinchisi takror (409). Javob va baza bir xil gapiradi.
npm run checkBiz 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:
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?
TransactionHostning xususiyati. - Kanon
/toplamuchun 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
IlovaXatosielement 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
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:
409 1
{ umumiy: 207, holatlar: [ 201, 201, 409 ] } 3Birinchi 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
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:
1: 2
tx 2: ROLLBACK (1 yozuv)
xato: izoh bo'sh
2: 2
3: 3 vazifa: Olma olishKanon 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 loyihalaridanestjs-clsbilan 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, TypeORMmanager) yoki CLS (@Transactional+TransactionHost). - Chegara — xizmatda; repository joriy tranzaksiyadan (
this.tx.tx) foydalanadi; tashqi chaqiruvlar — tranzaksiyadan tashqarida. - Kanon tajribasi:
@Transactionalbilan rollback — 1 → 1, tranzaksiyasiz — 1 → 2 (yarim yozuv). vazifalar-apidagi/toplam25-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
@nestjs-cls/transactionalhujjati (@Transactional,TransactionHost, adapterlar): https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional- Prisma — Interactive transactions: https://www.prisma.io/docs/orm/prisma-client/queries/transactions
- TypeORM — Transactions (
DataSource.transaction,QueryRunner): https://typeorm.io/docs/advanced-topics/transactions - RFC 4918 — 207 Multi-Status: https://www.rfc-editor.org/rfc/rfc4918#section-11.1
- PostgreSQL — Transactions: https://www.postgresql.org/docs/17/tutorial-transactions.html
- Kanon (IlmHamroh, 2026-10-28):
@Transactionaltajribasi (Kysely adapteri) va/toplamyarim yozuv xatosi (12 → 14, 409),/toplamtuzatishi va kanon tekshiruvi (118/119). SQL bloklari, tajribaning PG 17 dagi takrori,/toplamning tuzatishdan oldingi va keyingi javoblari, yiqilgan va o'tgan regressiya testi — biz ishga tushirdik (2026-11-02, PG 17, NestJS 12.2.0, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!