Mundarija (23)
- Bu darsda
- 1. Nega bu kerak?
- 2. N+1 va DataLoader
- 2.1 Muammo: kanon o'lchovi
- 2.2 DataLoader qanday ishlaydi
- 3. Chuqurlik va murakkablik limiti
- 3.1 Chuqurlik
- 3.2 Murakkablik
- 4. Cursor sahifalash: Relay connections
- 5. Xatolar: ikki maktab
- 6. Subscriptions
- 7. Persisted queries
- 8. GraphQL Code Generator
- 9. Hujumchi nigohi
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): tartib va yo'q id
- 3-mashq (qiyin): alias'larni sanash
- 12. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi
- 13. Real ishda
- Xulosa
- Manbalar
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 bittaWHERE id IN (...)ga yig'adi. Mijoz so'rov shaklini o'zi tanlagani uchun server chuqurlik va murakkablik limiti qo'yishi shart. Sahifalash — Relayedges/pageInfoshaklidagi 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:
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:
- 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, bittabatchFn([id1, id2, ...])chaqiradi. - 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 bajarildiUchinchi load(1) bazaga umuman bormadi — u birinchisining Promise'ini oldi. O'zimiz yozamiz:
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:
SELECT ... WHERE id IN (1, 2, 3)
Aziz, Dilnoza, Aziz, Sardor, Dilnoza, Aziz
so'rovlar: 1Olti load — bitta SQL, uchta noyob id bilan. Takrorlangan id'lar keshdan keldi. Bu yerda ikki muhim qoida yashiringan — haqiqiy DataLoader ham ularni talab qiladi:
batchFnnatijasi kirish tartibida va kirish uzunligida bo'lishi kerak. SQLWHERE id IN (3, 1, 2)qatorlarni ixtiyoriy tartibda qaytaradi — ularniMapbilan id bo'yicha qayta tartiblash sizning ishingiz. Topilmagan id uchun —nullyokiError.- Loader har so'rov uchun yangi yaratiladi (
contextichida). 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:
const yoga = createYoga({
schema,
context: () => ({ egaLoader: new DataLoader(egalarniOl) }),
});
// resolver:
// Vazifa.egasi: (v, _a, ctx) => ctx.egaLoader.load(v.egasiId)Tekshirib ko'ring:
egasiresolveriawaitbilan boshqa narsani kutib, keyinloadqilsa (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:
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):
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:
- 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'ningonValidatehook'i vaaddValidationRule. (Apollo Server'da esavalidationRulesopsiyasi bor — shuning uchun maqolalardagi kod ko'chirilganda adashish oson.) - 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:
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:
40 ok
200 ok
2002000 RAD ETILDIUchinchi 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):
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):
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:
[ 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):
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.
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):
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:
55ed5bf63dfd25db85d73bad15ced65e80367e4bcbd93c643bc295f83a0fd2b6
bajariladi
radIkkinchisi — 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
- Chuqur yoki keng so'rov. Sikllik sxema + katta
first— serverni yiqitadi. Himoya: chuqurlik va murakkablik limiti,firstga chegara, timeout, trusted documents. - 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. - 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. - DataLoader orqali sizish. Global (so'rovlararo) loader keshi — bir foydalanuvchining ma'lumoti boshqasiga. Himoya: loader har so'rovda yangi.
- 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
loadchaqiruvlarini yig'adigan kutubxona — - Kanon: 50 vazifa uchun DataLoader bilan "baza" so'rovlari —
- Yoga'da qoida qo'shish — plugin
onValidateva - 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 —
validationRulesopsiyasi Yoga'da yo'q. - edges —
{ node, cursor }, yonidapageInfo. - 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
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:
[ '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:
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
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:
1 ok
5 radHujum 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
loadlarni 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,validationRulesjim e'tiborsiz qoladi. - Relay connections:
edges { node cursor },pageInfo { hasNextPage endCursor }, ichida keyset. - Kutilgan xatolar — sxemada (union), kutilmaganlar —
errorsda; 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
- DataLoader: https://github.com/graphql/dataloader
- GraphQL Yoga — validatsiya va plugin'lar (
onValidate): https://the-guild.dev/graphql/yoga-server/docs/features/envelop-plugins - GraphQL Cursor Connections spetsifikatsiyasi: https://relay.dev/graphql/connections.htm
- GraphQL — xavfsizlik bo'yicha qo'llanma: https://graphql.org/learn/security/
- Apollo — Automatic Persisted Queries: https://www.apollographql.com/docs/apollo-server/performance/apq
- GraphQL Code Generator: https://the-guild.dev/graphql/codegen
- graphql-ws: https://github.com/enisdenjo/graphql-ws
- Kanon tajribasi (IlmHamroh, 2026-10-27): GraphQL Yoga 5.24.6, graphql 17.0.2, DataLoader 2.2.3, Node 24.21.0. Relay, subscriptions, persisted queries, Code Generator — sinalmagan. JS bloklari va mashq yechimlari — biz ishga tushirdik (2026-10-28, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!