Graphql中的N+1问题

这篇具有很好参考价值的文章主要介绍了Graphql中的N+1问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

开篇

原文出处

Graphql 是一种 API 查询语言和运行时环境,可以帮助开发人员快速构建可伸缩的 API。然而,尽管 Graphql 可以提供一些优秀的查询性能和数据获取的能力,但是在使用 Graphql 的过程中,开发人员也会遇到一些常见问题,其中最常见的一个问题是 N+1 问题。

什么是 GraphQL 中的 N+1 问题

在 GraphQL 中,N+1 问题指的是在一个查询语句中,某个字段需要通过 N 次额外查询来获取其关联的数据,导致查询效率低下的情况。这个问题的本质是由于 GraphQL 的数据模型本身的特性引起的。

在 GraphQL 中,查询语句可以包含多个字段,每个字段可能需要访问一个不同的数据源。当查询涉及到关联数据时,如果不做特殊处理,GraphQL 会逐个获取每个字段的数据,这可能会导致大量的额外查询,进而影响查询效率。

假设我们有一个电影网站,它有电影和演员两个实体,每部电影都有多个演员。我们可以用 GraphQL 定义如下的 schema:

type Movie {
  id: ID!
  title: String!
  actors: [Actor!]!
}

type Actor {
  id: ID!
  name: String!
  age: Int!
}

type Query {
  movies: [Movie!]!
}

现在,我们想要查询所有电影及其演员。我们可以像这样编写 GraphQL 查询:

query {
  movies {
    title
    actors {
      name
    }
  }
}

在这个查询中,我们获取了所有电影的标题,以及每部电影的所有演员的名称。然而,如果我们没有采取任何措施来解决 N+1 问题,每个电影的演员都将需要单独查询。因此,如果我们有 100 部电影,就会产生 101 次查询(1 次获取电影,100 次获取演员),这会严重影响性能。

解决方案

Data loader

Data loader 是一个常用的解决 N+1 问题的工具,它可以将多个查询合并成一个查询,以减少查询次数。它的工作原理是在执行查询时,将多个相同类型的查询合并成一个批量查询,并将结果缓存起来,以便在需要时快速获取。Data loader 可以轻松地与 GraphQL 集成,并提供了许多可配置的选项,以便根据应用程序的需要进行优化。

下面是一个使用 data loader 的示例代码:

const DataLoader = require('dataloader')
const { actorsByMovieId } = require('./db')

const actorsLoader = new DataLoader(async (movieIds) => {
  const actors = await actorsByMovieId(movieIds)
  const actorsMap = actors.reduce((acc, actor) => {
    acc[actor.movieId] = acc[actor.movieId] || []
    acc[actor.movieId].push(actor)
    return acc
  }, {})
  return movieIds.map((movieId) => actorsMap[movieId] || [])
})

const resolvers = {
  Query: {
    movies: () => getMovies(),
  },
  Movie: {
    actors: (movie, args, context, info) => actorsLoader.load(movie.id),
  },
}

在上面的代码中,我们使用 data loader 来批量获取每个电影的演员。当 GraphQL 执行查询时,它将调用 load 函数,并将所有需要获取的电影 ID 传递给它。load 函数将所有电影 ID 作为参数,并从数据库中获取所有与这些电影相关的演员。然后,它将演员按电影 ID 分组,并将结果返回到 GraphQL 查询中。由于使用了 data loader,我们现在只需要进行一次查询来获取所有电影及其演员。

Join Monster

Join Monster 是一个解决 GraphQL N+1 问题的工具,它使用了 SQL 批量操作的思想。Join Monster 的主要思想是将多个 GraphQL 解析器的数据请求合并成一个 SQL 查询。这个 SQL 查询是经过优化的,只会查询数据库中需要的数据。同时,Join Monster 还使用了多级缓存来减少数据库的访问次数。

在代码层面,使用 Join Monster 时,我们需要先定义一个解析器,然后在 GraphQL 的 schema 中使用该解析器来查询数据。以下是一个使用 Join Monster 的示例代码:

const joinMonster = require('join-monster').default
const { GraphQLObjectType, GraphQLList } = require('graphql')
const db = require('./db')
const { UserType } = require('./userType')

