IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya22/34-dars21 daqiqa
Mundarija (26)

Soft delete ORM'da: Prisma extension va TypeORM @DeleteDateColumn

Qisqacha: ORM'da soft delete — delete() ni UPDATE … SET ochirilgan_vaqt ga aylantirish va har o'qishga ochirilgan_vaqt IS NULL ni avtomatik qo'shish. TypeORM buni @DeleteDateColumn bilan 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 uchun vazifalar-api filtrni 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.
  • findUnique ga ham filtr qo'shish mumkinligini va include qayerda teshik qoldirishini ko'rasiz.
  • TypeORM'da softDelete, withDeleted va restore ni ishlatasiz va tiklashdagi 23505 ni 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):

ts
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:

  • delete ichida xom.vazifa.update — kengaytirilmagan klient. db.vazifa.update desak, chaqiruv yana extension'dan o'tardi. Prisma Client extensions darsida aytganmiz: asl klientni chaqirish — odatiy naqsh.
  • { ochirilganVaqt: null, ...a.where } — tartib muhim. Chaqiruvchi where da ochirilganVaqt ni o'zi bersa (Savatni ko'rish uchun), uniki ustun bo'ladi.
  • findUnique ro'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):

text
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 qamralmaydi

Biz 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):

ts
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);
text
findUnique (filtr bilan): topilmadi
findUniqueOrThrow: xato P2025

Ishladi. 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 updateMany kanon 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):

ts
const tor = await db.foydalanuvchi.findUnique({
  where: { id: f.id },
  include: { vazifalar: { where: { ochirilganVaqt: null } } },
});
text
include: Birinchi, Uchinchi, Ikkinchi
include + where: Birinchi, Uchinchi

Lekin 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:

js
// 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:

text
{"vazifalar":{"include":{"izohlar":{"where":{"ochirilganVaqt":null}}},"where":{"ochirilganVaqt":null}}}
{"vazifa":true}
noma'lum: vazifa.teglar

Uch natijani o'qiymiz:

  1. Ichma-ich include — ikkala darajaga ham where qo'shildi. Shu qismi ishlaydi.
  2. Izohdan vazifaga (izoh.vazifa) — filtr qo'shilmadi. Bu "bitta" tomonga relation: Prisma unda where ni qabul qilmaydi, chunki natija massiv emas, bitta obyekt. Savatdagi vazifaning izohini ochsangiz, vazifaning o'zi ham keladi.
  3. 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):

text
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:

ts
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:

js
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):

js
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):

text
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'q

Bu skriptni ham PostgreSQL 17.6 da qayta yurgizdik — chiqish aynan shunday.

4.2 Chiqishni qatorma-qator o'qish

  1. softDelete — UPDATE, DELETE emas. Shartga ochirilgan_vaqt IS NULL ham qo'shilgan: allaqachon o'chirilgan yozuvning vaqti qayta yozilmaydi.
  2. find() — "Hisobot" yo'q. Biz filtr yozmadik — TypeORM o'zi qo'shdi.
  3. withDeleted: true — Savatni ko'rish usuli: filtr o'chadi.
  4. Yaratilgan SQL'da filtr aniq ko'rinadi: WHERE "Vazifa"."ochirilgan_vaqt" IS NULL.
  5. O'chirilgan "Hisobot" turganda yangi "Hisobot" saqlandi — partial index ruxsat berdi. Bazada endi ikkita "Hisobot" bor: biri Savatda, biri faol.
  6. Savatdagisini tiklash — 23505 (unique violation). Tiklansa, ikki faol "Hisobot" bo'lardi. TypeORM buni oldindan tekshirmaydi — baza rad etdi.
  7. 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:

ts
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.ts dagi 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 endi CASCADE ishlaydi — izohlar, teglar, embeddinglar ham ketadi. Tozalash paketlab bajariladi: bitta ulkan DELETE jadvalni 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:

ts
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:

  1. Qamrov to'liq emas. O'lchovda oltita yo'ldan uchtasi teshik edi. findUnique ni tuzatsa bo'ladi, lekin "bitta" tomonga include va xom SQL — yo'q. Extension "hamma joyda filtr bor" degan yolg'on ishonch beradi.
  2. Turlar. $extends boshqa tur qaytaradi — ombor, testlardagi soxta klient va tranzaksiya turlari o'zgaradi.
  3. Ikki ombor bir xil bo'lishi kerak. Kanonda PgOmbori (toza SQL) va PrismaOmbori bir 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.
  4. Kod review ko'radi. Aniq where ni 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 find ga beriladigan parametr —
  • Postgres'da noyoblik buzilishining SQLSTATE kodi —
  • Prisma'da "yozuv topilmadi" xatosining kodi —
  • Kanon Prisma omborida ochirilganVaqt: null necha 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 — findUniqueOrThrow filtrdan keyin yozuvni topmadi.
  • 6 — har biri egasiId bilan yonma-yon.
  • purge — Savatni tozalash; shundan keyin CASCADE ishlaydi.

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
js
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:

text
to'qnashuv: Hisobot
tiklandi
faol
yo'q

Birinchi 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
js
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:

text
[ [ 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: null bormi?", "include bilan 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/delete ni qamraydi; findUnique ni ham — where ga filtr qo'shsangiz (biz o'lchadik, P2025).
  • include ning "bitta" tomoni va xom SQL extension'dan tashqarida qoladi — kanon o'lchovida o'chirilgan vazifa chiqdi.
  • TypeORM @DeleteDateColumn tayyor: softDelete, avtomatik filtr, withDeleted; restore partial unique bilan 23505 beradi — 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Soft delete ORM'da: Prisma extension va TypeORM @DeleteDateColumn — IlmHamroh