使用GraphQL的过程当中,可能须要在一个图数据上作屡次查询。使用原始的数据加载方式,很容易产生性能问题。java
经过使用java-dataloader,能够结合缓存(Cache)和批处理(Batching)的方式,在图形数据上发起批量请求。若是dataloader已经获取过相关的数据,那么它会缓存数据的值,而后直接返回给调用方(无需重复发起请求)。web
假设咱们有一个StarWars的执行语句以下:它容许咱们找到一个hero,他的朋友的名字以及朋友的朋友的名字。显然会有一部分朋友数据,会在这个查询中被屡次请求到。redis
{ hero { name friends { name friends { name } } } }
其查询结果以下所示:缓存
{ "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker", "friends": [ {"name": "Han Solo"}, {"name": "Leia Organa"}, {"name": "C-3PO"}, {"name": "R2-D2"} ] }, { "name": "Han Solo", "friends": [ {"name": "Luke Skywalker"}, {"name": "Leia Organa"}, {"name": "R2-D2"} ] }, { "name": "Leia Organa", "friends": [ {"name": "Luke Skywalker"}, {"name": "Han Solo"}, {"name": "C-3PO"}, {"name": "R2-D2"} ] } ] } }
比较原始的实现方案是,每次query的时候都调用一次DataFetcher来获取一个person对象。网络
在这种场景下,将会发起15次调用,而且其中有不少数据被屡次、重复请求。结合dataLoader,可使数据的请求效率更高。less
针对Query语句的层级,GraphQL会逐层次降低依次查询。(例如:首先处理hero字段,而后处理friends,而后处理每一个friend的friends)。data loader是一种契约,使用它能够得到查询的对象,但它将延迟发起对象数据的请求。在每个层级上,dataloader.dispatch()方法会批量触发这一层级上的全部请求。在开启了缓存的条件下,任何以前已请求到的数据都会直接返回,而不会再次发起请求调用。异步
上述的实例中,只有五个惟一的person对象。经过使用缓存+批处理的获取方式,实际上只发起了三次网络调用就实现了数据的请求。async
相比于原始的15次请求方式,效率大大提高。ide
若是使用了java.util.concurrent.CompletableFuture.supplyAsync(),还能够经过开启异步执行的方式,进一步提高执行效率,减小响应时间。memcached
示例代码以下:
// // a batch loader function that will be called with N or more keys for batch loading // This can be a singleton object since it's stateless // BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() { @Override public CompletionStage<List<Object>> load(List<String> keys) { // // we use supplyAsync() of values here for maximum parellisation // return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys)); } }; // // use this data loader in the data fetchers associated with characters and put them into // the graphql schema (not shown) // DataFetcher heroDataFetcher = new DataFetcher() { @Override public Object get(DataFetchingEnvironment environment) { DataLoader<String, Object> dataLoader = environment.getDataLoader("character"); return dataLoader.load("2001"); // R2D2 } }; DataFetcher friendsDataFetcher = new DataFetcher() { @Override public Object get(DataFetchingEnvironment environment) { StarWarsCharacter starWarsCharacter = environment.getSource(); List<String> friendIds = starWarsCharacter.getFriendIds(); DataLoader<String, Object> dataLoader = environment.getDataLoader("character"); return dataLoader.loadMany(friendIds); } }; // // this instrumentation implementation will dispatch all the data loaders // as each level of the graphql query is executed and hence make batched objects // available to the query and the associated DataFetchers // // In this case we use options to make it keep statistics on the batching efficiency // DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions .newOptions().includeStatistics(true); DataLoaderDispatcherInstrumentation dispatcherInstrumentation = new DataLoaderDispatcherInstrumentation(options); // // now build your graphql object and execute queries on it. // the data loader will be invoked via the data fetchers on the // schema fields // GraphQL graphQL = GraphQL.newGraphQL(buildSchema()) .instrumentation(dispatcherInstrumentation) .build(); // // a data loader for characters that points to the character batch loader // // Since data loaders are stateful, they are created per execution request. // DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(characterBatchLoader); // // DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together // in this case there is 1 but you can have many. // // Also note that the data loaders are created per execution request // DataLoaderRegistry registry = new DataLoaderRegistry(); registry.register("character", characterDataLoader); ExecutionInput executionInput = newExecutionInput() .query(getQuery()) .dataLoaderRegistry(registry) .build(); ExecutionResult executionResult = graphQL.execute(executionInput);
如上,咱们添加了DataLoaderDispatcherInstrument实例。由于咱们想要调整它的初始化选项(Options)。若是不去显式指定的话,它默认会自动添加进来。
graphql.execution.AsyncExecutionStrategy是dataLoader的惟一执行策略。这个执行策略能够自行肯定dispatch的最佳时间,它经过追踪还有多少字段未完成,以及它们是否为列表值等来实现此目的。
其余的执行策略,例如:ExecutorServiceExecutionStrategy策略没法实现该功能。当data loader检测到并未使用AsyncExecutionStrategy策略时,它会在遇到每一个field时都调用data loader的dispatch方法。虽然能够经过缓存值的方式减小请求次数,但没法使用批量请求策略。
若是正在发起Web请求,那么数据能够特定于请求它的用户。 若是有特定于用户的数据,且不但愿缓存用于用户A的数据,而后在后续请求中将其提供给用户B。
DataLoader实例的做用域很重要。为每一个web请求建立dataLoader实例,并确保数据仅仅缓存在该web请求中,而对于其余web请求无效。它也确保了调用仅仅影响本次graphql的执行,而不影响其余的graphql请求执行。
默认状况下,DataLoaders充当缓存。 若是访问到以前请求过的key的值,那么它们会自动返回它以便提升效率。
若是数据须要在多个web请求当中共享,那么须要修改data loader的缓存实现,以使不一样的请求之间,其data loader能够经过一些中间层(如redis缓存或memcached)共享数据。
在使用的过程当中,仍然为每次请求都建立一个data loaders,经过缓存层在不一样的data loader之间开启数据共享。
CacheMap<String, Object> crossRequestCacheMap = new CacheMap<String, Object>() { @Override public boolean containsKey(String key) { return redisIntegration.containsKey(key); } @Override public Object get(String key) { return redisIntegration.getValue(key); } @Override public CacheMap<String, Object> set(String key, Object value) { redisIntegration.setValue(key, value); return this; } @Override public CacheMap<String, Object> delete(String key) { redisIntegration.clearKey(key); return this; } @Override public CacheMap<String, Object> clear() { redisIntegration.clearAll(); return this; } }; DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(crossRequestCacheMap); DataLoader<String, Object> dataLoader = DataLoader.newDataLoader(batchLoader, options);
采用data loader的编码模式,经过将全部未完成的data loader请求合并为一个批量加载的请求,提升了请求的效率。
GraphQL - Java会追踪那些还没有完成的data loader请求,并在最合适的时间调用dispatch方法,触发数据的批量请求。