Java GraphQL数据加载器批处理的实现详解

 更新时间:2023年12月22日 15:26:42   作者:happyEnding  
GraphQL 数据加载器是优化 GraphQL API 的关键组件,旨在解决臭名昭著的 N+1 查询问题,在本中,我们将深入研究其批处理功能,感兴趣的小伙伴可以了解下

介绍

GraphQL 是一种强大而灵活的 API 查询语言,使客户端能够准确请求他们所需的数据,从而消除信息的过度获取和获取不足。然而,随着 GraphQL 查询变得更加复杂并涉及多个数据源,有效地检索数据并向客户端提供数据可能具有挑战性。这就是 GraphQL 数据加载器发挥作用的地方。

GraphQL 数据加载器是优化 GraphQL API 的关键组件,旨在解决臭名昭著的 N+1 查询问题,该问题在 GraphQL 服务器重复获取相关项目列表的相同数据时发生。数据加载器通过批处理和缓存请求,帮助简化从各种来源(例如数据库、API,甚至本地缓存)获取数据的过程。通过这样做,他们显着提高了 GraphQL 查询的效率和性能。

在本中,我们将深入研究批处理功能,通过查看数据加载器的 java 实现来探索它如何发挥其魔力。

批处理

批处理是将多个单独的数据检索请求收集到单个批处理请求中的过程,从而减少对数据源的调用次数。在处理 GraphQL 查询中的关系时,这一点尤其重要。

考虑一个典型场景,其中 GraphQL 查询请求一个项目列表,以及每个项目的附加相关数据(例如用户信息)。如果不进行批处理,这将导致对每个项目进行单独的数据库查询或 API 请求,从而导致 N+1 查询问题。通过批处理,可以将这些单独的请求有效地组合成单个请求,从而大大减少数据源的往返次数

Java 数据加载器批处理

假设我们有一个如下所示的 graphql 查询

{
  user {
    name
    friends {
      name
    }
  }
}

它生成以下查询结果

{
  "user": {
    "name": "zhangsan",
    “friends”: [
      {
        "name": "lisi",
      },
      {
        "name": "wanmgwu",
      },
      {
        "name": "zhouliu",
      }
    ]
  }
}

一个简单的实现方法是为查询响应中的每个用户执行一次调用以检索一个用户对象,即 4 次调用,一次针对根对象,一次针对列表中的每个好友。

然而,它DataLoader不会立即执行远程调用,它只是将调用排入队列并返回一个 Promise ( CompletableFuture) 来传递用户对象。一旦我们将构建查询结果的所有调用排入队列,我们​​必须请求DataLoader开始执行它们。这就是奇迹发生的地方。将DataLoader开始提取每次调用的用户 ID 并将其放入一个列表中,该列表将用于查询我们配置的后端,并仅使用一个请求即可检索用户列表。

批处理通常按级别进行,在本例中我们有 2 个级别。root 用户和他的朋友。通过使用DataLoaderbatchig,此响应将只需要 2 次调用。

代码示例

让我们添加一些代码来展示如何使用它。

我们首先需要拥有一个BatchLoader. 它将从用户后端批量加载用户,从而减少对该后端的 API 调用量。

List<User> loadUsersById(List<Long> userIds) {
   System.out.println("Api call to load users = " + userIds);
   return users.stream().filter(u -> userIds.contains(u.id())).toList();
}

BatchLoader<Long, User> userBatchLoader = new BatchLoader<>() {
   @Override
   public CompletionStage<List<User>> load(List<Long> userIds) {
      return CompletableFuture.supplyAsync(() -> {
         return loadUsersById(userIds);
      });
   }
};

然后我们需要创建一个DataLoader将使用前面的BatchLoader来执行整个用户树的加载。

var userLoader = DataLoaderFactory.newDataLoader(userBatchLoader);

var userDTO = new UserDTO();
userLoader.load(1L).thenAccept(user -> {
   userDTO.id = user.id();
   userDTO.name = user.name();
   user.friends().forEach(friendId -> {
      userLoader.load(friendId).thenAccept(friend -> {
         userDTO.friends.add(new FriendDTO(friend.id(), friend.name()));
      });
   });
});

userLoader.dispatchAndJoin();
System.out.println(userDTO);

它将产生以下调试输出

Api call to load users = [1]
Api call to load users = [2, 3, 4]
UserDTO{id=1, name='John', friends=[FriendDTO[id=2, name=Jane], FriendDTO[id=3, name=Bob], FriendDTO[id=4, name=Alice]]}

如果您对它的内部工作原理感到好奇,我将向您展示用户的一种自定义实现DataLoader。不是真正的。只需一个简化版本即可帮助您了解全貌。

static class UserLoader {
   BatchLoader<Long, User> userBatchLoader;

   record QueueEntry(long id, CompletableFuture<User> value) { }
   List<QueueEntry> loaderQueue = new ArrayList<>();

   UserLoader(BatchLoader<Long, User> userBatchLoader) {
      this.userBatchLoader = userBatchLoader;
   }

