Skip to content

Commit 4bdaea1

Browse files
committed
Added doco for dataloaders
1 parent e1a1e5e commit 4bdaea1

File tree

2 files changed

+151
-41
lines changed

2 files changed

+151
-41
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
public
2-
.DS_STORE
2+
.DS_STORE
3+
.idea

content/documentation/master/batching.md

Lines changed: 149 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ Here is how you might put this in place:
9494

9595
{{< highlight java "linenos=table" >}}
9696

97+
//
9798
// a batch loader function that will be called with N or more keys for batch loading
99+
// This can be a singleton object since its stateless
100+
//
98101
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
99102
@Override
100103
public CompletionStage<List<Object>> load(List<String> keys) {
@@ -105,8 +108,6 @@ Here is how you might put this in place:
105108
}
106109
};
107110

108-
// a data loader for characters that points to the character batch loader
109-
DataLoader<String, Object> characterDataLoader = new DataLoader<>(characterBatchLoader);
110111

111112
//
112113
// use this data loader in the data fetchers associated with characters and put them into
@@ -115,7 +116,8 @@ Here is how you might put this in place:
115116
DataFetcher heroDataFetcher = new DataFetcher() {
116117
@Override
117118
public Object get(DataFetchingEnvironment environment) {
118-
return characterDataLoader.load("2001"); // R2D2
119+
DataLoader<String, Object> dataLoader = environment.getDataLoader("character");
120+
return dataLoader.load("2001"); // R2D2
119121
}
120122
};
121123

@@ -124,24 +126,22 @@ Here is how you might put this in place:
124126
public Object get(DataFetchingEnvironment environment) {
125127
StarWarsCharacter starWarsCharacter = environment.getSource();
126128
List<String> friendIds = starWarsCharacter.getFriendIds();
127-
return characterDataLoader.loadMany(friendIds);
129+
DataLoader<String, Object> dataLoader = environment.getDataLoader("character");
130+
return dataLoader.loadMany(friendIds);
128131
}
129132
};
130133

131-
//
132-
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
133-
// in this case there is 1 but you can have many
134-
//
135-
DataLoaderRegistry registry = new DataLoaderRegistry();
136-
registry.register("character", characterDataLoader);
137134

138135
//
139-
// this instrumentation implementation will dispatch all the dataloaders
136+
// this instrumentation implementation will dispatched all the data loaders
140137
// as each level fo the graphql query is executed and hence make batched objects
141138
// available to the query and the associated DataFetchers
142139
//
140+
DataLoaderDispatcherInstrumentationOptions options = DataLoaderDispatcherInstrumentationOptions
141+
.newOptions().includeStatistics(true);
142+
143143
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
144-
= new DataLoaderDispatcherInstrumentation(registry);
144+
= new DataLoaderDispatcherInstrumentation(options);
145145

146146
//
147147
// now build your graphql object and execute queries on it.
@@ -152,11 +152,34 @@ Here is how you might put this in place:
152152
.instrumentation(dispatcherInstrumentation)
153153
.build();
154154

155+
//
156+
// a data loader for characters that points to the character batch loader
157+
//
158+
// Since data loaders are stateful, they are created per execution request.
159+
//
160+
DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(characterBatchLoader);
161+
162+
//
163+
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
164+
// in this case there is 1 but you can have many.
165+
//
166+
// Also note that the data loaders are created per execution request
167+
//
168+
DataLoaderRegistry registry = new DataLoaderRegistry();
169+
registry.register("character", characterDataLoader);
170+
171+
ExecutionInput executionInput = newExecutionInput()
172+
.query(getQuery())
173+
.dataLoaderRegistry(registry)
174+
.build();
175+
176+
ExecutionResult executionResult = graphQL.execute(executionInput);
177+
178+
155179
{{< / highlight >}}
156180

157-
One thing to note is the above only works if you use `DataLoaderDispatcherInstrumentation` which makes sure `dataLoader.dispatch()`
158-
is called. If this was not in place, then all the promises to data will never be dispatched ot the batch loader function
159-
and hence nothing would ever resolve.
181+
In this example we explicitly added the `DataLoaderDispatcherInstrumentation` because we wanted to tweak its options. However
182+
it will be automatically added for you if you don't add it manually.
160183

161184
## Data Loader only works with AsyncExecutionStrategy
162185