const CommentType = new GraphQLObjectType({
  name: 'Comment',
  fields: {
    id: { type: GraphQLInt },
    content: { type: GraphQLString },
    user: {
      type: UserType,
      resolve: (parent, args, context, resolveInfo) => {
        return joinMonster(resolveInfo, {}, (sql) => {
          return db.query(sql)
        })
      },
    },
  },
})

const Query = new GraphQLObjectType({
  name: 'Query',
  fields: {
    comments: {
      type: new GraphQLList(CommentType),
      resolve: (parent, args, context, resolveInfo) => {
        return joinMonster(resolveInfo, {}, (sql) => {
          return db.query(sql)
        })
      },
    },
  },
})

module.exports = new GraphQLSchema({ query: Query })

在上述代码中,我们定义了一个 CommentType,它包含了一个 user 字段,该字段使用 Join Monster 进行了解析。同时,我们还定义了一个 Query,该 Query 包含了 comments 字段,使用了 joinMonster 进行解析。在 resolve 函数中,我们将 Join Monster 的解析器传入,并在其中使用了 db.query 函数执行了查询。

假设我们有如下 GraphQL 查询:

{
  comments {
    id
    content
    user {
      id
      name
    }
  }
}

在使用 Join Monster 之前,该查询需要进行 N+1 次 SQL 查询,每个 comment 对应一次查询,每个 user 对应一次查询。

在使用 Join Monster 之后,我们的查询只需要进行一次 SQL 查询。Join Monster 会根据 GraphQL 查询中的字段生成相应的 SQL 查询语句,并在数据库中执行该语句。以下是 Join Monster 生成的 SQL 语句的示例:

SELECT
  `Comment`.`id`,
  `Comment`.`content`,
  `User`.`id` AS `user.id`,
  `User`.`name` AS `user.name`
FROM
  `Comment`
LEFT JOIN
  `User`
ON
  `Comment`.`userId` = `User`.`id`

这个 SQL 查询语句会同时返回 comments 和它们对应的 users 的信息。由于只进行了一次 SQL 查询,Join Monster 大大减少了数据库访问的次数,从而提升了性能。

方案对比

方案 优点 缺点 适用场景
dataloader 可以自动处理 N+1 查询问题;可以使用缓存机制提高性能;比较成熟稳定,社区支持度高 不能自动处理多层嵌套,对复杂查询支持不够好,需要手动编写基于 dataloader 嵌套查询 适用于中小规模的项目,需要快速上手,提高开发效率的场景
join-monster 可以自动生成高效的 SQL 查询,性能优秀; 可以自动处理多层嵌套的 N+1 查询问题 依赖于 SQL 数据库,不适用于非 SQL 数据库场景(需要将 Graphql 当作 ORM) 适用于需要高性能的场景,需要处理复杂查询场景

Data loader 的实现

考虑到 dataloader 比较好实现,且使用广泛,我们选取它进行简单的实现,以此更加深入的理解它是如何解决 N+1 问题的。

根据DataLoader的使用例子来看,DataLoader除了构造器以外,只有一个 load 方法,所以一个简单的 DataLoader 的声明如下:

type BatchFn = <Key, Entity>(keys: Key[]): Promise<Entity[]>;

class DataLoader<Key, Entity> {
  constructor(batchFn: BatchFn<Key, Entity>) {
    // todo
  }
  load(key: Key): Promise<Entity> {
    // todo
  }
}

load 方法只是加入到 batch 的队列中,并不会立刻执行,执行条件是“没有地方调用 load 后“,才会执行整个 batch 队列的请求。于是有了一个小实现:

class DataLoader<Key, Entity> {
  readonly batchFn: BatchFn<Key, Entity>;
  readonly keys: Key[] = [];
  constructor(batchFn: BatchFn<Key, Entity>) {
    this.batchFn = batchFn;
  }
  async load(key: Key): Promise<void> {
    this.keys.push(key);
    if (this.keys.length === 1) {
      this.doBatch();//I hope it executes later
    }
  }
  doBatch(): Promise<Entity[]> {
    return this.batchFn(this.keys);
  }
}

代码很简单,只是遗留了一个问题,也是最重要的问题,如何让this.doBatch能够延迟行,延迟到所有的 load 同步方法调用完后。

此时就需要利用事件循环来改变它的执行顺序:

setImmediate(() => this.doBatch())

因为setImmediate会在回调阶段执行,因此会等到所有同步方法完成在执行。

一个DataLoader的最小实现就产生了:

