🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Risolutori Asincroni in GraphQL: Migliorare la Gestione dei Dati con Funzioni Asincrone

Codegrind TeamSep 03 2024

I risolutori asincroni sono una caratteristica fondamentale in GraphQL, che ti permette di gestire richieste di dati da fonti esterne, come database o API, in modo efficiente e non bloccante. Poiché molte operazioni coinvolte nei risolutori (come l’accesso al database o le chiamate HTTP) sono operazioni di I/O, l’uso di risolutori asincroni consente di migliorare le prestazioni e ridurre i tempi di risposta. In questo articolo esploreremo come funzionano i risolutori asincroni, come implementarli in un’API GraphQL e quali vantaggi offrono.

Cos’è un Risolutore Asincrono?

In GraphQL, un risolutore è una funzione che recupera i dati per un campo specifico di una query. Quando si utilizzano dati da fonti esterne, come database o API, queste operazioni possono essere eseguite in modo asincrono per evitare che il server si blocchi mentre attende la risposta. I risolutori asincroni utilizzano il meccanismo di promesse o la sintassi async/await di JavaScript per gestire queste operazioni di I/O in modo non bloccante.

Esempio di Risolutore Semplice

In un risolutore sincrono, una funzione potrebbe restituire immediatamente un valore:

const resolvers = {
  Query: {
    hello: () => {
      return "Hello, world!";
    },
  },
};

Ma se il risolutore deve eseguire una chiamata al database, è necessario renderlo asincrono:

const resolvers = {
  Query: {
    hello: async () => {
      const result = await someDatabaseCall();
      return result;
    },
  },
};

Vantaggi dei Risolutori Asincroni

  • Non bloccanti: I risolutori asincroni permettono al server di gestire altre richieste mentre attende che i dati vengano recuperati, migliorando l’efficienza generale.
  • Facilità di Gestione delle Operazioni di I/O: Utilizzando async/await, è possibile gestire in modo pulito e leggibile operazioni asincrone, come chiamate API o accessi a database.
  • Supporto per Operazioni Complesse: Se una query richiede più operazioni che dipendono da risorse esterne, i risolutori asincroni aiutano a orchestrare queste operazioni in parallelo o in sequenza.

Implementazione di Risolutori Asincroni

1. Usare Async/Await in un Risolutore

L’approccio più comune per implementare un risolutore asincrono è utilizzare la sintassi async/await, che semplifica la gestione delle promesse.

Esempio con Accesso al Database

Supponiamo di avere un’API GraphQL che restituisce i dettagli di un utente da un database. Ecco come implementare un risolutore asincrono per questa operazione:

const resolvers = {
  Query: {
    user: async (parent, args, context) => {
      // Chiamata asincrona al database per ottenere l'utente
      const user = await context.db.getUserById(args.id);
      return user;
    },
  },
};

In questo esempio:

  • async: La funzione è dichiarata asincrona, il che significa che può usare await.
  • await: Si attende che la chiamata a context.db.getUserById completi prima di restituire il risultato.

2. Gestione di Più Operazioni Asincrone

In alcune situazioni, potresti dover eseguire più operazioni asincrone all’interno dello stesso risolutore. Un esempio è il recupero di dati correlati da fonti diverse.

Esempio: Recupero di Post e Autore

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

Qui, prima si recupera il post e poi, utilizzando post.authorId, si recuperano i dettagli dell’autore in modo asincrono.

3. Esecuzione di Operazioni in Parallelo

Se hai operazioni asincrone indipendenti tra loro, puoi eseguirle in parallelo usando Promise.all, riducendo i tempi di attesa complessivi.

Esempio: Recupero di Utenti e Post in Parallelo

const resolvers = {
  Query: {
    dashboard: async (parent, args, context) => {
      const [users, posts] = await Promise.all([
        context.db.getAllUsers(),
        context.db.getAllPosts(),
      ]);
      return { users, posts };
    },
  },
};

In questo caso, entrambe le operazioni vengono eseguite contemporaneamente, migliorando l’efficienza del risolutore.

4. Gestione degli Errori nei Risolutori Asincroni

Quando si utilizzano risolutori asincroni, è importante gestire correttamente gli errori che possono verificarsi durante le operazioni asincrone, come errori di rete o problemi di accesso al database.

Gestione degli Errori con Try/Catch

Puoi usare try/catch per gestire gli errori nei risolutori asincroni e restituire messaggi di errore significativi al client.

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

5. Ottimizzazione delle Chiamate Asincrone

L’uso di risolutori asincroni può introdurre inefficienze se non viene gestito correttamente, ad esempio eseguendo query ridondanti. DataLoader è uno strumento utile per evitare il problema N+1, batchando e memorizzando in cache le chiamate al database.

Esempio con DataLoader

const DataLoader = require("dataloader");

// Crea un DataLoader per gli utenti
const userLoader = new DataLoader(async (userIds) => {
  const users = await db.getUsersByIds(userIds);
  return userIds.map((id) => users.find((user) => user.id === id));
});

const resolvers = {
  Query: {
    user: (parent, args) => userLoader.load(args.id),
  },
};

In questo modo, se una query richiede più utenti, le richieste saranno batchate in una singola operazione anziché eseguire una query separata per ciascun utente.

Risolutori Asincroni e Sottoscrizioni

I risolutori asincroni non sono limitati solo alle query e alle mutazioni. Anche le sottoscrizioni (subscriptions) in GraphQL possono beneficiare di risolutori asincroni per gestire operazioni in tempo reale.

Esempio di Subscription Asincrona

const { PubSub } = require("apollo-server");
const pubsub = new PubSub();

const resolvers = {
  Subscription: {
    newMessage: {
      subscribe: async (parent, args, context) => {
        return pubsub.asyncIterator("NEW_MESSAGE");
      },
    },
  },
};

In questo esempio, il risolutore subscribe è asincrono e utilizza il sistema di pubblicazione/sottoscrizione per inviare aggiornamenti in tempo reale al client.

Conclusione

I risolutori asincroni in GraphQL sono essenziali per migliorare l’efficienza e le prestazioni delle API, specialmente quando si lavora con fonti di dati esterne come database e API. Utilizzando la sintassi async/await, è possibile gestire facilmente operazioni asincrone e ridurre i tempi di risposta del server, consentendo una gestione efficiente delle risorse. Inoltre, strumenti come DataLoader e tecniche come il batching e l’esecuzione in parallelo possono ottimizzare ulteriormente le prestazioni dei risolutori.