IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime60/69-dars18 daqiqa
Mundarija (23)

GraphQL chuqur: DataLoader, chuqurlik limiti, cursor sahifalash va persisted queries

Qisqacha: GraphQL'da har maydon alohida resolver bo'lgani uchun 50 ta vazifaning egasi 50 ta alohida so'rov bilan olinadi — N+1. DataLoader bir tik ichidagi load(id) chaqiruvlarini bitta WHERE id IN (...) ga yig'adi. Mijoz so'rov shaklini o'zi tanlagani uchun server chuqurlik va murakkablik limiti qo'yishi shart. Sahifalash — Relay edges/pageInfo shaklidagi cursor connections. Production'da so'rov matni o'rniga uning hash'i yuboriladi — persisted queries: tezroq va faqat ruxsat etilgan so'rovlar.

Bu darsda

  • N+1 qayerdan kelishini ko'rib, DataLoader'ni o'zingiz yozasiz va uning ikki vazifasini (batch, kesh) tushuntirasiz.
  • Chuqurlik va murakkablik limitini yozasiz va kanon topgan Yoga tuzog'ini bilasiz.
  • Relay cursor connection shaklini (edges, node, cursor, pageInfo) yasaysiz.
  • Subscriptions, persisted queries va GraphQL Code Generator qachon kerakligini bilasiz.

Oldin bilishingiz kerak: GraphQL asoslari, N+1 muammosi, Pagination: offset, cursor, keyset, Event loop.

1. Nega bu kerak?

Oldingi darsdagi server ishladi. Endi uni haqiqiy ma'lumot bilan tasavvur qiling: Vazifa.egasi resolveri bazaga murojaat qiladi. Mobil ilova bosh ekranda 50 ta vazifa va har birining egasini so'raydi. Server nima qiladi? 1 so'rov vazifalar uchun, keyin 50 ta so'rov egalar uchun — bu oldingi darsdagi "Tekshirib ko'ring" savolining javobi.

Ikkinchi muammo — erkinlikning narxi. REST'da server javob shaklini belgilaydi. GraphQL'da mijoz: hujumchi ham mijoz. U egasi { vazifalar { egasi { vazifalar ... } } } deb 10 qavat yozsa, server millionlab obyekt hisoblaydi.

Uchinchisi — sahifalash, realtime va tiplar. Bularning har biri uchun GraphQL dunyosida qabul qilingan naqsh bor. Bu dars — shu naqshlar.

Kanon tajribasi (2026-10-27): GraphQL Yoga 5.24.6, graphql 17.0.2, DataLoader 2.2.3 — N+1, chuqurlik limiti va xato formati. Relay connections, subscriptions, persisted queries va GraphQL Code Generator — sinalmagan, hujjat bo'yicha (2026-10).

2. N+1 va DataLoader

2.1 Muammo: kanon o'lchovi

Kanon 50 ta vazifa va egalarini so'radi, egasi resolveri esa har chaqiruvda "baza" hisoblagichini oshirdi:

text
50 vazifa + egasi: oddiy resolver — 'baza' so'rovlari 50, 1-chi {"id":1,"egasi":{"ism":"Aziz"}}
50 vazifa + egasi: DataLoader — 'baza' so'rovlari 1, 1-chi {"id":1,"egasi":{"ism":"Aziz"}}

Natija bir xil, so'rovlar soni 50 dan 1 ga tushdi. Egalar atigi 5 ta edi — oddiy resolver bir xil egani 10 martadan so'radi.

2.2 DataLoader qanday ishlaydi