class DataLoader<Key, Entity> {
  readonly batchFn: BatchFn<Key, Entity>;
  readonly keys: Key[] = [];
  constructor(batchFn: BatchFn<Key, Entity>) {
    this.batchFn = batchFn;
  }
  async load(key: Key): Promise<void> {
    this.keys.push(key);
    if (this.keys.length === 1) {
      setImmediate(() => this.doBatch());
    }
  }
  doBatch(): Promise<Entity[]> {
    return this.batchFn(this.keys);
  }
}

可是它的功能很局限,load 方法不能返回任何的值,Graphql 的 resolve 也就解析不了了。

因此,修改如下:

export default class DataLoader<Key, Entity> {
  readonly batchFn: BatchFn<Key, Entity>;
  readonly storage: {
    key: Key;
    promise: Promise<Entity>;
    resolve: ((entity: Entity) => void) | null;
  }[] = [];
  constructor(batchFn: BatchFn<Key, Entity>) {
    this.batchFn = batchFn;
  }
  async load(key: Key): Promise<Entity> {
    let resolve = null;
    const promise = new Promise<Entity>((res) => (resolve = res));
    this.storage.push({
      key,
      promise,
      resolve,
    });
    if (this.storage.length === 1) {
      setImmediate(() => this.doBatch());
    }
    return promise;
  }
  doBatch(): Promise<void> {
    const keys = this.storage.map(({ key }) => key);
    return this.batchFn(keys).then((entities) =>
      entities.forEach((entity, index) => {
        const { resolve } = this.storage[index];
        resolve && resolve(entity);
      })
    );
  }
}

doBatch将结果依次给到 load 当时挂载的 promise 上,这样以来 resolver 中的 promise 状态就会由 pending 转化为 fulfilled。

当然,为了考虑性能和健壮性,我们还可以继续扩展:

  • 增加缓存
  • 捕获异常
  • 支持手动执行 batch

最终完善如下(github repo):

type BatchFn<K, E> = (keys: K[]) => Promise<E[]>;

interface PromiseMeta<E> {
  resolve: ((entity: E) => void) | null;
  promise: Promise<E>;
}

interface Options {
  immediate: boolean;
}

export default class DataLoader<K, E> {
  readonly batchFn: BatchFn<K, E>;
  readonly cache = new Map<K, PromiseMeta<E>>();
  readonly options: Options = {
    immediate: true,
  };
  constructor(batchFn: BatchFn<K, E>, options?: Options) {
    this.batchFn = batchFn;
    this.options = {
      ...this.options,
      ...options,
    };
  }
  async load(key: K): Promise<E> {
    if (this.options.immediate) {
      if (this.cache.size === 0) {
        setImmediate(() => this.doBatch());
      }
    }

    let resolve = null;
    const promise = new Promise<E>((res) => (resolve = res));
    this.cache.set(key, {
      promise,
      resolve,
    });

    return promise;
  }
  doBatch(): Promise<void> {
    const keys = [...this.cache.keys()];
    return this.batchFn(keys)
      .then((entities) =>
        entities.forEach((entity, index) => {
          const promiseMeta = this.cache.get(keys[index]);
          if (promiseMeta) {
            const { resolve } = promiseMeta;
            resolve && resolve(entity);
          }
        })
      )
      .catch(() => this.cache.clear());
  }
  dispatch(): Promise<void> {
    if (!this.options.immediate) {
      return this.doBatch();
    }
    throw new Error("Doesn't allow to dispatch given immediate is true!");
  }
}

最后

在本文中,我们深入探讨了 GraphQL 中的 N+1 问题。首先,我们介绍了 GraphQL 中常见的一些问题,例如查询过度嵌套和查询重复等。然后,我们详细介绍了 N+1 问题的定义及其出现的原因。接着,我们给出了具体的例子,并讨论了 N+1 问题对性能的影响。在解决 N+1 问题方面,我们列举了几种工具,包括 Batch loading、Data loader 和 Join Monster,并展示了它们在代码层面上的使用。我们还对这些工具的优缺点进行了比较和分析,并给出了最佳实践。

最后,我们介绍了一些避免 N+1 问题的最佳实践,例如避免嵌套查询、使用 GraphQL 片段和优化查询。这些实践可以帮助开发人员避免 N+1 问题并提高查询性能。

