使用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方法,觸發數據的批量請求。