DataLoader (Facebook, graphql-js mualliflari) — kichik kutubxona, ikki ish qiladi:

  1. Batching (to'plash): bir tik (event loop'ning bitta aylanishi — joriy sinxron kod va undan keyingi microtask'lar) ichidagi barcha loader.load(id) chaqiruvlarini yig'ib, bitta batchFn([id1, id2, ...]) chaqiradi.
  2. Caching (kesh): bitta so'rov davomida bir id ikkinchi marta so'ralsa — o'sha Promise qaytadi.

Sir — event loop: GraphQL bajaruvchisi 50 ta egasi resolverini sinxron ketma-ket chaqiradi. Har biri load(id) qiladi va Promise oladi. DataLoader to'plamni darhol yubormaydi — joriy sinxron kod tugashini kutadi (microtask navbatidan keyin), keyin hammasini bitta chaqiruvda yuboradi.

sequenceDiagram
  participant R as Resolverlar
  participant L as DataLoader
  participant D as Baza
  R->>L: load(1)
  R->>L: load(2)
  R->>L: load(1)
  Note over L: sinxron kod tugashini kutadi
  L->>D: WHERE id IN (1, 2)
  D-->>L: 2 qator
  L-->>R: 3 ta Promise bajarildi

Uchinchi load(1) bazaga umuman bormadi — u birinchisining Promise'ini oldi. O'zimiz yozamiz:

js
function yuklovchi(batchFn) {
  const kesh = new Map();
  let navbat = [];
  function yubor() {
    const toplam = navbat;
    navbat = [];
    const idlar = toplam.map((x) => x.id);
    batchFn(idlar).then((natijalar) => {
      toplam.forEach((x, i) => x.ok(natijalar[i]));
    });
  }
  return {
    load(id) {
      if (kesh.has(id)) return kesh.get(id);
      const p = new Promise((ok) => {
        navbat.push({ id, ok });
        if (navbat.length === 1) queueMicrotask(yubor);
      });
      kesh.set(id, p);
      return p;
    },
  };
}

let sorovlar = 0;
const EGALAR = { 1: "Aziz", 2: "Dilnoza", 3: "Sardor" };
async function egalarniOl(idlar) {
  sorovlar++;
  console.log(`SELECT ... WHERE id IN (${idlar.join(", ")})`);
  return idlar.map((id) => EGALAR[id] ?? null);
}

const loader = yuklovchi(egalarniOl);
const vazifalar = [1, 2, 1, 3, 2, 1].map((egasiId) => ({ egasiId }));
const ismlar = await Promise.all(
  vazifalar.map((v) => loader.load(v.egasiId)),
);
console.log(ismlar.join(", "));
console.log("so'rovlar:", sorovlar);

Konsolda:

text
SELECT ... WHERE id IN (1, 2, 3)
Aziz, Dilnoza, Aziz, Sardor, Dilnoza, Aziz
so'rovlar: 1

Olti load — bitta SQL, uchta noyob id bilan. Takrorlangan id'lar keshdan keldi. Bu yerda ikki muhim qoida yashiringan — haqiqiy DataLoader ham ularni talab qiladi:

  • batchFn natijasi kirish tartibida va kirish uzunligida bo'lishi kerak. SQL WHERE id IN (3, 1, 2) qatorlarni ixtiyoriy tartibda qaytaradi — ularni Map bilan id bo'yicha qayta tartiblash sizning ishingiz. Topilmagan id uchun — null yoki Error.
  • Loader har so'rov uchun yangi yaratiladi (context ichida). Global loader bitta foydalanuvchi uchun keshlangan ma'lumotni boshqasiga berib yuboradi va eskirgan ma'lumotni abadiy ushlaydi.

Kanon loader'ni aynan shunday context da yaratgan:

js
const yoga = createYoga({
  schema,
  context: () => ({ egaLoader: new DataLoader(egalarniOl) }),
});
// resolver:
// Vazifa.egasi: (v, _a, ctx) => ctx.egaLoader.load(v.egasiId)

Tekshirib ko'ring: egasi resolveri await bilan boshqa narsani kutib, keyin load qilsa (masalan avval ruxsatni bazadan tekshirsa), batching ishlaydimi?

Javob

Qisman. Har resolver await dan keyin turli vaqtda "uyg'onadi", ularning load chaqiruvlari turli tiklarga tushishi mumkin — natijada bitta o'rniga bir nechta to'plam. Shuning uchun load ni resolverning boshida, boshqa await siz chaqirish yaxshi; ruxsat tekshiruvini esa ham loader orqali (yoki context da bir marta) qilish kerak.

3. Chuqurlik va murakkablik limiti

3.1 Chuqurlik

Sxemada sikl bor: Vazifa.egasi → Foydalanuvchi.vazifalar → Vazifa.egasi → .... Kanon chuqurlik limitini o'z validatsiya qoidasi sifatida yozgan va 4 va 6 qavatli so'rovlar bilan sinagan:

text
chuqurlik 4: "xatosiz" | chuqurlik 6: "So'rov chuqurligi 5 dan oshdi"

Qoida — graphql-js validatsiya qoidasi: so'rov daraxtini aylanib chiqadigan visitor. Kanon kodi (formatladik):

js
import { GraphQLError } from "graphql";

const chuqurlikLimiti = (maks) => (ctx) => ({
  Field(node, _k, _o, _y, ajdodlar) {
    const d = ajdodlar.filter((a) => a.kind === "Field").length;
    if (d >= maks) {
      ctx.reportError(new GraphQLError(
        `So'rov chuqurligi ${maks} dan oshdi`, { nodes: [node] },
      ));
    }
  },
});

const yoga = createYoga({
  schema,
  plugins: [{
    onValidate: ({ addValidationRule }) =>
      addValidationRule(chuqurlikLimiti(5)),
  }],
});

Validatsiya bajarishdan oldin ishlaydi — resolver'lar umuman chaqirilmaydi, server resurs sarflamaydi.

Kanon topilmalari — ikkita xato, ikkalasi ham jim:

  1. Birinchi urinishda qoida createYoga({ validationRules: [...] }) bilan berilgan. Yoga'da bunday opsiya yo'q — qoida xatosiz, jimgina e'tiborsiz qoldi va 6 qavatli so'rov o'tib ketdi. To'g'ri yo'l — plugin'ning onValidate hook'i va addValidationRule. (Apollo Server'da esa validationRules opsiyasi bor — shuning uchun maqolalardagi kod ko'chirilganda adashish oson.)
  2. Visitor ichida ctx.getAncestors() chaqirilgan — bunday metod yo'q. Ajdodlar visitor funksiyasining beshinchi argumenti.

Saboq: xavfsizlik qoidasini yozdingizmi — uni buzuvchi so'rov bilan test qiling. "Kod xatosiz ishga tushdi" — qoida ishladi degani emas.

Tayyor paket ham bor — graphql-depth-limit, lekin kanon tekshirdi: 1.1.0 versiyasi 2017-yildan beri yangilanmagan. Bunday paketni xavfsizlik uchun ishlatishdan oldin o'ylang; qoida 15 qatorlik — o'zingiz yozib, test qilish mumkin.

3.2 Murakkablik

Chuqurlik yetmaydi: vazifalar(birinchi: 1000) { egasi { vazifalar(birinchi: 1000) { matn } } } — atigi 3 qavat, lekin million obyekt. Murakkablik (complexity, cost) — har maydonga narx berib, ro'yxat argumentlarini ko'paytiruvchi sifatida hisoblash. Oldingi darsdagi kichik tahlilchiga ulab ko'ramiz:

js
function tahlil(matn) {
  const tok = matn.match(/[{}():]|\w+/g);
  let i = 0;
  function tanlov() {
    const m = {};
    i++;
    while (tok[i] !== "}") {
      const nom = tok[i++];
      let soni = 1;
      if (tok[i] === "(") {
        soni = Number(tok[i + 3]);
        i += 5;
      }
      m[nom] = { soni, ichki: tok[i] === "{" ? tanlov() : null };
    }
    i++;
    return m;
  }
  return tanlov();
}

function narx(tanlov) {
  let jami = 0;
  for (const { soni, ichki } of Object.values(tanlov)) {
    jami += soni * (1 + (ichki ? narx(ichki) : 0));
  }
  return jami;
}

const sorovlar = [
  "{ vazifalar(birinchi: 20) { matn } }",
  "{ vazifalar(birinchi: 50) { matn egasi { ism } } }",
  "{ vazifalar(birinchi: 1000) { egasi " +
    "{ vazifalar(birinchi: 1000) { matn } } } }",
];
for (const s of sorovlar) {
  const n = narx(tahlil(s));
  console.log(n, n > 10_000 ? "RAD ETILDI" : "ok");
}

Konsolda:

text
40 ok
200 ok
2002000 RAD ETILDI

Uchinchi so'rov chuqurligi bor-yo'g'i 3, lekin narxi milliondan oshadi. Ishlab chiqarishda ikkalasi ham kerak: chuqurlik limiti (oddiy, tez) va murakkablik limiti (aniqroq). Bundan tashqari ro'yxat argumentiga yuqori chegara (birinchi ≤ 100) — kanon resolverida Math.min(birinchi, 100) bor edi. Bu ham Rate limiting ning to'ldiruvchisi: limit so'rovlar sonini emas, har so'rov "og'irligini" cheklaydi.

4. Cursor sahifalash: Relay connections

Pagination darsida kursorli sahifalashni REST'da qildik. GraphQL'da uning qabul qilingan shakli — Relay Cursor Connections spetsifikatsiyasi (Relay — Facebook'ning GraphQL klienti; shakl esa Relay'siz ham hamma joyda ishlatiladi):

graphql
type VazifaConnection {
  edges: [VazifaEdge!]!
  pageInfo: PageInfo!
}
type VazifaEdge {
  node: Vazifa!
  cursor: String!
}
type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}
type Query {
  vazifalar(first: Int!, after: String): VazifaConnection!
}

