DEV Community

Daniel Santos
Daniel Santos

Posted on

Porquê e como utilizar Dataloaders

Por muitas vezes, fontes externas de dados podem ser o caminho crítico para o seu serviço, e é essencial assegurar-se de não o percorrer mais vezes que o necessário, uma vez que tempo e dinheiro andam lado a lado no que diz respeito ao desenvolvimento de software.

O cenário

Imagine uma aplicação utilizando GraphQL, onde é possível que um usuário poste diversas publicações, cada postagem possui o id do seu autor, e a partir desse id podemos buscar informações adicionais sobre o mesmo.

graphql schema

Como descrito, no resolver do atributo author seria feito o relacionamento da outra entidade.

author: {
  type: User,
  resolve: async (obj) => {
    const author =
      await prisma.$queryRaw`SELECT * FROM User WHERE id = ${obj.authorId}`;
    return author ? author[0] : null;
  },
},
Enter fullscreen mode Exit fullscreen mode

Para acessar as postagens e o seu autor, teremos uma query como essa:

query allPosts {
   getPosts {
    title
    author {
      name
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
getPosts: {
  type: new GraphQLList(Post),
  resolve: async () => await prisma.post.findMany(),
},
Enter fullscreen mode Exit fullscreen mode

Num cenário em que existem apenas 3 postagens, todas do mesmo autor, executando a query e registrando os logs do sistema no console, teríamos um resultado como esse:

prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId` FROM `main`.`Post` WHERE 1=1 LIMIT ? OFFSET ?
prisma:query SELECT * FROM User WHERE id = ?
prisma:query SELECT * FROM User WHERE id = ?
prisma:query SELECT * FROM User WHERE id = ?
Enter fullscreen mode Exit fullscreen mode

N + 1 problem

O ideal seria resolver essa query do GraphQL fazendo apenas duas consultas no banco de dados, uma para as postagens, e outra para cada um dos autores distintos. O que não é o caso aqui, apesar de termos apenas um usuário, ainda realizamos a mesma busca pelos seus dados 3 vezes.

O exemplo acima configura o n+1 problem, isto é, fazemos várias execuções desnecessárias, mesmo que os dados que desejávamos a princípio já tenham sido retornados, esse problema não é exclusivo do GraphQL, nem mesmo ocorre somente em acessos a bancos de dados.

Dataloader

Existem algumas formas de resolução para esse problema, a mais comum é utilizando o Dataloader, um utilitário criado a partir de uma das APIs internas do Facebook. Com o Dataloader, estratégias de batching e caching podem ser facilmente adicionadas à aplicação.

Analisando as consultas feitas ao banco de dados, podemos notar que o ponto de deficiência se encontra no resolver do atributo author, logo, precisamos inserir o Dataloader nessa função, substituindo a chamada direta ao banco de dados.

Para criar um loader, precisamos passar uma função que aceite um array de keys, e retorne um array de respostas com o mesmo tamanho. Podemos executar todas as consultas ao mesmo tempo através do método Promise.allSettled, caso alguma Promise seja rejeitada, podemos substituir o valor do erro por null, para que o array retornado seja do mesmo tamanho do array de keys fornecido.

Colocando em prática, teremos algo semelhante a isso:

const batchFn: BatchLoadFn<number, unknown> = async (keys) => {
  const promises = keys.map(
    (k) => prisma.$queryRaw`SELECT * FROM User WHERE id = ${k}`
  );
  const response = await Promise.allSettled(promises);
  return response.map((r) => (r.status === "rejected" ? null : r.value));
};

export const userDataloader = new DataLoader(batchFn);
Enter fullscreen mode Exit fullscreen mode

Com o loader pronto, podemos utilizá-lo no resolver, como cada postagem possui apenas um autor, utilizamos o método .load, passando o id do autor como parâmetro.

author: {
  type: User,
  resolve: async (obj) => {
    const author = await userDataloader.load(obj.authorId);

    return author ? author[0] : null;
  },
},
Enter fullscreen mode Exit fullscreen mode

Assim, a primeira chamada a um determinado autor irá realizar a busca no banco de dados e adicionar estes dados ao cache, chamadas subsequentes irão consumir deste cache criado.

Executando a query novamente, com o Dataloader implementado, temos como resultado os seguintes logs:

prisma:query SELECT `main`.`Post`.`id`, `main`.`Post`.`title`, `main`.`Post`.`content`, `main`.`Post`.`published`, `main`.`Post`.`authorId` FROM `main`.`Post` WHERE 1=1 LIMIT ? OFFSET ?
prisma:query SELECT * FROM User WHERE id = ?
Enter fullscreen mode Exit fullscreen mode

Fomos de 4 consultas no banco de dados para 2, imagine se o usuário tivesse dezenas ou centenas de posts, com certeza a diferença seria ainda mais significativa.

É importante destacar, que caso os dados na sua fonte sofram alguma alteração, o cache do Dataloader não terá conhecimento dessa mudança. Portanto, você precisa remover esse dado específico através do método .clear(), e assim, chamadas futuras ao método .load() irão atualizar o cache.

// mutation
changeOneUser: {
    type: User,
    args: {
      id: {
        type: GraphQLInt,
      },
      name: {
        type: GraphQLString,
      },
    },
    resolve: async (_, { id, name }) => {
      const data = await prisma.user.update({
        where: {
          id,
        },
        data: {
          name,
        },
      });

      userDataloader.clear(id);

      return data;
    },
  },
Enter fullscreen mode Exit fullscreen mode

Finalizando

Apesar de não fazer parte da ideia inicial deste texto, é preciso destacar aqui o Prisma já possui uma solução para este problema através do método .findUnique, optei por não utilizá-lo aqui para demonstrar como seria realizada uma implementação manual através da biblioteca em questão.

Outro ponto relevante a ser levantado, é de que o Dataloader pode ser usado também com outras linguagens além do Javascript, como Java, Go, Python e por aí vai.

E por fim, você pode acessar o restante do código através deste repositório: dataloader-example

Top comments (0)