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数据加载器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis初始化知识小结

    Mybatis初始化知识小结

    Mybatis的初始化过程就是加载自己运行时所需要的配置信息的过程,这篇文章主要介绍了Mybatis初始化知识小结,需要的朋友可以参考下
    2021-10-10
  • java 11新特性HttpClient主要组件及发送请求示例详解

    java 11新特性HttpClient主要组件及发送请求示例详解

    这篇文章主要为大家介绍了java 11新特性HttpClient主要组件及发送请求示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • 关于Spring Cloud的熔断器监控问题

    关于Spring Cloud的熔断器监控问题

    Turbine是一个聚合Hystrix监控数据的工具,它可将所有相关/hystrix.stream端点的数据聚合到一个组合的/turbine.stream中,从而让集群的监控更加方便,接下来通过本文给大家介绍Spring Cloud的熔断器监控,感兴趣的朋友一起看看吧
    2022-01-01
  • HashSet工作原理_动力节点Java学院整理

    HashSet工作原理_动力节点Java学院整理

    HashSet 底层采用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单。接下来通过本文给大家介绍HashSet工作原理_动力节点Java学院整理,需要的朋友可以参考下
    2017-04-04
  • java设计模式笔记之适配器模式

    java设计模式笔记之适配器模式

    这篇文章主要为大家详细介绍了java设计模式之适配器模式笔记,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-10-10
  • Spring Boot 端口被占用的解决方法

    Spring Boot 端口被占用的解决方法

    这篇文章主要介绍了解决 Spring Boot 端口被占用的方法详解,通过本文的介绍,你学习了如何解决 Spring Boot 端口被占用的问题。你了解了检查端口是否被占用、停止占用端口的进程、更改应用程序的端口号以及检查应用程序间的端口冲突等方法,需要的朋友可以参考下
    2023-07-07
  • Java异常处理Guava Throwables类使用实例解析

    Java异常处理Guava Throwables类使用实例解析

    这篇文章主要为大家介绍了Java异常处理神器Guava Throwables类使用深入详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Spring Boot 指定外部启动配置文件详解

    Spring Boot 指定外部启动配置文件详解

    在springboot项目中,也可以使用yml类型的配置文件代替properties文件。接下来通过本文给大家分享Springboot配置文件的使用,感兴趣的朋友一起看看吧
    2021-09-09
  • Java之InputStreamReader类的实现

    Java之InputStreamReader类的实现

    这篇文章主要介绍了Java之InputStreamReader类的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • Nacos配合SpringBoot实现动态线程池的基本步骤

    Nacos配合SpringBoot实现动态线程池的基本步骤

    使用Nacos配合Spring Boot实现动态线程池,可以让你的应用动态地调整线程池参数而无需重启,这对于需要高度可配置且需要适应不同负载情况的应用来说非常有用,本文给大家介绍实现动态线程池的基本步骤,需要的朋友可以参考下
    2024-02-02

最新评论