总的来说,N+1 问题是 GraphQL 中常见的性能问题之一,但是通过合适的工具和最佳实践,我们可以有效地解决它,提高查询性能,为用户提供更好的体验。文章来源地址https://www.toymoban.com/news/detail-659573.html

到了这里,关于Graphql中的N+1问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • GraphQL渗透测试详解

    GraphQL 是一种查询语言,用于 API 设计和数据交互。它是由 Facebook 发布的一款新型的数据查询和操作语言,自 2012 年起在内部使用,自 2015 年起获得开源许可。由于技术原因,越来越多的公司使用 GraphQL 并将其后端切换到这个新系统,但是,虽然这种查询语言有很多优点,但

    2023年04月24日
    浏览(38)
  • vscode GraphQL插件踩坑

    vscode的GraphQL语法插件, 目前比较推荐 Graphql Foundation 的 GraphQL: Language Feature Support 相关配置, 见[GraphQL: Language Feature Support](# GraphQL: Language Feature Support) 配置文件的语法规则, 参考GraphQL Config 之前用的GraphQL插件, 只开启了语法高亮. 自己写的schema没什么问题, 排查问题时, 只能靠搜

    2024年02月16日
    浏览(42)
  • GraphQL渗透测试案例及防御办法

    GraphQL 是一种 API 查询语言,旨在促进客户端和服务器之间的高效通信。它使用户能够准确指定他们在响应中所需的数据,从而有助于避免有时使用 REST API 看到的大型响应对象和多个调用。 GraphQL 服务定义了一个合约,客户端可以通过该合约与服务器进行通信。客户端不需要

    2024年02月10日
    浏览(50)
  • GraphQL(三) - Authentication 和 Authorication

    本文介绍GraphQL中的Authenication和Authorication 参考: https://graphql.org/learn/authorization/ https://www.apollographql.com/docs/apollo-server/security/authentication/ Authenication 和 Authorication 的概念十分容易混淆,两者的定义如下: Authenication 指用户认证,即是否有用户登录,哪个用户登录 Authorication 指

    2024年01月20日
    浏览(52)
  • Spring Boot中使用GraphQL

    GraphQL是Facebook的一个相对较新的概念,被称为Web API的REST的替代品。 在本文中,我们将学习如何使用 Spring Boot 设置 GraphQL 服务器,以便我们可以将其添加到现有应用程序中或在新应用程序中使用它。 传统的 REST API 使用服务器管理资源的概念。我们可以使用各种HTTP请求以一些

    2024年02月15日
    浏览(33)
  • 何时使用 GraphQL、gRPC 和 REST?

    构建 API 是现代工程中开发人员的最重要任务之一。这些 API 允许不同的系统进行通信和数据交换。虽然  REST  多年来一直是实现 API 的事实标准,但今天也有新兴的标准,如  gRPC  和  GraphQL 。 “应用程序编程接口”(API)是各种软件服务之间的通信渠道。传输请求和响应

    2024年04月25日
    浏览(28)
  • Spring Boot集成Graphql快速入门Demo

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。 优势 GraphQL 速度快,并且比较稳定,GraphQL 的操作是在数据层面的,

    2024年04月13日
    浏览(44)
  • 使用GraphQL在Postman中进行API测试

    GraphQL 是一种用于API的开源数据查询和操作语言,用于API的查询语言和运行时。它使客户端能够精确地指定其数据需求,并获得预测性地结果。GraphQL旨在提高API的效率、灵活性和可靠性。 Postman 是一款用于API开发的强大工具,它支持REST和GraphQL API。Postman还提供了一个用户友好

    2024年02月12日
    浏览(35)
  • Elasticsearch 与 GraphQL 整合:构建实时搜索 API

    随着互联网的普及和数据的快速增长,实时搜索已经成为现代网站和应用程序的必不可少的功能。实时搜索可以帮助用户快速找到相关信息,提高用户体验,增加用户留存时间,并提高销售转化率。 Elasticsearch 是一个开源的搜索和分析引擎,基于 Lucene 库,它提供了一个实时

    2024年04月22日
    浏览(33)
  • Introduction to GraphQL-style APIs

    GraphQL is an open-source query language and runtime environment developed by Facebook for constructing APIs. Unlike traditional RESTful APIs, GraphQL allows clients to send precise queries to retrieve the necessary data without returning extraneous information. The core idea of GraphQL is to allow clients to define the data structure they require, rather th

    2024年02月20日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包