Porquê e como utilizar Dataloaders
Tags: api, typescript
Published: 2022-10-19 17:52:24

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0hjdz3w0rp8i4l4rxko.png)

Como descrito, no _resolver_ do atributo _author_ seria feito o relacionamento da outra entidade.

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

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

```gql
query allPosts {
   getPosts {
    title
    author {
      name
    }
  }
}
```

```ts
getPosts: {
  type: new GraphQLList(Post),
  resolve: async () => await prisma.post.findMany(),
},
```

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 = ?
```

## 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](https://github.com/graphql/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:
```ts
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);
```

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.

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

    return author ? author[0] : null;
  },
},
```

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 = ?
```

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.

```ts
// 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;
    },
  },
```

## 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``](https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance#solving-the-n1-problem), 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](https://github.com/graphql/dataloader#other-implementations).

E por fim, você pode acessar o restante do código através deste repositório: [dataloader-example](https://github.com/ddanielsantos/dataloader-example)