Resolverek működése
Resolver lánc, async működés, N+1 probléma és DataLoader használata.
Resolverek működése
A resolver az a függvény, amely egy GraphQL field értékét visszaadja. A GraphQL motor minden requestnél felépíti a resolver-fát, és egymás után hívja meg az egyes resolvereket.
A resolver szignatúrája
fieldName: (parent, args, context, info) => returnValue| Paraméter | Leírás |
|---|---|
parent | A szülő objektum (az eggyel feljebb lévő resolver visszatérési értéke) |
args | A lekérdezésben átadott argumentumok |
context | Megosztott kontextus (DB kapcsolat, auth, loaderek) |
info | A lekérdezés AST-je és egyéb metaadatok (ritkán szükséges) |
Resolver láncolat
A resolverek hierarchikusan futnak. Vegyük ezt a lekérdezést:
query {
user(id: "1") {
name
posts {
title
}
}
}A végrehajtási lánc:
1. Query.user(_, { id: "1" }, context)
→ visszaadja: { id: "1", name: "Kovács Péter" }
2. User.name({ id: "1", name: "Kovács Péter" }, _, context)
→ visszaadja: "Kovács Péter"
(alapértelmezett resolver, nem kell implementálni)
3. User.posts({ id: "1", name: "Kovács Péter" }, _, context)
→ visszaadja: [{ id: "10", title: "Első bejegyzés" }, ...]
4. Post.title({ id: "10", title: "Első bejegyzés" }, _, context)
→ visszaadja: "Első bejegyzés"
(alapértelmezett resolver)Async resolverek
A resolverek lehetnek aszinkronok – adatbázis-lekérdezésekhez ez szinte mindig szükséges:
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
const user = await db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return user.rows[0];
},
posts: async (_, { filter, pagination }, { db }) => {
const { page = 1, limit = 20 } = pagination ?? {};
const offset = (page - 1) * limit;
const result = await db.query(
'SELECT * FROM posts WHERE status = $1 LIMIT $2 OFFSET $3',
[filter?.status ?? 'PUBLISHED', limit, offset]
);
return result.rows;
},
},
User: {
posts: async (parent, _, { db }) => {
return db
.query('SELECT * FROM posts WHERE author_id = $1', [parent.id])
.then(r => r.rows);
},
},
};N+1 probléma és DataLoader
A legnagyobb teljesítményprobléma GraphQL-ben az N+1 lekérdezési probléma. Ha 10 bejegyzés szerzőjét kéred le, a naiv implementáció 10 külön adatbázis-lekérdezést indít:
Lekérdezés: posts(limit: 10) { author { name } }
→ SELECT * FROM posts LIMIT 10
→ SELECT * FROM users WHERE id = 1
→ SELECT * FROM users WHERE id = 2
→ SELECT * FROM users WHERE id = 3
...összesen 11 lekérdezésA megoldás a DataLoader: összegyűjti a kéréseket, és egyetlen lekérdezésben oldja meg őket.
const DataLoader = require('dataloader');
// DataLoader létrehozása
const userLoader = new DataLoader(async (ids) => {
const users = await db.query(
'SELECT * FROM users WHERE id = ANY($1)',
[ids]
);
// Fontos: az eredményt az id-k sorrendjében kell visszaadni!
return ids.map(id => users.rows.find(u => u.id === id));
});
// Resolver használatban
const resolvers = {
Post: {
author: (parent, _, { loaders }) => {
return loaders.user.load(parent.authorId);
},
},
};
// Context-ben átadjuk a loadereket
const context = ({ req }) => ({
db,
loaders: {
user: new DataLoader(batchUsersById),
},
});Eredmény: A 10 bejegyzés szerzőinek lekérdezése egyetlen SQL-lekérdezéssel megvalósul:
SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)Rövid összefoglaló
- A resolver négy paramétert kap:
parent,args,context,info. - A resolverek hierarchikusan futnak, egymásra épülve.
- A resolverek lehetnek aszinkronok – adatbázis-lekérdezésekhez ez szükséges.
- Az N+1 probléma teljesítményromlást okoz nested lekérdezéseknél.
- A DataLoader megoldja az N+1 problémát batch-eléssel és cache-eléssel.