edges — har element va uning kursori, node — elementning o'zi, pageInfo — keyingi sahifa bormi va oxirgi kursor. Mijoz keyingi sahifa uchun after: endCursor yuboradi. Resolverda bu shaklni yasash (shaffof kursor — id dan base64):

js
const kodla = (id) => btoa(`vazifa:${id}`);
const och = (k) => Number(atob(k).split(":")[1]);

function connection(royxat, { first, after }) {
  const boshi = after ? och(after) : 0;
  const keyingi = royxat.filter((v) => v.id > boshi);
  const sahifa = keyingi.slice(0, first);
  return {
    edges: sahifa.map((node) => ({ node, cursor: kodla(node.id) })),
    pageInfo: {
      hasNextPage: keyingi.length > first,
      endCursor: sahifa.length ? kodla(sahifa.at(-1).id) : null,
    },
  };
}

const VAZ = [3, 5, 8, 13, 21].map((id) => ({ id, matn: `V${id}` }));
let after = null;
for (let i = 0; i < 3; i++) {
  const s = connection(VAZ, { first: 2, after });
  console.log(s.edges.map((e) => e.node.id), s.pageInfo);
  after = s.pageInfo.endCursor;
}

Konsolda:

text
[ 3, 5 ] { hasNextPage: true, endCursor: 'dmF6aWZhOjU=' }
[ 8, 13 ] { hasNextPage: true, endCursor: 'dmF6aWZhOjEz' }
[ 21 ] { hasNextPage: false, endCursor: 'dmF6aWZhOjIx' }

