使用DataLoader
使用GraphQL的過程中,可能需要在一個圖數據上做多次查詢。使用原始的數據加載方式,很容易產生性能問題。
通過使用java-dataloader,可以結合緩存(Cache)和批處理(Batching)的方式,在圖形數據上發起批量請求。如果dataloader已經獲取過相關的數據,那么它會緩存數據的值,然后直接返回給調用方(無需重復發起請求)。
假設我們有一個StarWars的執行語句如下:它允許我們找到一個hero,他的朋友的名字以及朋友的朋友的名字。顯然會有一部分朋友數據,會在這個查詢中被多次請求到。
{
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,可以使數據的請求效率更高。
針對Query語句的層級,GraphQL會逐層次下降依次查詢。(例如:首先處理hero字段,然后處理friends,然后處理每個friend的friends)。data loader是一種契約,使用它可以獲得查詢的對象,但它將延遲發起對象數據的請求。在每一個層級上,dataloader.dispatch()方法會批量觸發這一層級上的所有請求。在開啟了緩存的條件下,任何之前已請求到的數據都會直接返回,而不會再次發起請求調用。
上述的實例中,只有五個唯一的person對象。通過使用緩存+批處理的獲取方式,實際上只發起了三次網絡調用就實現了數據的請求。
相比於原始的15次請求方式,效率大大提升。
如果使用了java.util.concurrent.CompletableFuture.supplyAsync(),還可以通過開啟異步執行的方式,進一步提升執行效率,減少響應時間。
示例代碼如下:
//
// 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)。如果不去顯式指定的話,它默認會自動添加進來。
使用AsyncExecutionStrategy策略的Data Loader
graphql.execution.AsyncExecutionStrategy是dataLoader的唯一執行策略。這個執行策略可以自行確定dispatch的最佳時間,它通過追蹤還有多少字段未完成,以及它們是否為列表值等來實現此目的。
其他的執行策略,例如:ExecutorServiceExecutionStrategy策略無法實現該功能。當data loader檢測到並未使用AsyncExecutionStrategy策略時,它會在遇到每個field時都調用data loader的dispatch方法。雖然可以通過緩存值的方式減少請求次數,但無法使用批量請求策略。
request特定的Data Loader
如果正在發起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);
異步調用batch loader功能
采用data loader的編碼模式,通過將所有未完成的data loader請求合並為一個批量加載的請求,提高了請求的效率。
GraphQL - Java會追蹤那些尚未完成的data loader請求,並在最合適的時間調用dispatch方法,觸發數據的批量請求。