@@ -174,37 +197,54 @@ may get `caching` of values but you will not get `batching` of them.
174197
If you are serving web requests then the data can be specific to the user requesting it. If you have user specific data then you will not want to
175198
cache data meant for user A to then later give it to user B in a subsequent request.
176199

177-
The scope of your DataLoader instances is important. You might want to create them per web request to
178-
ensure data is only cached within that web request and no more.
200+
The scope of your DataLoader instances is important. You will want to create them per web request to
201+
ensure data is only cached within that web request and no more. It also ensures that a ``dispatch`` call
202+
only affects that graphql execution and no other.
179203

180-
If your data can be shared across web requests then you might want to scope your data loaders so they survive
181-
longer than the web request say.
204+
DataLoaders by default act as caches. If they have seen a value before for a key then they will automatically return
205+
it in order to be efficient.
182206

183-
But if you are doing per request data loaders then creating a new set of `GraphQL` and `DataLoader` objects per
184-
request is super cheap. It's the `GraphQLSchema` creation that can be expensive, especially if you are using graphql SDL parsing.
207+
If your data can be shared across web requests then you might want to change the caching implementation of your data loaders so they share
208+
data via a caching layer say like memcached or redis.
185209

186-
Structure your code so that the schema is statically held, perhaps in a static variable or in a singleton IoC component but
187-
build out a new `GraphQL` set of objects on each request.
210+
You still create data loaders per request, however the caching layer will allow data sharing (if that's suitable).
188211

189212

190213
{{< highlight java "linenos=table" >}}
191214

192-
GraphQLSchema staticSchema = staticSchema_Or_MayBeFrom_IoC_Injection();
215+
CacheMap<String, Object> crossRequestCacheMap = new CacheMap<String, Object>() {
216+
@Override
217+
public boolean containsKey(String key) {
218+
return redisIntegration.containsKey(key);
219+
}
193220

194-
DataLoaderRegistry registry = new DataLoaderRegistry();
195-
registry.register("character", getCharacterDataLoader());
221+
@Override
222+
public Object get(String key) {
223+
return redisIntegration.getValue(key);
224+
}
196225

197-
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
198-
= new DataLoaderDispatcherInstrumentation(registry);
226+
@Override
227+
public CacheMap<String, Object> set(String key, Object value) {
228+
redisIntegration.setValue(key, value);
229+
return this;
230+
}
199231

200-
GraphQL graphQL = GraphQL.newGraphQL(staticSchema)
201-
.instrumentation(dispatcherInstrumentation)
202-
.build();
232+
@Override
233+
public CacheMap<String, Object> delete(String key) {
234+
redisIntegration.clearKey(key);
235+
return this;
236+
}
237+
238+
@Override
239+
public CacheMap<String, Object> clear() {
240+
redisIntegration.clearAll();
241+
return this;
242+
}
243+
};
203244

204-
graphQL.execute("{ helloworld }");
245+
DataLoaderOptions options = DataLoaderOptions.newOptions().setCacheMap(crossRequestCacheMap);
205246

206-
// you can now throw away the GraphQL and hence DataLoaderDispatcherInstrumentation
207-
// and DataLoaderRegistry objects since they are really cheap to build per request
247+
DataLoader<String, Object> dataLoader = DataLoader.newDataLoader(batchLoader, options);
208248

209249
{{< / highlight >}}
210250

@@ -222,14 +262,16 @@ The following will not work (it will never complete).
222262

223263
{{< highlight java "linenos=table" >}}
224264

225-
BatchLoader<String, Object> batchLoader = new BatchLoader<String, Object>() {
265+
BatchLoader<String, Object> batchLoader = new BatchLoader<String, Object>() {
226266
@Override
227267
public CompletionStage<List<Object>> load(List<String> keys) {
228268
return CompletableFuture.completedFuture(getTheseCharacters(keys));
229269
}
230270
};
231271

232-
DataLoader<String, Object> characterDataLoader = new DataLoader<>(batchLoader);
272+
DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(batchLoader);
273+
274+
// .... later in your data fetcher
233275

234276
DataFetcher dataFetcherThatCallsTheDataLoader = new DataFetcher() {
235277
@Override
@@ -239,10 +281,12 @@ The following will not work (it will never complete).
239281
//
240282
return CompletableFuture.supplyAsync(() -> {
241283
String argId = environment.getArgument("id");
242-
return characterDataLoader.load(argId);
284+
DataLoader<String, Object> characterLoader = environment.getDataLoader("characterLoader");
285+
return characterLoader.load(argId);
243286
});
244287
}
245288
};
289+
246290
{{< / highlight >}}
247291

248292
In the example above, the call to `characterDataLoader.load(argId)` can happen some time in the future on another thread. The graphql-java
@@ -263,7 +307,9 @@ The following is how you can still have asynchronous code, by placing it into th
263307
}
264308
};
265309