Bu — keyset sahifalash (id > oxirgi), faqat GraphQL kiyimida. Bazada esa WHERE id > $1 ORDER BY id LIMIT first + 1 — bitta ortiqcha qator hasNextPage ni hisoblash uchun. slice emas, LIMIT — aks holda butun jadval xotiraga keladi. Kursor base64 — shifrlash emas, faqat "ichiga qaramang" degan kelishuv; uni imzolash yoki ichidagini tekshirish kerak bo'lsa — Pagination darsidagi qoidalar.

5. Xatolar: ikki maktab

Oldingi darsda xatolar errors massivida edi. Katta loyihalarda ikkinchi uslub ham uchraydi — xatoni sxemaga qo'shish (errors as data):

graphql
type VazifaYaratildi { vazifa: Vazifa! }
type ValidatsiyaXatosi { maydon: String!, xabar: String! }
union VazifaQoshNatija = VazifaYaratildi | ValidatsiyaXatosi

type Mutation {
  vazifaQosh(kirish: YangiVazifa!): VazifaQoshNatija!
}

Mijoz __typename bo'yicha ajratadi va TypeScript tiplari har holatni majburan ko'rib chiqtiradi. Qoida ko'pincha shunday: kutilgan biznes xatolari (validatsiya, "slot band") — sxemada; kutilmagan (baza ishlamayapti, ruxsat yo'q) — errors da extensions.code bilan. Kanon ikkinchisini (NOT_FOUND — extensions.code) sinagan; union uslubi — hujjat bo'yicha.