   CompletableFuture<User> load(long userId) {
      var future = new CompletableFuture<User>();
      loaderQueue.add(new QueueEntry(userId, future));
      return future;
   }

   List<User> dispatchAndJoin() {
      List<User> joinedResults = dispatch().join();
      List<User> results = new ArrayList<>(joinedResults);
      while (loaderQueue.size() > 0) {
         joinedResults = dispatch().join();
         results.addAll(joinedResults);
      }
      return results;
   }

   CompletableFuture<List<User>> dispatch() {
      var userIds = new ArrayList<Long>();
      final List<CompletableFuture<User>> queuedFutures = new ArrayList<>();

      loaderQueue.forEach(qe -> {
         userIds.add(qe.id());
         queuedFutures.add(qe.value());
      });

      loaderQueue.clear();

      var userFutures = userBatchLoader.load(userIds).toCompletableFuture();

      return userFutures.thenApply(users -> {
         for (int i = 0; i < queuedFutures.size(); i++) {
            var userId = userIds.get(i);
            var user = users.get(i);
            var future = queuedFutures.get(i);
            future.complete(user);
         }
         return users;
      });
   }
}

所以,首先看一下CompletableFuture<User> load(long userId),它不执行任何 userId 查找,它只是:

  • 将查找排入队列
  • 生成一个,CompletableFuture让您根据您提供的查找链接进一步查找。因此,查找被推迟,直到我们实际使用dispatchAndJoin()

现在,看看List<User> dispatchAndJoin()。一旦我们准备好检索用户列表,就会调用该函数。它会:

1.调用 CompletableFuture<List<User>> dispatch()将执行以下操作:

将所有 userId 分组到一个列表中,并将其发送到底层BatchLoader ,底层对后端执行实际的 API 调用。

完成我们注册查找时(当我们调用 )时提供的 CompletableFuture CompletableFuture<User> load(long userId),从而向 中添加更多元素loaderQueue。此时,下一级的 userId 查找已排队。

2.当中还有剩余元素时重复该过程loaderQueue

到此这篇关于Java GraphQL数据加载器批处理的实现详解的文章就介绍到这了,更多相关Java GraphQL数据加载器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 教你怎么使用Java实现WebSocket

    教你怎么使用Java实现WebSocket

    这篇文章主要介绍了教你怎么使用Java WebSocket,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好的帮助,需要的朋友可以参考下
    2021-05-05
  • java中Iterator和ListIterator实例详解

    java中Iterator和ListIterator实例详解

    这篇文章主要介绍了java中Iterator和ListIterator实例详解,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • JAVA MyBatis入门学习过程记录

    JAVA MyBatis入门学习过程记录

    MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架。这篇文章主要介绍了mybatis框架入门学习教程,需要的朋友可以参考下,希望能帮助到你
    2021-06-06
  • Mybatis-Plus使用ID_WORKER生成主键id重复的解决方法

    Mybatis-Plus使用ID_WORKER生成主键id重复的解决方法

    本文主要介绍了Mybatis-Plus使用ID_WORKER生成主键id重复的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • IntelliJ IDEA 2023.2最新版激活方法及验证ja-netfilter配置是否成功

    IntelliJ IDEA 2023.2最新版激活方法及验证ja-netfilter配置是否成功

    随着2023.2版本的发布,用户们渴望了解如何激活这个最新版的IDE,本文将介绍三种可行的激活方案,包括许可证服务器、许可证代码和idea vmoptions配置,帮助读者成功激活并充分利用IDEA的功能,感兴趣的朋友参考下吧
    2023-08-08
  • Sentinel结合Nacos实现数据持久化过程详解

    Sentinel结合Nacos实现数据持久化过程详解

    这篇文章主要介绍了Sentinel结合Nacos实现数据持久化过程,要持久化的原因是因为每次启动Sentinel都会使之前配置的规则就清空了,这样每次都要再去设定规则显得非常的麻烦,感兴趣想要详细了解可以参考下文
    2023-05-05
  • MyBatisPlus 一对多、多对一、多对多的完美解决方案

    MyBatisPlus 一对多、多对一、多对多的完美解决方案

    这篇文章主要介绍了MyBatisPlus 一对多、多对一、多对多的完美解决方案,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • MybatisPlus只取一条记录的两种方法实现

    MybatisPlus只取一条记录的两种方法实现

    本文介绍了MyBatis-Plus2.x和3.x版本在IService接口获取单条记录的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-07-07
  • java的Console类的使用方法及实例

    java的Console类的使用方法及实例

    这篇文章主要介绍了java的Console类的使用方法及实例的相关资料,需要的朋友可以参考下
    2017-07-07
  • IDEA中没有Mapper.xml模板选项的处理方法

    IDEA中没有Mapper.xml模板选项的处理方法

    这篇文章主要介绍了IDEA中没有Mapper.xml模板选项的处理方法,需其实解决方法很简单,只需要在idea中导入模板即可,本文图文的形式给大家分享解决方法,需要的朋友可以参考下
    2021-04-04

最新评论