Learning
GraphQL

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éterLeírás
parentA szülő objektum (az eggyel feljebb lévő resolver visszatérési értéke)
argsA lekérdezésben átadott argumentumok
contextMegosztott kontextus (DB kapcsolat, auth, loaderek)
infoA 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és

A 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.

On this page