266-
DataLoader<String, Object> characterDataLoader = new DataLoader<>(batchLoader);
310+
DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(batchLoader);
311+
312+
// .... later in your data fetcher
267313

268314
DataFetcher dataFetcherThatCallsTheDataLoader = new DataFetcher() {
269315
@Override
@@ -272,14 +318,77 @@ The following is how you can still have asynchronous code, by placing it into th
272318
// This is OK
273319
//
274320
String argId = environment.getArgument("id");
275-
return characterDataLoader.load(argId);
321+
DataLoader<String, Object> characterLoader = environment.getDataLoader("characterLoader");
322+
return characterLoader.load(argId);
276323
}
277324
};
325+
278326
{{< / highlight >}}
279327

280328
Notice above the `characterDataLoader.load(argId)` returns immediately. This will enqueue the call for data until a later time when all
281329
the graphql fields are dispatched.
282330

283331
Then later when the `DataLoader` is dispatched, it's `BatchLoader` function is called. This code can be asynchronous so that if you have multiple batch loader
284332
functions they all can run at once. In the code above `CompletableFuture.supplyAsync(() -> getTheseCharacters(keys));` will run the ``getTheseCharacters``
285-
method in another thread.
333+
method in another thread.
334+
335+
# Passing context to your data loader
336+
337+
The data loader library supports two types of context being passed to the batch loader. The first is
338+
an overall context object per dataloader and the second is a map of per loaded key context objects.
339+
340+
This allows you to pass in the extra details you may need to make downstream calls. The dataloader key is used
341+
in the caching of results but the context objects can be made available to help with the call.
342+
343+
So in the example below we have an overall security context object that gives out a call token and we also pass the graphql source
344+
object to each ``dataLoader.load()`` call.
345+
346+
{{< highlight java "linenos=table" >}}
347+
348+
BatchLoaderWithContext<String, Object> batchLoaderWithCtx = new BatchLoaderWithContext<String, Object>() {
349+
350+
@Override
351+
public CompletionStage<List<Object>> load(List<String> keys, BatchLoaderEnvironment loaderContext) {
352+
//
353+
// we can have an overall context object
354+
SecurityContext securityCtx = loaderContext.getContext();
355+
//
356+
// and we can have a per key set of context objects
357+
Map<Object, Object> keysToSourceObjects = loaderContext.getKeyContexts();
358+
359+
return CompletableFuture.supplyAsync(() -> getTheseCharacters(securityCtx.getToken(), keys, keysToSourceObjects));
360+
}
361+
};
362+
363+
// ....
364+
365+
SecurityContext securityCtx = SecurityContext.newSecurityContext();
366+
367+
BatchLoaderContextProvider contextProvider = new BatchLoaderContextProvider() {
368+
@Override
369+
public Object getContext() {
370+
return securityCtx;
371+
}
372+
};
373+
//
374+
// this creates an overall context for the dataloader
375+
//
376+
DataLoaderOptions loaderOptions = DataLoaderOptions.newOptions().setBatchLoaderContextProvider(contextProvider);
377+
DataLoader<String, Object> characterDataLoader = DataLoader.newDataLoader(batchLoaderWithCtx, loaderOptions);
378+
379+
// .... later in your data fetcher
380+
381+
DataFetcher dataFetcherThatCallsTheDataLoader = new DataFetcher() {
382+
@Override
383+
public Object get(DataFetchingEnvironment environment) {
384+
String argId = environment.getArgument("id");
385+
Object source = environment.getSource();
386+
//
387+
// you can pass per load call contexts
388+
//
389+
return characterDataLoader.load(argId, source);
390+
}
391+
};
392+
393+
{{< / highlight >}}
394+

0 commit comments

Comments
 (0)