Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Hayot sikli
- 2.1 To'rt holat
- 2.2 ORM nimani bilishi kerak
- 3. Prisma: query extension
- 3.1 Kanon tajribasi
- 3.2 findUnique ni ham qamrash
- 3.3 include — eng qiyin teshik
- 3.4 Xom SQL va turlar
- 4. TypeORM: @DeleteDateColumn
- 4.1 Tayyor imkoniyat
- 4.2 Chiqishni qatorma-qator o'qish
- 4.3 Tiklashdagi to'qnashuv
- 4.4 Ikki yondashuv yonma-yon
- 5. Kaskad va haqiqiy o'chirish
- 6. Kanon qarori: nega loyihada extension yo'q
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): tiklashdan oldin tekshirish
- 3-mashq (qiyin): tozalash paketlari
- 9. Loyiha qadami: kanon tajribasi
- 10. Real ishda
- Xulosa
- Manbalar
Soft delete ORM'da: Prisma extension va TypeORM @DeleteDateColumn
Qisqacha: ORM'da soft delete —
delete()niUPDATE … SET ochirilgan_vaqtga aylantirish va har o'qishgaochirilgan_vaqt IS NULLni avtomatik qo'shish. TypeORM buni@DeleteDateColumnbilan tayyor beradi, Prisma'da esa o'zingiz query extension yozasiz. Kanon o'lchovi: ikkalasi ham asosiy yo'llarda ishlaydi, lekin relation orqali o'qish va xom SQL filtrsiz qoladi, tiklash esa noyoblik bilan to'qnashadi. Shuning uchunvazifalar-apifiltrni har so'rovda aniq yozadi.
Bu darsda
- Soft delete'ning to'liq hayot siklini chizasiz: o'chirish, ko'rinmaslik, tiklash, haqiqiy o'chirish.
- Prisma 7 query extension'ni yozasiz va uning qaysi yo'llarni qamramasligini o'lchaysiz.
findUniquega ham filtr qo'shish mumkinligini vaincludeqayerda teshik qoldirishini ko'rasiz.- TypeORM'da
softDelete,withDeletedvarestoreni ishlatasiz va tiklashdagi23505ni tushuntirasiz. - Kanon nega extension o'rniga aniq filtrni tanlaganini asoslab bera olasiz.
Oldin bilishingiz kerak: Soft delete, timestamps va tarix jadvallari, Prisma Client extensions, TypeORM, Ma'lumot saqlash muddati, arxivlash va anonimlashtirish.
Oldingi darsda AI bloki tugadi. Endi prodakshn naqshlariga o'tamiz — ma'lumot bilan halol ishlashdan boshlaymiz. Bu darsda loyiha o'zgarmaydi: bu — tajriba va qaror darsi.
1. Nega bu kerak?
Soft delete, timestamps va tarix jadvallari darsida yumshoq o'chirishni bazada qurdik: ochirilgan_vaqt ustuni, faollar orasidagi noyoblik (partial unique index) va "Savat". Baza tomoni tayyor. Muammo — ilova tomonida.
Har o'qish so'rovi ochirilgan_vaqt IS NULL ni eslashi kerak. vazifalar-api ning Prisma omborida bu shart 6 joyda qo'lda yozilgan. Yangi dasturchi yettinchi so'rovni yozadi va shartni unutadi — Malika Savatga tashlagan "Universitet kontrakti" vazifasi ro'yxatda qayta paydo bo'ladi.
Tabiiy savol: shartni bir marta, markazda yozib, unutib qo'ysa bo'lmaydimi? ORM'lar aynan shuni taklif qiladi. TypeORM'da bu — bitta dekorator. Prisma'da — kengaytma (extension). Prisma Client extensions darsida birinchi urinishni ko'rib, teshiklarini topgandik. Bu darsda masalani oxirigacha olib boramiz: to'liqroq extension, TypeORM varianti va ikkalasining o'lchovi.
O'xshatish: kutubxonadagi "hisobdan chiqarilgan" kitoblar. Ularni javondan olib, omborga qo'yishadi — kitob yo'q bo'lmaydi, lekin katalogda ko'rinmaydi. Agar katalogning bitta bo'limi "omborni ham ko'rsat" deb sozlangan bo'lsa — o'quvchi yo'q kitobni so'raydi.
2. Hayot sikli
2.1 To'rt holat
Yumshoq o'chirish faqat "o'chirish" emas. Yozuvning to'liq yo'li bor:
stateDiagram-v2
[*] --> Faol: yaratish
Faol --> Savatda: yumshoq o'chirish
Savatda --> Faol: tiklash
Savatda --> Yoq: tozalash (30 kun)
Faol --> Yoq: hisob o'chirildi
Yoq --> [*]Diagrammani o'qing. "Savatda" — qator bazada, ochirilgan_vaqt to'ldirilgan. "Tiklash" — ochirilgan_vaqt = NULL. "Yoq" — haqiqiy DELETE (inglizcha purge yoki hard delete). Kanonda tozalashni ochirilganlarni_tozala() funksiyasi 30 kundan keyin bajaradi — Ma'lumot saqlash muddati darsida ko'rgansiz.
2.2 ORM nimani bilishi kerak
Har holat o'tishi va har o'qish turi ORM uchun alohida vazifa:
| Amal | To'g'ri xatti-harakat |
|---|---|
delete, deleteMany |
UPDATE … SET ochirilgan_vaqt = now() |
| ro'yxat, son, bitta yozuv | faqat ochirilgan_vaqt IS NULL |
| relation orqali o'qish | bog'langan yozuvlarda ham filtr |
| xom SQL | qo'lda — ORM ko'rmaydi |
| tiklash | noyoblik to'qnashuvini ushlash |
| Savatni ko'rish | filtrni ataylab o'chirish |
Jadvaldagi har qator — keyingi bo'limlarda o'lchanadigan savol. Biror qator qamralmasa, o'chirilgan ma'lumot "sizib" chiqadi.
3. Prisma: query extension
3.1 Kanon tajribasi
Kanon Prisma 7.10 da extension yozdi. Asosiy g'oya — $allOperations: vazifa modelidagi har amal shu funksiyadan o'tadi. O'qish amallariga filtr qo'shiladi, delete esa update ga aylantiriladi (qatorlar bo'lindi, ma'no aynan):
const OQISH = ["findMany", "findFirst", "findFirstOrThrow", "count",
"aggregate", "groupBy", "updateMany"];
const db = xom.$extends({
name: "yumshoq-ochirish",
query: {
vazifa: {
async $allOperations({ operation, args, query }) {
const a = args as { where?: Record<string, unknown> };
if (OQISH.includes(operation)) {
a.where = { ochirilganVaqt: null, ...a.where };
}
if (operation === "delete") {
return xom.vazifa.update({
where: (args as { where: { id: bigint } }).where,
data: { ochirilganVaqt: new Date() },
});
}
if (operation === "deleteMany") {
return xom.vazifa.updateMany({
where: { ochirilganVaqt: null, ...a.where },
data: { ochirilganVaqt: new Date() },
});
}
return query(args);
},
},
},
});Uch narsaga e'tibor bering:
deleteichidaxom.vazifa.update— kengaytirilmagan klient.db.vazifa.updatedesak, chaqiruv yana extension'dan o'tardi. Prisma Client extensions darsida aytganmiz: asl klientni chaqirish — odatiy naqsh.{ ochirilganVaqt: null, ...a.where }— tartib muhim. ChaqiruvchiwheredaochirilganVaqtni o'zi bersa (Savatni ko'rish uchun), uniki ustun bo'ladi.findUniquero'yxatda yo'q. Kanon skriptining izohi: "Prisma unique where'ga qo'shimcha maydonni qo'shmaydi". Buni pastda tekshiramiz.
Uchta vazifa yaratildi, "Ikkinchi" extension orqali o'chirildi. Kanon chiqishi (bitta yugurish, 2026-11-03):
delete() → UPDATE: bazada qator qoldi = true, ochirilgan_vaqt bor = true
findMany (extension bilan): Birinchi, Uchinchi
count (extension): 2; count (xom): 3
findUnique (extension, id bilan — filtrsiz: Prisma unique where'ga qo'shimcha maydonni qo'shmaydi): Ikkinchi ← findUnique/findUniqueOrThrow QAMRALMADI
include (foydalanuvchi → vazifalar, ichma-ich): Birinchi, Uchinchi, Ikkinchi ← o'chirilgan ham CHIQDI (query extension faqat vazifa modelining o'z so'roviga)
$queryRaw: 3 ta ← xom SQL ham qamralmaydiBiz bu skriptni o'z kompyuterimizda, PostgreSQL 17.6 va o'sha generatsiya qilingan klient bilan qayta yurgizdik — chiqish qatorma-qator bir xil chiqdi.
Natija: findMany, count va delete — to'g'ri. findUnique, include va $queryRaw — o'chirilgan "Ikkinchi" ni qaytardi. Oltita yo'ldan uchtasi teshik.
3.2 findUnique ni ham qamrash
Kanon izohi bizni shubhaga soldi. Prisma 5 dan beri findUnique ning where qismiga noyob maydondan tashqari oddiy maydonlar ham qo'shish mumkin (hujjatda "filter on non-unique fields with UserWhereUniqueInput"). Shart bitta: kamida bitta noyob maydon (id) bo'lsin. Prisma 7 hujjati bu imkoniyatning qo'llanishlari orasida aynan soft delete ni sanaydi: findUnique({ where: { id, isDeleted: false } }). Ya'ni muammo Prisma'da emas — kanon extension'i findUnique ga filtr qo'shmagan, xolos.
Tekshirdik. findUnique va findUniqueOrThrow ni o'qish ro'yxatiga qo'shdik va o'sha bazada sinadik (biz ishga tushirdik, PostgreSQL 17.6, Prisma 7.10.1):
const OQISH = ["findMany", "findFirst", "findFirstOrThrow",
"findUnique", "findUniqueOrThrow", "count", "aggregate", "groupBy"];
// ...extension avvalgidek; delete → update
await db.vazifa.delete({ where: { id: ikkinchi.id } });
const u = await db.vazifa.findUnique({ where: { id: ikkinchi.id } });
console.log("findUnique (filtr bilan):", u?.matn ?? "topilmadi");
const x = await db.vazifa
.findUniqueOrThrow({ where: { id: ikkinchi.id } })
.catch((e: { code?: string }) => `xato ${String(e.code)}`);
console.log("findUniqueOrThrow:", typeof x === "string" ? x : x.matn);findUnique (filtr bilan): topilmadi
findUniqueOrThrow: xato P2025Ishladi. findUnique endi o'chirilgan vazifani qaytarmaydi, OrThrow varianti esa Prisma'ning "yozuv topilmadi" xatosini (P2025) tashlaydi — xuddi yozuv yo'qdek. Bu natija Prisma Client extensions darsidagi "findUnique ni taqiqlab, findFirst ishlating" maslahatiga yaxshiroq muqobil: taqiqlash shart emas, filtr qo'shish yetarli.
Tekshirib ko'ring: Nega
updateManykanon ro'yxatida bor edi, bizning ro'yxatimizda yo'q? Qaysi biri to'g'ri?
Javob
Kanon ro'yxati to'g'riroq. updateMany filtrsiz bo'lsa, Savatdagi vazifalarni ham o'zgartiradi — masalan "hammasini bajarildi qil" Savatdagilarga ham tegadi. Biz qisqa sinov uchun ro'yxatni qayta yozdik va updateMany tushib qoldi. Bu — extension'ning asosiy xavfi: ro'yxat qo'lda yuritiladi va bitta unutilgan amal jim teshik qoldiradi. Haqiqiy kodda ikki ro'yxatni birlashtiring.
3.3 include — eng qiyin teshik
foydalanuvchi.findUnique({ include: { vazifalar: true } }) — bu foydalanuvchi so'rovi. Vazifa modeli uchun yozilgan extension unda umuman chaqirilmaydi. Prisma relation'ni o'z ichida yuklaydi.
Qo'lda where qo'shsa bo'ladi. O'sha sinovda (biz ishga tushirdik):
const tor = await db.foydalanuvchi.findUnique({
where: { id: f.id },
include: { vazifalar: { where: { ochirilganVaqt: null } } },
});include: Birinchi, Uchinchi, Ikkinchi
include + where: Birinchi, UchinchiLekin buni har joyda eslash — biz qochmoqchi bo'lgan muammoning o'zi. Avtomatlashtirsa bo'ladimi? include obyektini rekursiv aylanib, yumshoq o'chiriladigan modelga olib boradigan relation'larga where qo'shish mumkin. Brauzerda kichik model bilan sinaymiz — faraz qilaylik, izohlar ham yumshoq o'chiriladi:
// qaysi model yumshoq o'chiriladi,
// relation qaysi modelga olib boradi
const YUMSHOQ = new Set(["vazifa", "izoh"]);
const RELATIONLAR = {
foydalanuvchi: { vazifalar: "vazifa" },
vazifa: { izohlar: "izoh", egasi: "foydalanuvchi" },
izoh: { vazifa: "vazifa" },
};
function includeFiltrla(model, include) {
const natija = {};
for (const [nom, qiymat] of Object.entries(include)) {
const nishon = RELATIONLAR[model]?.[nom];
if (nishon === undefined) {
throw new Error(`noma'lum: ${model}.${nom}`);
}
const ichki = qiymat === true ? {} : { ...qiymat };
if (ichki.include) {
ichki.include = includeFiltrla(nishon, ichki.include);
}
// faqat "ko'p" tomonga (massiv) where qo'yiladi
const kop = nom.endsWith("lar");
if (YUMSHOQ.has(nishon) && kop) {
ichki.where = { ...ichki.where, ochirilganVaqt: null };
}
natija[nom] = Object.keys(ichki).length === 0 ? true : ichki;
}
return natija;
}
const a = includeFiltrla("foydalanuvchi", {
vazifalar: { include: { izohlar: true } },
});
console.log(JSON.stringify(a));
console.log(JSON.stringify(includeFiltrla("izoh", { vazifa: true })));
try {
includeFiltrla("vazifa", { teglar: true });
} catch (e) {
console.log(e.message);
}Konsolda:
{"vazifalar":{"include":{"izohlar":{"where":{"ochirilganVaqt":null}}},"where":{"ochirilganVaqt":null}}}
{"vazifa":true}
noma'lum: vazifa.teglarUch natijani o'qiymiz:
- Ichma-ich
include— ikkala darajaga hamwhereqo'shildi. Shu qismi ishlaydi. - Izohdan vazifaga (
izoh.vazifa) — filtr qo'shilmadi. Bu "bitta" tomonga relation: Prisma undawhereni qabul qilmaydi, chunki natija massiv emas, bitta obyekt. Savatdagi vazifaning izohini ochsangiz, vazifaning o'zi ham keladi. - Model bilmagan relation — xato. Yangi relation qo'shilsa va xarita yangilanmasa, jim o'tmasligi kerak.
Ikkinchi qatorni loyihaning Prisma turlarida tekshirdik: xom.izoh.findMany({ include: { vazifa: { where: { ochirilganVaqt: null } } } }) ni tsc rad etdi (biz ishga tushirdik, TypeScript 6.0.3):
t-tur.ts(3,48): error TS2353: Object literal may only specify known properties, and 'where' does not exist in type 'VazifaDefaultArgs<DefaultArgs>'.Tarjimasi: "obyekt literalida faqat ma'lum xususiyatlar bo'lishi mumkin, where esa VazifaDefaultArgs turida yo'q". Ya'ni "bitta" tomonda filtr qo'yish uchun joyning o'zi yo'q.
Model qayerda soddalashgan: "ko'p tomon" ni nom oxiridagi "lar" dan bilamiz — haqiqiy kodda bu ma'lumotni Prisma'ning sxema metama'lumotidan olish kerak. select ichidagi relation'lar, _count va ichma-ich yozish amallari (create ichida connect) ham qamralmagan. Kod o'sib, Prisma'ning ichki tuzilishini qayta yozishga aylanadi.
Yana bir to'siq — Prisma'ning o'zidan. Query extension hujjatiga ko'ra args ning hamma qismini o'zgartirish mumkin, faqat include va select dan tashqari: ular natija turini o'zgartiradi va tur xavfsizligini buzadi. Demak includeFiltrla ni extension ichiga qo'yib bo'lmaydi. Uni har chaqiruv joyida qo'lda ishlatish kerak — bu esa yana "har joyda eslash" ga qaytadi.
3.4 Xom SQL va turlar
Kanon extension'i query.vazifa ichida — u faqat vazifa modelining amallarini ko'radi, $queryRaw va $executeRaw unga kirmaydi. Prisma'da xom so'rovni ham ushlash mumkin: hujjatga ko'ra query ichida model nomi o'rniga $queryRaw deb yozasiz yoki yuqori darajadagi $allOperations ni ishlatasiz. Lekin bu filtr bermaydi — ORM SQL matnini tahlil qilmaydi va unga ochirilgan_vaqt IS NULL ni qo'sha olmaydi. Xom SQL'dagi filtr — har doim qo'lda.
Yana bir yashirin narx — turlar. xom.$extends(...) qaytargan obyektning turi PrismaClient emas, yangi tur. Kanon omborida klient konstruktor orqali keladi, testlarda soxtasi beriladi, tranzaksiya callback'lari Prisma.TransactionClient turini kutadi. Extension'li klientga o'tish — omborning hamma tur imzolarini o'zgartirish demak:
const db = xom.$extends(yumshoqOchirish);
type Klient = typeof db; // PrismaClient emas — yangi tur
// tranzaksiya ichidagi `tx` ham boshqa tur bo'ladi
await db.$transaction(async (tx) => {
// tx: Klient ning tranzaksiya varianti
});4. TypeORM: @DeleteDateColumn
4.1 Tayyor imkoniyat
TypeORM darsida entity, DataSource va repository'ni o'rgandik. Soft delete TypeORM'da ichki imkoniyat: ustunni o'chirilgan sana ustuni deb belgilaysiz, qolganini ORM qiladi. Kanon tajribasi dekoratorsiz EntitySchema bilan yozilgan (deleteDate: true — @DeleteDateColumn() ning o'zi). Qatorlar bo'lindi:
import { DataSource, EntitySchema, IsNull, Not } from "typeorm";
const Vazifa = new EntitySchema({
name: "Vazifa",
tableName: "vazifalar",
columns: {
id: { type: "bigint", primary: true, generated: "identity" },
matn: { type: "text" },
ochirilganVaqt: {
type: "timestamptz", name: "ochirilgan_vaqt",
deleteDate: true, nullable: true, // = @DeleteDateColumn
},
},
indices: [{
name: "vazifalar_matn_faol", columns: ["matn"], unique: true,
where: "ochirilgan_vaqt IS NULL",
}],
});indices dagi where — partial unique index: matn faqat faol vazifalar orasida noyob. Bu Soft delete, timestamps va tarix jadvallari darsidagi indeksning o'zi.
Sinov kodi (asosiy qatorlar):
const r = ds.getRepository(Vazifa);
await r.save([{ matn: "Non olish" }, { matn: "Hisobot" },
{ matn: "Sport" }]);
await r.softDelete({ matn: "Hisobot" });
await r.find({ order: { id: "ASC" } });
await r.find({ withDeleted: true, order: { id: "ASC" } });
await r.save({ matn: "Hisobot" }); // xuddi shu matn qayta
await r.restore({ matn: "Hisobot", ochirilganVaqt: Not(IsNull()) })
.catch((e) => console.log(`restore: ${e.code} ${e.detail}`));
await ds.query("SELECT count(*)::int AS n FROM vazifalar");Kanon chiqishi (TypeORM 1.1.2, 2026-11-03):
softDelete SQL: UPDATE "vazifalar" SET "ochirilgan_vaqt" = CURRENT_TIMESTAMP WHERE "matn" = $1 AND "ochirilgan_vaqt" IS NULL
find(): Non olish, Sport
find({ withDeleted: true }): Non olish, Hisobot, Sport
find SQL WHERE: WHERE "Vazifa"."ochirilgan_vaqt" IS NULL ORDER BY "Vazifa"."id" ASC
o'chirilgan "Hisobot" bilan bir xil matn qayta saqlandi (partial unique ochirilgan_vaqt IS NULL): 2 ta qator
restore: 23505 Key (matn)=(Hisobot) already exists.
ds.query (xom SQL): 4 ta — filtr yo'qBu skriptni ham PostgreSQL 17.6 da qayta yurgizdik — chiqish aynan shunday.
4.2 Chiqishni qatorma-qator o'qish
softDelete—UPDATE,DELETEemas. Shartgaochirilgan_vaqt IS NULLham qo'shilgan: allaqachon o'chirilgan yozuvning vaqti qayta yozilmaydi.find()— "Hisobot" yo'q. Biz filtr yozmadik — TypeORM o'zi qo'shdi.withDeleted: true— Savatni ko'rish usuli: filtr o'chadi.- Yaratilgan SQL'da filtr aniq ko'rinadi:
WHERE "Vazifa"."ochirilgan_vaqt" IS NULL. - O'chirilgan "Hisobot" turganda yangi "Hisobot" saqlandi — partial index ruxsat berdi. Bazada endi ikkita "Hisobot" bor: biri Savatda, biri faol.
- Savatdagisini tiklash —
23505(unique violation). Tiklansa, ikki faol "Hisobot" bo'lardi. TypeORM buni oldindan tekshirmaydi — baza rad etdi. - Xom SQL — 4 ta: filtrsiz, xuddi Prisma'dagidek.
4.3 Tiklashdagi to'qnashuv
Oltinchi qator — eng amaliy saboq. Bu TypeORM xatosi emas, ma'lumotning tabiati. "Savatdan qaytarish" ham yangi yozuv qo'shishdek noyoblik qoidasidan o'tishi kerak. Soft delete, timestamps va tarix jadvallari darsida xuddi shu 23505 ni toza SQL'da ko'rgandik — ORM qatlami uni yashirmaydi.
Ilova bu xatoni ushlab, foydalanuvchiga tushunarli javob berishi kerak. vazifalar-api da tiklash yo'li yo'q, lekin bo'lsa, loyihaning KonfliktXatosi klassi bilan shunday ko'rinardi:
try {
await r.restore({ id });
} catch (e) {
if ((e as { code?: string }).code === "23505") {
throw new KonfliktXatosi(
"Bunday vazifa ro'yxatda bor — nomini o'zgartirib tiklang",
{ kod: "tiklab-bolmaydi" },
);
}
throw e;
}409 Conflict — Status kodlarni to'g'ri tanlash darsidan: so'rov to'g'ri, lekin resurs holati bilan to'qnashdi.
Tekshirib ko'ring: TypeORM
find({ relations: ["izohlar"] })da Savatdagi izohlarni ham yashiradimi?
Javob
Kanon buni sinamagan, shuning uchun "ha" yoki "yo'q" deb aytmaymiz. To'g'ri yo'l — o'zingiz o'lchash: bitta izohni softDelete qiling, keyin relation bilan o'qing va yaratilgan SQL'ni log'da ko'ring (logging: true). Bu darsning asosiy saboqi ham shu: ORM hujjatidagi va'dani har yo'l uchun alohida tekshiring.
4.4 Ikki yondashuv yonma-yon
| Prisma extension | TypeORM @DeleteDateColumn |
|
|---|---|---|
| Kim yozadi | siz (har amal ro'yxati) | ORM ichida tayyor |
| Bitta yozuv, ro'yxat, son | filtr qo'shsangiz — ha | ha |
| Relation orqali | "ko'p" tomonga qo'lda, "bitta" tomonga yo'q | sinalmagan |
| Xom SQL | yo'q | yo'q |
Boshqa ORM'larda ham shunga o'xshash imkoniyat bor: Sequelize'da paranoid: true, Drizzle'da tayyor yechim yo'q — filtr qo'lda (hujjat bo'yicha, sinalmagan). Qaysi ORM bo'lmasin, ikki savol qoladi: relation yo'llari va xom SQL.
5. Kaskad va haqiqiy o'chirish
Tashqi kalitning ON DELETE CASCADE qoidasi faqat haqiqiy DELETE da ishlaydi. Vazifa Savatga tushsa, uning izohlari va teglari joyida qoladi. Bu yaxshi — tiklanganda hammasi qaytadi. Lekin ikki joyda e'tibor kerak:
- Bog'langan ma'lumotni qidirish. "Teg bo'yicha qidiruv" Savatdagi vazifaning tegini ham topadi — so'rovda vazifa holati tekshirilishi kerak. Kanon
pg-ombor.tsdagi teglar so'rovi aynan shunday:WHERE t.egasi_id = $1 AND v.ochirilgan_vaqt IS NULL. - Haqiqiy o'chirish. 30 kundan keyin
ochirilganlarni_tozala()qatorni o'chiradi va endiCASCADEishlaydi — izohlar, teglar, embeddinglar ham ketadi. Tozalash paketlab bajariladi: bitta ulkanDELETEjadvalni uzoq qulflaydi.
Soft delete ORM extension'iga eng xavfli topshiriq — haqiqiy o'chirish yo'lini ham yopib qo'yish. Agar delete doim update ga aylansa, tozalash ishchisi qanday o'chiradi? Javob: kengaytirilmagan klient (xom) yoki SQL funksiya. Kanonda tozalash — SQL funksiya, uni alohida rol (vazifalar_ishchi) chaqiradi.
6. Kanon qarori: nega loyihada extension yo'q
Kanon o'lchovdan keyin extension'ni loyihaga qo'shmadi. vazifalar-api da filtr har so'rovda aniq yozilgan. Prisma omboridagi oltita joydan biri:
const q = await this.#oqi((tx) => tx.vazifa.findFirst({
where: {
id: BigInt(id), egasiId: this.#egasi, ochirilganVaqt: null,
},
select: TANLOV,
}));Shu qatorda ikkita shart yonma-yon: egasiId (begona vazifani ko'rmaslik) va ochirilganVaqt: null (Savatdagini ko'rmaslik). Ikkalasi ham aniq. Sabablar:
- Qamrov to'liq emas. O'lchovda oltita yo'ldan uchtasi teshik edi.
findUniqueni tuzatsa bo'ladi, lekin "bitta" tomongaincludeva xom SQL — yo'q. Extension "hamma joyda filtr bor" degan yolg'on ishonch beradi. - Turlar.
$extendsboshqa tur qaytaradi — ombor, testlardagi soxta klient va tranzaksiya turlari o'zgaradi. - Ikki ombor bir xil bo'lishi kerak. Kanonda
PgOmbori(toza SQL) vaPrismaOmboribir xil shartnoma bo'yicha ishlaydi va bir xil testlardan o'tadi. SQL omborida extension yo'q — u yerda filtr baribir qo'lda. Ikkalasida bir xil uslub — o'qish va review oson. - Kod review ko'radi. Aniq
whereni ko'zdan kechirish mumkin. Yashirin extension esa so'rovni ko'zdan yashiradi.
Narxi ham bor: 6 joyda takrorlanish. Kanon buni testlar bilan yopadi — ombor shartnomasi testi (hamma omborlar uchun bitta to'plam) o'chirilgan vazifa ro'yxatda chiqmasligini va qayta o'chirishda "topilmadi" bo'lishini tekshiradi. Yangi so'rov qo'shilsa, unga ham shunday test yozish — review'dagi birinchi savol.
Qachon extension to'g'ri tanlov? Kichik loyiha, bitta ORM, xom SQL yo'q va relation yo'llari kam bo'lsa. TypeORM'da esa tayyor imkoniyatni ishlatish tabiiy — faqat relation va xom SQL yo'llarini test bilan tekshiring.
flowchart TD
S{"Soft delete filtri qayerda?"} --> A["Har so'rovda aniq (kanon)"]
S --> B["ORM extension / dekorator"]
S --> C["Baza ko'rinishi (view)"]
A --> A1["ko'rinadi, review oson, takror"]
B --> B1["qulay, lekin include va raw SQL teshik"]
C --> C1["hamma yo'lga, lekin ORM bilan noqulay"]Diagrammadagi uchinchi yo'l — Soft delete, timestamps va tarix jadvallari darsidagi faol_vazifalar ko'rinishi. U xom SQL'ni ham qamraydi, lekin ORM'da yozish va o'qish ikki xil nomga (jadval va ko'rinish) bo'linadi. Har yo'lning narxi bor — tanlovni o'lchov bilan qiling.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| O'chirilgan vazifa ro'yxatda chiqdi | include relation orqali o'qidi |
relation ichida where; test |
findUnique Savatdagini qaytardi |
extension ro'yxatida yo'q | where ga ochirilganVaqt: null |
| "Hammasini bajarildi qil" Savatga ham tegdi | updateMany filtrsiz |
ro'yxatga qo'shish |
Tiklashda 23505 |
faol nusxa allaqachon bor | 409 va tushunarli xabar |
| Tozalash ishchisi hech narsani o'chirmaydi | delete doim update ga aylanadi |
asl klient yoki SQL funksiya |
| Hisobotda o'chirilganlar sanaldi | xom SQL'da filtr yo'q | SQL'da shartni qo'lda yozish |
8. Mashqlar
Mashqlar kurs/mashqlar/31/22-soft-delete/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): bo'shliqlarni to'ldiring
- TypeORM'da Savatdagilarni ham ko'rish uchun
findga beriladigan parametr — - Postgres'da noyoblik buzilishining SQLSTATE kodi —
- Prisma'da "yozuv topilmadi" xatosining kodi —
- Kanon Prisma omborida
ochirilganVaqt: nullnecha joyda yozilgan? — - Haqiqiy o'chirishning inglizcha nomi (hard delete'ning sinonimi) —
Yechim
- withDeleted —
find({ withDeleted: true }); filtr o'chadi. - 23505 — unique violation; tiklashda faol nusxa bor edi.
- P2025 —
findUniqueOrThrowfiltrdan keyin yozuvni topmadi. - 6 — har biri
egasiIdbilan yonma-yon. - purge — Savatni tozalash; shundan keyin
CASCADEishlaydi.
2-mashq (o'rta): tiklashdan oldin tekshirish
Ilova 23505 ni kutib o'tirmay, tiklashdan oldin o'zi tekshirsin. tiklash(qatorlar, id) funksiyasini yozing: qatorlar — { id, matn, ochirilgan } obyektlari massivi (ochirilgan — null yoki sana satri). Natija — "tiklandi" (qator o'zgaradi: ochirilgan = null), "yo'q" (bunday id yo'q), "faol" (allaqachon faol) yoki "to'qnashuv: <matn>" (registrga befarq bir xil matnli faol vazifa bor). Ishora: find, some (massiv metodlari), toLowerCase (matnni o'zgartirish).
Yechim
function tiklash(qatorlar, id) {
const q = qatorlar.find((x) => x.id === id);
if (q === undefined) return "yo'q";
if (q.ochirilgan === null) return "faol";
const kalit = q.matn.toLowerCase();
const band = qatorlar.some((x) => x.ochirilgan === null
&& x.matn.toLowerCase() === kalit);
if (band) return `to'qnashuv: ${q.matn}`;
q.ochirilgan = null;
return "tiklandi";
}
const qatorlar = [
{ id: 1, matn: "Non olish", ochirilgan: null },
{ id: 2, matn: "Hisobot", ochirilgan: "2026-11-01" },
{ id: 3, matn: "hisobot", ochirilgan: null },
{ id: 4, matn: "Sport", ochirilgan: "2026-11-02" },
];
console.log(tiklash(qatorlar, 2));
console.log(tiklash(qatorlar, 4));
console.log(tiklash(qatorlar, 4));
console.log(tiklash(qatorlar, 9));Konsolda:
to'qnashuv: Hisobot
tiklandi
faol
yo'qBirinchi qator — kanon tajribasidagi holat: Savatdagi "Hisobot" ni faol "hisobot" to'sib turibdi (registr farqi ahamiyatsiz — 26-qismdagi noyoblik qoidasi shunday). Haqiqiy ilovada oldindan tekshiruv baribir yetmaydi: tekshiruv va UPDATE orasida boshqa so'rov xuddi shu matnni yozishi mumkin. Shuning uchun 23505 ni ushlash ham qoladi — tekshiruv faqat yaxshiroq xabar uchun.
3-mashq (qiyin): tozalash paketlari
Tozalash ishchisi Savatda 30 kundan ortiq yotgan vazifalarni paketlab o'chiradi. tozalashPaketlari(qatorlar, hozir, kunlar, paket) funksiyasini yozing: ochirilgan sanasi hozir dan kunlar kundan oldin bo'lgan qatorlarning id'larini eng eskisidan boshlab paket talik massivlarga bo'lib qaytarsin. Faol qatorlar (ochirilgan === null) hech qachon kirmasin. Ishora: Date.parse, filter, toSorted (saralash), slice.
Yechim
const KUN = 24 * 60 * 60 * 1000;
function tozalashPaketlari(qatorlar, hozir, kunlar, paket) {
const chegara = Date.parse(hozir) - kunlar * KUN;
const eskilar = qatorlar
.filter((q) => q.ochirilgan !== null)
.filter((q) => Date.parse(q.ochirilgan) < chegara)
.toSorted((a, b) => Date.parse(a.ochirilgan)
- Date.parse(b.ochirilgan))
.map((q) => q.id);
const natija = [];
for (let i = 0; i < eskilar.length; i += paket) {
natija.push(eskilar.slice(i, i + paket));
}
return natija;
}
const qatorlar = [
{ id: 1, ochirilgan: null },
{ id: 2, ochirilgan: "2026-09-01T00:00:00Z" },
{ id: 3, ochirilgan: "2026-10-30T00:00:00Z" },
{ id: 4, ochirilgan: "2026-08-15T00:00:00Z" },
{ id: 5, ochirilgan: "2026-09-20T00:00:00Z" },
{ id: 6, ochirilgan: "2026-10-03T00:00:00Z" },
];
const hozir = "2026-11-03T00:00:00Z";
console.log(tozalashPaketlari(qatorlar, hozir, 30, 2));
console.log(tozalashPaketlari(qatorlar, hozir, 90, 2));Konsolda:
[ [ 4, 2 ], [ 5, 6 ] ]
[]30 kunlik chegara — 4-oktabr. 3-vazifa (30-oktabr) hali Savatda qoladi — foydalanuvchi uni tiklashi mumkin. 6-vazifa (3-oktabr) — chegaradan bir kun oldin, o'chadi. 90 kunlik chegarada hech kim yo'q. Kanonning ochirilganlarni_tozala() funksiyasi xuddi shu mantiqni SQL'da paketlab bajaradi.
9. Loyiha qadami: kanon tajribasi
Bu darsda vazifalar-api o'zgarmaydi va commit yo'q.
Kanon tajribasi ikki skriptdan iborat. Birinchisi — Prisma 7.10 extension, loyihaning o'z generatsiya qilingan klienti bilan, alohida bo'sh bazada (sxema — loyihaniki, namuna ma'lumotsiz). Ikkinchisi — TypeORM 1.1.2, alohida bazada synchronize bilan. Ikkalasining natijasini biz PostgreSQL 17.6 da qayta yurgizib, qatorma-qator bir xil oldik. findUnique ni qamraydigan variant — bizning qo'shimcha sinovimiz.
Nega loyihaga kirmadi — «Kanon qarori: nega loyihada extension yo'q» bo'limida: qamrov to'liq emas, turlar o'zgaradi, SQL ombori bilan uslub birligi buziladi. Loyihadagi soft delete — 26-qismdagi bazaviy qurilma (ustun, partial unique, ko'rinish) + omborlardagi aniq filtr + Ma'lumot saqlash muddati darsidagi 30 kunlik tozalash.
Docker'siz takrorlaydigan qism: darsdagi includeFiltrla modeliga izoh: { muallif: "foydalanuvchi" } relation'ini qo'shing va vazifalar → izohlar → muallif zanjirini filtrlang. Natijada qayerda where paydo bo'lishini oldindan ayting, keyin ishga tushirib tekshiring.
10. Real ishda
- Qayerda uchraydi: deyarli har SaaS'da "Savat" yoki "Arxiv" bor — Trello, Notion, Gmail. Bank va buxgalteriya tizimlarida yozuvlar umuman o'chirilmaydi, faqat belgilanadi.
- Intervyu savollari: "Soft delete'ni ORM'da qanday qilasiz va qayerda teshik qoladi?", "Tiklashdagi noyoblik to'qnashuvini qanday hal qilasiz?"
- GDPR savoli: "Soft delete 'unutilish huquqi' bilan qanday kelishadi?" Javob — muddatli tozalash va anonimlashtirish (Ma'lumot saqlash muddati).
- Kod review'da: "bu yangi so'rovda
ochirilganVaqt: nullbormi?", "includebilan Savatdagi yozuv sizmaydimi?", "tiklash 23505 ni ushlaydimi?"
Xulosa
- Soft delete — to'rt holatli hayot sikli: faol → Savat → tiklash yoki tozalash; ORM har o'tishni bilishi kerak.
- Prisma query extension
findMany/count/deleteni qamraydi;findUniqueni ham —wherega filtr qo'shsangiz (biz o'lchadik, P2025). includening "bitta" tomoni va xom SQL extension'dan tashqarida qoladi — kanon o'lchovida o'chirilgan vazifa chiqdi.- TypeORM
@DeleteDateColumntayyor:softDelete, avtomatik filtr,withDeleted;restorepartial unique bilan23505beradi — 409 ga aylantiring. - Kanon filtrni har so'rovda aniq yozadi: to'liq qamrov, o'zgarmagan turlar, SQL ombori bilan bir uslub.
Keyingi dars: Audit log — kim, qachon, nimani o'zgartirganini o'zgarmas jurnalda saqlaymiz va jurnal buzilganini hash zanjiri bilan aniqlaymiz.
Manbalar
- Prisma hujjati — Client extensions,
querykomponenti (include/selecto'zgartirilmaydi, xom so'rovlarni ushlash): https://www.prisma.io/docs/orm/v7/prisma-client/client-extensions/query - Prisma hujjati — Filter on non-unique fields with
WhereUniqueInput(soft delete misoli bilan): https://www.prisma.io/docs/orm/v7/reference/prisma-client-reference#filter-on-non-unique-fields-with-userwhereuniqueinput - Prisma hujjati — Error reference (P2025): https://www.prisma.io/docs/orm/v7/reference/error-reference#p2025
- TypeORM hujjati — Entities, special columns (
@DeleteDateColumn): https://typeorm.io/docs/entity/entities/#special-columns - TypeORM hujjati — Find options (
withDeleted): https://typeorm.io/docs/working-with-entity-manager/find-options/ - Sequelize hujjati — Paranoid: https://sequelize.org/docs/v6/core-concepts/paranoid/
- (Hujjat havolalari 2026-11-03 da ochildi; TypeORM 1.1.2 — npm'dagi joriy versiya.)
- Kanon o'lchovi (IlmHamroh, 2026-11-03): Prisma 7.10.1 + @prisma/adapter-pg, TypeORM 1.1.2, PostgreSQL 18.6, Node 24.21.0. Ikkala kanon skripti,
findUniquefiltri sinovi,includeFiltrlamodeli va mashq yechimlari — biz ishga tushirdik (2026-11-03, PostgreSQL 17.6, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!