🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Risolutori di Query in GraphQL: Gestione delle Operazioni di Lettura

Codegrind Team•Sep 03 2024

In GraphQL, i risolutori di query gestiscono la logica per ottenere i dati richiesti dai client. Le query sono operazioni di lettura che consentono ai client di richiedere esattamente i dati di cui hanno bisogno, il che rende i risolutori di query fondamentali per il funzionamento delle API GraphQL. In questo articolo esploreremo come implementare risolutori di query, come ottimizzare le prestazioni e quali best practices seguire per garantire efficienza e sicurezza nelle operazioni di lettura.

Cos’è un Risolutore di Query?

Un risolutore di query è una funzione associata a un campo in una query GraphQL che si occupa di recuperare i dati richiesti dal client. Quando un client invia una query, il risolutore determina come ottenere i dati dal database o da altre fonti.

Ogni campo nello schema GraphQL può avere il proprio risolutore, che viene eseguito quando quel campo è richiesto in una query. Se non viene fornito un risolutore esplicito per un campo, GraphQL utilizza un risolutore predefinito che restituisce il valore corrispondente nell’oggetto.

Esempio di Schema di Base

type User {
  id: ID!
  name: String!
  email: String!
}

type Query {
  user(id: ID!): User
  users: [User!]!
}

In questo schema, abbiamo una query user che accetta un parametro id e restituisce un singolo utente, e una query users che restituisce una lista di tutti gli utenti.

Come Funzionano i Risolutori di Query

I risolutori di query ricevono quattro argomenti principali:

  1. parent: L’oggetto genitore, che è il risultato del risolutore del livello superiore.
  2. args: Gli argomenti passati alla query dal client (ad esempio, id in una query user(id: ID!)).
  3. context: Un oggetto condiviso tra tutti i risolutori, utilizzato per accedere a informazioni globali come l’utente autenticato o i database.
  4. info: Informazioni sull’esecuzione della query, come lo schema e il percorso della query.

Struttura di Base di un Risolutore di Query

const resolvers = {
  Query: {
    user: async (parent, args, context, info) => {
      const user = await context.db.getUserById(args.id);
      return user;
    },
    users: async (parent, args, context, info) => {
      const users = await context.db.getAllUsers();
      return users;
    },
  },
};

In questo esempio:

  • La query user accetta un argomento id e utilizza il metodo getUserById del database per recuperare un singolo utente.
  • La query users non accetta argomenti e restituisce un elenco di tutti gli utenti con getAllUsers.

Implementare Risolutori di Query

1. Recuperare Dati con Argomenti

Quando una query richiede un singolo elemento, come un utente specifico o un post, l’argomento passato dal client viene utilizzato per filtrare i dati.

Esempio: Recupero di un Utente con un ID

const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      const user = await context.db.getUserById(id);
      if (!user) {
        throw new Error("Utente non trovato");
      }
      return user;
    },
  },
};

In questo esempio, il risolutore della query user accetta l’argomento id, esegue una query nel database e restituisce l’utente corrispondente. Se l’utente non viene trovato, viene generato un errore.

2. Restituire una Lista di Elementi

Molte query GraphQL richiedono elenchi di elementi, come un elenco di utenti o di post. Il risolutore può semplicemente restituire un array di risultati.

Esempio: Restituzione di Tutti gli Utenti

const resolvers = {
  Query: {
    users: async (parent, args, context) => {
      return await context.db.getAllUsers();
    },
  },
};

Questo risolutore restituisce un array di utenti recuperato dal database utilizzando il metodo getAllUsers.

3. Filtrare Dati con Argomenti

Spesso le query richiedono funzionalitĂ  di filtro, come il recupero di elementi in base a criteri specifici.

Esempio: Filtro degli Utenti per Nome

const resolvers = {
  Query: {
    users: async (parent, { name }, context) => {
      return await context.db.getUsersByName(name);
    },
  },
};

Qui, la query users accetta un argomento name e restituisce tutti gli utenti il cui nome corrisponde a quello fornito dal client.

4. Gestione delle Relazioni

In GraphQL, è comune che un tipo contenga relazioni con altri tipi. Ad esempio, un Post potrebbe avere un campo author che rappresenta l’autore del post. I risolutori di query possono essere utilizzati per gestire queste relazioni.

Esempio: Recuperare l’Autore di un Post

const resolvers = {
  Query: {
    post: async (parent, { id }, context) => {
      return await context.db.getPostById(id);
    },
  },
  Post: {
    author: async (post, args, context) => {
      return await context.db.getUserById(post.authorId);
    },
  },
};

