引用

https://www.graphql-java.com/documentation/v16/batching/
https://github.com/graphql-java/java-dataloader

优势

graphql 为我们带来了图式查询

  • 较少的数据(请求你所要的数据,不多不少)
  • 灵活的递归(获取多个资源,只用一个请求)
  • 自动生成文档(描述所有的可能类型系统)
  • 前后端同步开发(前端甚至可以MOCK数据)

1+n问题

graphql字段以独立的方式解析!
所以一次查询,很容易导致可怕的 n+1 问题
当我们要查询fhc(user)的朋友和他朋友的朋友的姓名时,我们假设获得以下结果:

{
  "name": "fhc",
  "friends": [
    {
      "name": "jby",
      "friends": [
        {"name": "fhc"},
        {"name": "lem"}
      ]
    },
    {
      "name": "swl",
      "friends": [
        {"name": "lem"},
        {"name": "jby"},
        {"name": "fhc"}
      ]
    }
  ]
}

通常情况,graphql的解析器(DataFetcher)会解析每一个user,即产生1+2+5共8次数据库请求,这是一种严重的效率问题。
所幸DataLoader可以帮助我们解决这个问题。

官方解释
As graphql descends each level of the query (e.g. fhc,fhc的朋友,fhc朋友的朋友), the data loader is called to “promise” to deliver a person object. At each level dataloader.dispatch() will be called to fire off the batch requests for that part of the query. With caching turned on (the default) then any previously returned person will be returned as-is for no cost.

就是说dataloader在从上到下每一层的查询中,会调用dataloader.dispatch()方法来触发批处理请求。(实现可参考DataLoaderDispatcherInstrumentation)

在使用了dataloader后请求将变成1+1+1=3次 WoW!

DataLoader是一个实用程序类,其核心方法load

public CompletableFuture<V> load(K key, Object keyContext) {
        return helper.load(key, keyContext);
    }
  1. 传入一个键key,则异步在未来返回值V
  2. 如果启用了批处理(默认设置),则必须在稍后阶段调用dispatch()才能开始执行批处理。 如果您忘记了此调用,则将来将永远无法完成(除非已完成,并从缓存中返回)。

一个DataLoader对象需要一个BatchLoader用来装载键列表,并返回承诺(promise)值列表

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。

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

通过lambda表达式可简写为(并封装成方法)

public static BatchLoader<String, User> userBatchLoader() {
        return ids -> CompletableFuture.supplyAsync(() -> UserQry.getBy(ids));
    }

  1. 值列表大小需要与键列表相同
  2. 值列表的索引必须与键列表索引对应
  3. 键不存在值时,返回空仍占位
 [
      { id: 2, name: 'San Francisco' },
      { id: 9, name: 'Chicago' },
       null,
      { id: 1, name: 'New York' }
 ]

现在让我们注册并构建一个DataLoader

  1. 新建一个DataLoader的注册表
DataLoaderRegistry registry = new DataLoaderRegistry();
  1. 使用上面的userBatchLoader创建一个DataLoader,关键字为USER_DLR
registry.register("USER_DLR", DataLoader.newDataLoader(DataLoaders.userBatchLoader()));
  1. 从DataFetchingEnvironment中通过加载器名称获取DataLoader这里是dataLoaderName=USER_DLR(封装为方法)
public static <K, V> DataLoader<K, V> getDataLoader(DataFetchingEnvironment env, String dataLoaderName) {
        return env.getDataLoader(dataLoaderName);
    }

4.使用DataLoader(封装为方法)

#单个载入
public static <T extends BaseEntity> CompletableFuture<T> load(DataFetchingEnvironment env, String dataLoaderName, T entity) {
        return entity == null ? null : DataLoaderHelper.<String, T>getDataLoader(env, dataLoaderName).load(entity.getId());
    }
#批量载入
public static <T extends BaseEntity> CompletableFuture<List<T>> loadMany(DataFetchingEnvironment env, String dataLoaderName, List<String> ids) {
        return DataLoaderHelper.<String, T>getDataLoader(env, dataLoaderName).loadMany(ids);
    }

5.在解析器中调用

public CompletableFuture<User> getUser(User user, DataFetchingEnvironment env) {
        return DataLoaderHelper.load(env,"USER_DLR", user);
    }

余留问题

  • 批处理将多次查询组合成了一个sql。但一对多情况下,通过一查询多的id仍需要一次sql,故而仍需要n+1。