6. Subscriptions

Subscription — uchinchi kirish nuqtasi: mijoz obuna bo'ladi, server voqea bo'lganda natija yuboradi.

graphql
type Subscription {
  vazifaOzgardi(egasiId: Int!): Vazifa!
}

Transport GraphQL spetsifikatsiyasida belgilanmagan. Amalda ikki yo'l (hujjat bo'yicha, sinalmagan): WebSocket ustida graphql-ws protokoli va Server-Sent Events (GraphQL Yoga sukut bo'yicha SSE ishlatadi). Ichki tomonda — Pub/Sub: mutation voqeani nashr qiladi, subscription resolveri tinglaydi. Bir nechta server nusxasida Pub/Sub Redis orqali bo'lishi kerak (Realtime'ni masshtablash). Bu realtime bloki darslaridagi g'oyalarning GraphQL'dagi ko'rinishi — yangi muammo yo'q, yangi sintaksis bor.

7. Persisted queries

Mobil ilova har so'rovda 2 KB lik so'rov matnini yuboradi — va server har safar uni tahlil va validatsiya qiladi. Persisted queries g'oyasi: so'rov matni oldindan ma'lum, mijoz faqat uning hash'ini yuboradi.

Ikki variant (hujjat bo'yicha):

  • APQ (Automatic Persisted Queries, Apollo protokoli): mijoz avval faqat SHA-256 hash'ni yuboradi; server bilmasa — PersistedQueryNotFound, mijoz hash + matnni yuboradi, server eslab qoladi. Trafik kamayadi, xavfsizlik o'zgarmaydi — har qanday so'rov hali ham mumkin.
  • Trusted documents (ruxsat ro'yxati): build paytida frontend'dagi barcha so'rovlar yig'iladi, serverga ro'yxat beriladi; server faqat shu hash'larni bajaradi. Hujumchi o'z so'rovini yubora olmaydi — chuqurlik hujumlari ham yo'qoladi. Bu — o'z frontendingiz uchun API'da eng kuchli himoya.

Hash ko'rinishi (Web Crypto, brauzerda ham ishlaydi):

js
async function sha256(matn) {
  const b = new TextEncoder().encode(matn);
  const h = await crypto.subtle.digest("SHA-256", b);
  return [...new Uint8Array(h)]
    .map((x) => x.toString(16).padStart(2, "0"))
    .join("");
}

const sorov = "query Royxat { vazifalar { id matn } }";
const xesh = await sha256(sorov);
console.log(xesh);

const ruxsat = new Map([[xesh, sorov]]);
const begona = await sha256("{ __schema { types { name } } }");
for (const keldi of [xesh, begona]) {
  console.log(ruxsat.has(keldi) ? "bajariladi" : "rad");
}

Konsolda:

text
55ed5bf63dfd25db85d73bad15ced65e80367e4bcbd93c643bc295f83a0fd2b6
bajariladi
rad

Ikkinchisi — introspection so'rovi: ro'yxatda yo'q, rad etildi. Hash bilan so'rov GET orqali ham yuboriladi (?extensions={"persistedQuery":...}) — URL qisqa va CDN uni keshlay oladi. GraphQL'ning "HTTP kesh qiyin" degan kamchiligi shu yo'l bilan qisman yopiladi.

8. GraphQL Code Generator

Frontend tomonda sxema va so'rovlardan tiplar — GraphQL Code Generator (The Guild) ishi (hujjat bo'yicha, sinalmagan). U introspection yoki SDL'dan sxemani, loyiha fayllaridan so'rovlarni o'qiydi va har so'rov uchun aniq javob tipini yasaydi: { vazifalar: Array<{ id: number; matn: string }> } — sxemadagi hamma maydon emas, faqat so'ralganlar. Bu OpenAPI'dagi openapi-typescript dan bir qadam oldinda: tip so'rov shakliga mos. client-preset rejimida graphql(...) funksiyasi "typed document" qaytaradi, Apollo Client va urql uni tushunadi. Backend tomonda ham resolver tiplarini yasaydi — parent va args tipli bo'ladi.

9. Hujumchi nigohi

  1. Chuqur yoki keng so'rov. Sikllik sxema + katta first — serverni yiqitadi. Himoya: chuqurlik va murakkablik limiti, first ga chegara, timeout, trusted documents.
  2. Alias bilan ko'paytirish. a1: vazifa(id: 1) { ... } a2: vazifa(id: 2) { ... } — bitta so'rovda yuzta "endpoint". Login mutation'iga alias bilan 1 000 urinish — rate limit'ni chetlab o'tadi (so'rov soni bitta!). Himoya: murakkablik limitida alias'larni ham sanash, sezgir mutation'larni so'rovdagi soni bo'yicha cheklash.
  3. Batching. Ko'p serverlar bitta HTTP so'rovda massiv qabul qiladi ([{query}, {query}, ...]) — xuddi shu muammo. Himoya: kerak bo'lmasa o'chiring, kerak bo'lsa massiv uzunligini cheklang.
  4. DataLoader orqali sizish. Global (so'rovlararo) loader keshi — bir foydalanuvchining ma'lumoti boshqasiga. Himoya: loader har so'rovda yangi.
  5. Jim o'chgan himoya. Kanon topilmasi: noto'g'ri opsiya bilan berilgan qoida xatosiz e'tiborsiz qoldi. Himoya: har xavfsizlik qoidasiga "rad etilishi kerak" degan test.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
50 vazifa — 51 SQL maydon resolveri har element uchun bazaga boradi DataLoader
DataLoader natijalari aralashib ketdi batchFn natijasi kirish tartibida emas id bo'yicha Map bilan tartiblash
Boshqa foydalanuvchi ma'lumoti ko'rindi loader global yaratilgan context da har so'rov uchun
Chuqurlik limiti ishlamaydi, xato ham yo'q Yoga'da validationRules opsiyasi yo'q plugin onValidate + addValidationRule, test
first: 100000 serverni sekinlatdi ro'yxat argumentiga chegara yo'q Math.min yoki validatsiya xatosi
hasNextPage noto'g'ri aniq LIMIT first LIMIT first + 1

11. Mashqlar

Mashqlar kurs/mashqlar/29/60-graphql-chuqur/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Bir tikdagi load chaqiruvlarini yig'adigan kutubxona —
  • Kanon: 50 vazifa uchun DataLoader bilan "baza" so'rovlari —
  • Yoga'da qoida qo'shish — plugin onValidate va
  • Relay connection'da element va uning kursori —
  • Faqat ruxsat etilgan so'rov hash'larini bajarish — trusted
Yechim
  • DataLoader — batching + so'rov ichida kesh.
  • 1 — oddiy resolver bilan 50.
  • addValidationRule — validationRules opsiyasi Yoga'da yo'q.
  • edges — { node, cursor }, yonida pageInfo.
  • documents — persisted queries'ning ruxsat ro'yxati varianti.

2-mashq (o'rta): tartib va yo'q id

Darsdagi yuklovchi ni saqlab, egalarniOl ni haqiqiy bazaga o'xshatib qayta yozing: u qatorlarni teskari tartibda qaytaradi va mavjud bo'lmagan id uchun qator bermaydi ([{ id, ism }]). DataLoader qoidasi buzilmasligi uchun natijani kirish tartibiga keltiring va yo'q id uchun null qo'ying. load(3), load(1), load(9), load(2) bilan sinang.

Yechim
js
function yuklovchi(batchFn) {
  const kesh = new Map();
  let navbat = [];
  function yubor() {
    const toplam = navbat;
    navbat = [];
    batchFn(toplam.map((x) => x.id)).then((natijalar) => {
      toplam.forEach((x, i) => x.ok(natijalar[i]));
    });
  }
  return {
    load(id) {
      if (kesh.has(id)) return kesh.get(id);
      const p = new Promise((ok) => {
        navbat.push({ id, ok });
        if (navbat.length === 1) queueMicrotask(yubor);
      });
      kesh.set(id, p);
      return p;
    },
  };
}

const JADVAL = [
  { id: 1, ism: "Aziz" },
  { id: 2, ism: "Dilnoza" },
  { id: 3, ism: "Sardor" },
];

async function bazadanOl(idlar) {
  // haqiqiy baza: tartib kafolatlanmaydi, yo'q qator qaytmaydi
  return JADVAL.filter((q) => idlar.includes(q.id)).reverse();
}

async function egalarniOl(idlar) {
  const qatorlar = await bazadanOl(idlar);
  const boyicha = new Map(qatorlar.map((q) => [q.id, q.ism]));
  return idlar.map((id) => boyicha.get(id) ?? null);
}

const loader = yuklovchi(egalarniOl);
const idlar = [3, 1, 9, 2];
const natija = await Promise.all(idlar.map((id) => loader.load(id)));
console.log(natija);

Konsolda:

text
[ 'Sardor', 'Aziz', null, 'Dilnoza' ]

Map bo'lmaganda bazadanOl natijasi to'g'ridan qaytarilsa: 3-id ga Dilnoza, 1-id ga Aziz... — uzunligi ham boshqa (3 ta), DataLoader esa xato tashlaydi. Biz haqiqiy DataLoader 2.2.3 ga shunday funksiya berib ko'rdik:

text
DataLoader must be constructed with a function which accepts Array<key> and returns Promise<Array<value>>, but the function did not return a Promise of an Array of the same length as the Array of keys.

Bu — DataLoader bilan eng ko'p uchraydigan xato.

3-mashq (qiyin): alias'larni sanash

Kichik tahlilchi yozing: so'rovda nom: maydon(...) shaklidagi alias'lar bo'lishi mumkin. aliasSoni(sorov, maydon) funksiyasi berilgan maydon (masalan kirish) birinchi darajada necha marta chaqirilganini qaytarsin — alias bilan yoki alias'siz. Keyin limit qo'ying: kirish 3 martadan ko'p bo'lsa — "rad". Ichki tanlovlar ({ ... }) sanalmasin.

Yechim
js
function aliasSoni(sorov, maydon) {
  const tok = sorov.match(/[{}():,]|"[^"]*"|\w+/g);
  let chuqurlik = 0;
  let qavs = 0;
  let soni = 0;
  for (let i = 0; i < tok.length; i++) {
    const t = tok[i];
    if (t === "{") chuqurlik++;
    else if (t === "}") chuqurlik--;
    else if (t === "(") qavs++;
    else if (t === ")") qavs--;
    else if (chuqurlik === 1 && qavs === 0 && t === maydon) {
      const aliasmi = tok[i + 1] === ":";
      if (!aliasmi) soni++;
    }
  }
  return soni;
}

const hujum = "{ " + Array.from(
  { length: 5 },
  (_, i) => `a${i}: kirish(tel: "+99890", kod: "${1000 + i}") { ok }`,
).join(" ") + " }";
const oddiy = "{ kirish(tel: \"+998901\", kod: \"4821\") { ok } }";

for (const s of [oddiy, hujum]) {
  const n = aliasSoni(s, "kirish");
  console.log(n, n > 3 ? "rad" : "ok");
}

Konsolda:

text
1 ok
5 rad

Hujum so'rovi — bitta HTTP so'rov, lekin beshta login urinishi. So'rovlar sonini sanaydigan rate limiter uni bitta deb hisoblardi. aliasmi tekshiruvi teskari holat uchun: kirish: vazifa(id: 1) da kirish — alias nomi, maydon emas (undan keyin : keladi), uni sanamaymiz. Haqiqiy loyihada bu sanoq graphql paketining visitor'i bilan, validatsiya qoidasi sifatida yoziladi.

12. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

Bu darsda vazifalar-api ga kod qo'shilmaydi — loyiha REST bo'lib qoladi. Kanon N+1 (50 → 1), chuqurlik limiti (4 o'tdi, 6 rad) va xato formatini sinadi va ikki jim xatoni topdi (Yoga validationRules, visitor ajdodlari). DataLoader g'oyasi esa GraphQL'dan tashqarida ham foydali: Bildirishnoma tizimi kabi joylarda ko'p getById chaqiruvlarini yig'ish uchun. NestJS GraphQL moduli (30-qism) — bu darsdagi naqshlarni loyihada qo'llash joyi.

O'zingiz sinab ko'ring: oldingi darsdagi Yoga serveriga Foydalanuvchi.vazifalar maydonini qo'shing, chuqurlik limitini addValidationRule bilan ulang va 6 qavatli so'rov rad etilishini test bilan tasdiqlang. Keyin limitni validationRules opsiyasi bilan berib ko'ring — test yiqilishini o'z ko'zingiz bilan ko'ring.

13. Real ishda

  • Katta GraphQL API'lar (GitHub, Shopify) — so'rov narxi (cost) hisoblanadi va javobda ko'rsatiladi; limit soatiga "ochko" bilan.
  • Mobil ilovalar — trusted documents bilan: serverga faqat ilova build'idagi so'rovlar.
  • Apollo Federation / supergraph — DataLoader har servis ichida, so'rov rejasi esa router'da.
  • Code generation — frontend'da GraphQL Code Generator yoki Relay compiler odatiy qadam.
  • Intervyuda: "GraphQL'da N+1 qanday hal qilinadi?", "DataLoader qanday ishlaydi?", "GraphQL API'ni DoS'dan qanday himoya qilasiz?", "Relay connection nima?"

Xulosa

  • N+1: har elementga resolver; DataLoader bir tikdagi load larni bitta to'plamga yig'adi va so'rov ichida keshlaydi (kanon: 50 → 1).
  • batchFn — kirish tartibi va uzunligida natija; loader — har so'rov uchun yangi.
  • Chuqurlik va murakkablik limiti — validatsiya bosqichida; Yoga'da addValidationRule, validationRules jim e'tiborsiz qoladi.
  • Relay connections: edges { node cursor }, pageInfo { hasNextPage endCursor }, ichida keyset.
  • Kutilgan xatolar — sxemada (union), kutilmaganlar — errors da; subscriptions — WebSocket yoki SSE + Pub/Sub.
  • Persisted queries: APQ — trafik uchun, trusted documents — xavfsizlik uchun.

Keyingi dars: tRPC — bitta TypeScript monorepo'da sxemasiz, to'liq tipli API: router, procedure, Zod input va middleware.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!