In questo esempio:

  • La query post restituisce un post in base al suo id.
  • Il risolutore di campo author nel tipo Post recupera l’autore del post utilizzando il campo authorId presente nell’oggetto Post.

Migliorare le Performance dei Risolutori di Query

1. Evitare il Problema N+1 con DataLoader

Il problema N+1 si verifica quando una query che richiede dati correlati esegue troppe chiamate al database, ad esempio, recuperando prima un elenco di post e poi eseguendo una query separata per ciascun autore. Questo può causare un sovraccarico di richieste.

Per risolvere questo problema, puoi usare DataLoader per batchare e memorizzare in cache le richieste di dati correlati.

Esempio con DataLoader

const DataLoader = require("dataloader");

// Crea un DataLoader per batchare le richieste degli autori
const userLoader = new DataLoader(async (userIds) => {
  const users = await context.db.getUsersByIds(userIds);
  return userIds.map((id) => users.find((user) => user.id === id));
});

const resolvers = {
  Query: {
    posts: async (parent, args, context) => {
      return await context.db.getAllPosts();
    },
  },
  Post: {
    author: (post, args, context) => {
      return userLoader.load(post.authorId);
    },
  },
};

2. Caching per Migliorare la VelocitĂ 

Se i dati richiesti sono costanti o cambiano raramente, puoi implementare il caching per ridurre i tempi di risposta. Puoi usare strumenti come Redis per memorizzare in cache i risultati delle query e servire i dati piĂą velocemente senza doverli recuperare dal database ogni volta.

Esempio di Caching con Redis

const redis = require("redis");
const client = redis.createClient();

const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      const cacheKey = `user:${id}`;
      const cachedUser = await client.get(cacheKey);

      if (cachedUser) {
        return JSON.parse(cachedUser);
      }

      const user = await context.db.getUserById(id);
      if (user) {
        client.set(cacheKey, JSON.stringify(user), "EX", 3600); // Cache per 1 ora
      }

      return user;
    },
  },
};

In questo esempio, il risolutore verifica prima se i dati sono presenti nella cache Redis. Se lo sono, li restituisce immediatamente; altrimenti, esegue una query nel database e memorizza il risultato in cache.

3. Ottimizzare le Query SQL

Se utilizzi un database relazionale, è importante ottimizzare le query SQL nei risolutori per ridurre i tempi di esecuzione e migliorare

le prestazioni. Usa join efficienti, indici appropriati e limitazioni nel numero di righe restituite per migliorare la velocitĂ  delle operazioni di lettura.

Esempio di Ottimizzazione SQL

const resolvers = {
  Query: {
    posts: async (parent, args, context) => {
      return await context.db.query(`
        SELECT posts.*, users.name AS authorName
        FROM posts
        JOIN users ON posts.authorId = users.id
      `);
    },
  },
};

Best Practices per Risolutori di Query

1. Gestione degli Errori

Gestisci sempre gli errori nei risolutori di query. Se un dato non viene trovato o se c’è un problema nel recupero, restituisci un messaggio di errore significativo.

const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      try {
        const user = await context.db.getUserById(id);
        if (!user) {
          throw new Error("Utente non trovato");
        }
        return user;
      } catch (error) {
        throw new Error(
          `Errore durante il recupero dell'utente: ${error.message}`
        );
      }
    },
  },
};

2. Limitare i Risultati

Per query che possono restituire molti risultati, implementa la paginazione o un limite sui risultati per evitare di sovraccaricare il server o il database.

Esempio di Paginazione

const resolvers = {
  Query: {
    posts: async (parent, { limit, offset }, context) => {
      return await context.db.getPosts({ limit, offset });
    },
  },
};

3. Protezione delle Query

Limita la complessitĂ  delle query che i client possono eseguire per evitare che query troppo nidificate o complesse rallentino il server.

Puoi utilizzare strumenti come graphql-query-complexity per impostare limiti di complessitĂ  nelle query.

Conclusione

I risolutori di query in GraphQL sono il cuore delle operazioni di lettura, permettendo di recuperare dati in modo efficiente e sicuro. Implementare risolutori di query efficaci richiede una buona gestione degli argomenti, ottimizzazione delle query SQL, e tecniche avanzate come il batching con DataLoader e il caching. Seguendo le best practices, come la gestione degli errori e la paginazione, puoi garantire che le tue API GraphQL siano scalabili e performanti, offrendo un’esperienza utente ottimale.