GraphQL Java 数据加载器

介绍

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

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

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

批处理

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

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

Java 数据加载器批处理

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

markdown 复制代码
{
  user {
    name
    friends {
      name
    }
  }
}

它生成以下查询结果

json 复制代码
{
  "user": {
    "name": "zhangsan",
    "friends": [
      {
        "name": "lisi",
      },
      {
        "name": "wanmgwu",
      },
      {
        "name": "zhouliu",
      }
    ]
  }
}

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

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

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

代码示例

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

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

kotlin 复制代码
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来执行整个用户树的加载。

ini 复制代码
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);

它将产生以下调试输出

bash 复制代码
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。不是真正的。只需一个简化版本即可帮助您了解全貌。

ini 复制代码
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 查找,它只是:

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

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

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

    1. 将所有 userId 分组到一个列表中,并将其发送到底层BatchLoader ,底层对后端执行实际的 API 调用。
    2. 完成我们注册查找时(当我们调用 )时提供的 CompletableFuture CompletableFuture<User> load(long userId),从而向 中添加更多元素loaderQueue。此时,下一级的 userId 查找已排队。
  2. 当 中还有剩余元素时重复该过程loaderQueue

相关推荐
忙碌54415 天前
区块链应用开发的完整实战指南:从理论到落地的企业级解决方案
架构·区块链·restful·graphql
yuki_uix25 天前
GraphQL 重塑:从 API 语言到 AI 时代的"逻辑神经系统"
前端·graphql
果粒蹬i1 个月前
【HarmonyOS】RN of HarmonyOS实战开发项目+Apollo GraphQL客户端
华为·harmonyos·graphql
程序猿阿伟1 个月前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
程序猿阿伟1 个月前
《GraphQL状态图建模与低时延控制能力解析》
后端·graphql
程序猿阿伟1 个月前
《面向第三方的GraphQL开放平台设计指南》
后端·graphql
程序猿阿伟1 个月前
《GraphQL 强类型架构下的错误处理体系设计指南》
后端·架构·graphql
爬山算法1 个月前
Hibernate(78)如何在GraphQL服务中使用Hibernate?
java·hibernate·graphql
Irene19911 个月前
HTTP 请求方法选择与 RESTful 实践(对比 GraphQL、RPC)
rpc·restful·http请求·grpc·graphql
Dontla1 个月前
GraphQL介绍(声明式查询)文件上传GraphQL文件上传
后端·graphql