这是 GraphQL 的一个缺点:解决 GraphQL 中的 N + 1 查询问题

GraphQL 是如今前端领域中非常热门的技术,这是一种新兴的 API 语言,它具有较为强大的查询能力,一个 GraphQL 查询可以获取多个资源,这就是 GraphQL 所具有的天然优势。但是也正因此,它也存在一个问题:N+1 查询问题。

什么是 N+1 查询问题?

当我们使用 GraphQL 查询一个数据集合的时候,我们可能会遇到需要与每条 data 关联的其他数据,例如:

{
  allUsers {
    id
    name
    posts {
      id
      title
      comments {
        id
        content
      }
    }
  }
}

以上例子使用 GraphQL 查询所有用户,并查询他们的所有文章,还有每篇文章的评论。这种查询方式将查询数据分散成了多次查询,会导致 N+1 查询问题。

我们可以发现,在上面的查询中,首先会进行一次查询获取所有的用户,再进行 N 次查询获取每个用户的 posts 以及每篇文章的 comments。因此会进行 n+1 次查询,其中 n 表示 user 的数量。这种查询方式会造成后端系统压力上升,可能导致巨大的性能问题。

为了解决 GraphQL 中的 N+1 查询问题,我们可以使用 DataLoader 这个工具来进行优化。

DataLoader 是一个用于批量查询数据的 JavaScript 组件。它可以使用 batching 和 caching 来优化数据查询,从而避免 N+1 查询问题。

安装和配置 DataLoader

在使用 DataLoader 之前,需要先安装它。我们可以使用 npm 来安装 DataLoader:

npm install dataloader

接着,我们需要在 GraphQL 中进行 DataLoader 的配置。处理 GraphQL 请求的每个请求都应该有自己的 DataLoader 实例,这个实例应该是通过请求的上下文创建的。实例如下:

const { createServer } = require("http");
const { execute, subscribe } = require("graphql");
const { SubscriptionServer } = require("subscriptions-transport-ws");
const DataLoader = require("dataloader");

const schema = require("your-graphql-schema");

const dataLoader = new DataLoader(async (keys) => {
  const results = await Promise.all(
    keys.map((id) => getBatchedData(id))
  );
  return results;
});

const context = {
  dataLoader,
};

const server = createServer((req, res) => {
  // ...
});

SubscriptionServer.create(
  {
    schema,
    execute,
    subscribe,
    onConnect: (connectionParams) => {
      return { dataLoader };
    },
  },
  { server, path: "/" }
);

server.listen(8080, () =>
  console.log("Server running, open your browser on http://localhost:8080/")
);

在上述代码中,我们首先要创建一个 DataLoader 实例 dataLoader,然后定义一个上下文对象 context,并赋值 dataLoader

使用 DataLoader 解决 N+1 查询问题

现在我们可以使用 DataLoader 来处理 GraphQL 中的 N+1 查询问题了。我们可以将每个具有相同 resolver 的 GraphQL 查询,以一个 batch 的形式,统一查询到同一个 DataLoader 里。这样就可以批量查询数据,而不是进行 N+1 查询,在数据量较大时能极大地提升性能。以下是实例代码:

const resolvePosts = async (users, _, { dataLoader }) => {
  const postIds = users.map(({ id }) => id);
  const posts = await dataLoader.loadMany(postIds);
  const postsByUserId = {};

  posts.forEach((post) => {
    if (!postsByUserId[post.userId]) {
      postsByUserId[post.userId] = [post];
    } else {
      postsByUserId[post.userId].push(post);
    }
  });

  return users.map((user) => ({
    ...user,
    posts: postsByUserId[user.id] || [],
  }));
};

const resolveComments = async (posts, _, { dataLoader }) => {
  const commentIds = posts.reduce((acc, { comments }) => {
    acc.push(...comments);
    return acc;
  }, []);

  const comments = await dataLoader.loadMany(commentIds);
  const commentsByPostId = {};

  comments.forEach((comment) => {
    if (!commentsByPostId[comment.postId]) {
      commentsByPostId[comment.postId] = [comment];
    } else {
      commentsByPostId[comment.postId].push(comment);
    }
  });

  return posts.map((post) => ({
    ...post,
    comments: commentsByPostId[post.id] || [],
  }));
};

在上述代码中,首先我们需要定义一个 resolvePosts 函数来查询所有的 post,并根据 userId 来将其分组。再定义一个函数 resolveComments 来查询所有的 comments,并根据 postId 来分组。这两个函数都会接受一个 dataLoader 的参数,给定将多个查询合并为一个查询的能力,从而帮助我们进行批处理操作。

总结

以上就是如何使用 DataLoader 工具解决 GraphQL 查询中的 N+1 查询问题的指导。掌握 N+1 查询问题的解决方法非常重要,因为它可以极大地提升 GraphQL 的性能,减轻后端服务器的压力。如果您正在使用 GraphQL,那么 DataLoader 工具会是您不可或缺的好伙伴。

参考资料:https://graphql.cn/code/#/code/batching-2

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/6591c88feb4cecbf2d6cb95f


纠错
反馈