Skip to content

Commit 6b6eb73

Browse files
authored
Merge pull request #5 from graphql-java/3-instrumentation-dispatcher
3 instrumentation dispatcher
2 parents 0331945 + e9c801a commit 6b6eb73

8 files changed

+429
-4
lines changed

README.md

+125-2
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,136 @@ a list of user ids in one call.
165165

166166
```
167167

168-
That said, with key caching turn on (the default), it may still be more efficient using `dataloader` than without it.
168+
That said, with key caching turn on (the default), it will still be more efficient using `dataloader` than without it.
169+
170+
# Using dataloader in graphql for maximum efficiency
171+
172+
173+
If you are using `graphql`, you are likely to making queries on a graph of data (surprise surprise). `dataloader` will help
174+
you to make this a more efficient process by both caching and batching requests for that graph of data items. If `dataloader`
175+
has previously see a data item before, it will cached the value and will return it without having to ask for it again.
176+
177+
Imagine we have the StarWars query outlined below. It asks us to find a hero and their friend's names and their friend's friend's
178+
names. It is likely that many of these people will be friends in common.
179+
180+
181+
182+
{
183+
hero {
184+
name
185+
friends {
186+
name
187+
friends {
188+
name
189+
}
190+
}
191+
}
192+
}
193+
194+
The result of this query is displayed below. You can see that Han, Leia, Luke and R2-D2 are tight knit bunch of friends and
195+
share many friends in common.
196+
197+
198+
[hero: [name: 'R2-D2', friends: [
199+
[name: 'Luke Skywalker', friends: [
200+
[name: 'Han Solo'], [name: 'Leia Organa'], [name: 'C-3PO'], [name: 'R2-D2']]],
201+
[name: 'Han Solo', friends: [
202+
[name: 'Luke Skywalker'], [name: 'Leia Organa'], [name: 'R2-D2']]],
203+
[name: 'Leia Organa', friends: [
204+
[name: 'Luke Skywalker'], [name: 'Han Solo'], [name: 'C-3PO'], [name: 'R2-D2']]]]]
205+
]
206+
207+
A naive implementation would called a `DataFetcher` to retrieved a person object every time it was invoked.
208+
209+
In this case it would be *15* calls over the network. Even though the group of people have a lot of common friends.
210+
With `dataloader` you can make the `graphql` query much more efficient.
211+
212+
As `graphql` descends each level of the query ( eg as it processes `hero` and then `friends` and then for each their `friends`),
213+
the data loader is called to "promise" to deliver a person object. At each level `dataloader.dispatch()` will be
214+
called to fire off the batch requests for that part of the query. With caching turned on (the default) then
215+
any previously returned person will be returned as is for no cost.
216+
217+
In the above example there are only *5* unique people mentioned but with caching and batching retrieval in place their will be only
218+
*3* calls to the batch loader function. *3* calls over the network or to a database is much better than *15* calls you will agree.
219+
220+
If you use capabilities like `java.util.concurrent.CompletableFuture.supplyAsync()` then you can make it even more efficient by making the
221+
the remote calls asynchronous to the rest of the query. This will make it even more timely since multiple calls can happen at once
222+
if need be.
223+
224+
Here is how you might put this in place:
225+
226+
227+
```java
228+
229+
// a batch loader function that will be called with N or more keys for batch loading
230+
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
231+
@Override
232+
public CompletionStage<List<Object>> load(List<String> keys) {
233+
//
234+
// we use supplyAsync() of values here for maximum parellisation
235+
//
236+
return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
237+
}
238+
};
239+
240+
// a data loader for characters that points to the character batch loader
241+
DataLoader characterDataLoader = new DataLoader<String, Object>(characterBatchLoader);
242+
243+
//
244+
// use this data loader in the data fetchers associated with characters and put them into
245+
// the graphql schema (not shown)
246+
//
247+
DataFetcher heroDataFetcher = new DataFetcher() {
248+
@Override
249+
public Object get(DataFetchingEnvironment environment) {
250+
return characterDataLoader.load("2001"); // R2D2
251+
}
252+
};
253+
254+
DataFetcher friendsDataFetcher = new DataFetcher() {
255+
@Override
256+
public Object get(DataFetchingEnvironment environment) {
257+
StarWarsCharacter starWarsCharacter = environment.getSource();
258+
List<String> friendIds = starWarsCharacter.getFriendIds();
259+
return characterDataLoader.loadMany(friendIds);
260+
}
261+
};
262+
263+
//
264+
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
265+
// in this case there is 1 but you can have many
266+
//
267+
DataLoaderRegistry registry = new DataLoaderRegistry();
268+
registry.register(characterDataLoader);
269+
270+
//
271+
// this instrumentation implementation will dispatched all the dataloaders
272+
// as each level fo the graphql query is executed and hence make batched objects
273+
// available to the query and the associated DataFetchers
274+
//
275+
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
276+
= new DataLoaderDispatcherInstrumentation(registry);
277+
278+
//
279+
// now build your graphql object and execute queries on it.
280+
// the data loader will be invoked via the data fetchers on the
281+
// schema fields
282+
//
283+
GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
284+
.instrumentation(dispatcherInstrumentation)
285+
.build();
286+
```
287+
288+
One thing to note is the above only works if you use `DataLoaderDispatcherInstrumentation` which makes sure `dataLoader.dispatch()` is called. If
289+
this was not in place, then all the promises to data will never be dispatched ot the batch loader function and hence nothing would ever resolve.
290+
291+
See below for more details on `dataLoader.dispatch()`
169292

170293
## Differences to reference implementation
171294

172295
### Manual dispatching
173296

174-
The original data loader was written in Javascript for NodeJS. NodeJS is single-threaded in nature, but simulates
297+
The original [Facebook DataLoader](https://github.com/facebook/dataloader) was written in Javascript for NodeJS. NodeJS is single-threaded in nature, but simulates
175298
asynchronous logic by invoking functions on separate threads in an event loop, as explained
176299
[in this post](http://stackoverflow.com/a/19823583/3455094) on StackOverflow.
177300

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ compileJava {
4949
}
5050

5151
dependencies {
52+
compile "com.graphql-java:graphql-java:4.0"
5253
testCompile "junit:junit:$junitVersion"
5354
testCompile 'org.awaitility:awaitility:2.0.0'
5455
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.dataloader;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.concurrent.CopyOnWriteArrayList;
6+
7+
/**
8+
* This allows data loaders to be registered together into a single place so
9+
* they can be dispatched as one.
10+
*/
11+
public class DataLoaderRegistry {
12+
private final List<DataLoader<?, ?>> dataLoaders = new CopyOnWriteArrayList<>();
13+
14+
/**
15+
* @return the currently registered data loaders
16+
*/
17+
public List<DataLoader<?, ?>> getDataLoaders() {
18+
return new ArrayList<>(dataLoaders);
19+
}
20+
21+
/**
22+
* This will register a new dataloader
23+
*
24+
* @param dataLoader the data loader to register
25+
*
26+
* @return this registry
27+
*/
28+
public DataLoaderRegistry register(DataLoader<?, ?> dataLoader) {
29+
if (!dataLoaders.contains(dataLoader)) {
30+
dataLoaders.add(dataLoader);
31+
}
32+
return this;
33+
}
34+
35+
/**
36+
* This will unregister a new dataloader
37+
*
38+
* @param dataLoader the data loader to unregister
39+
*
40+
* @return this registry
41+
*/
42+
public DataLoaderRegistry unregister(DataLoader<?, ?> dataLoader) {
43+
dataLoaders.remove(dataLoader);
44+
return this;
45+
}
46+
47+
/**
48+
* This will called {@link org.dataloader.DataLoader#dispatch()} on each of the registered
49+
* {@link org.dataloader.DataLoader}s
50+
*/
51+
public void dispatchAll() {
52+
dataLoaders.forEach(DataLoader::dispatch);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.dataloader.graphql;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.execution.instrumentation.InstrumentationContext;
5+
import graphql.execution.instrumentation.NoOpInstrumentation;
6+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters;
7+
import org.dataloader.DataLoaderRegistry;
8+
9+
import java.util.concurrent.CompletableFuture;
10+
11+
/**
12+
* This graphql {@link graphql.execution.instrumentation.Instrumentation} will dispatch
13+
* all the contained {@link org.dataloader.DataLoader}s when each level of the graphql
14+
* query is executed.
15+
*/
16+
public class DataLoaderDispatcherInstrumentation extends NoOpInstrumentation {
17+
18+
private final DataLoaderRegistry dataLoaderRegistry;
19+
20+
public DataLoaderDispatcherInstrumentation(DataLoaderRegistry dataLoaderRegistry) {
21+
this.dataLoaderRegistry = dataLoaderRegistry;
22+
}
23+
24+
@Override
25+
public InstrumentationContext<CompletableFuture<ExecutionResult>> beginExecutionStrategy(InstrumentationExecutionStrategyParameters parameters) {
26+
return (result, t) -> {
27+
if (t == null) {
28+
// only dispatch when there are no errors
29+
dataLoaderRegistry.dispatchAll();
30+
}
31+
};
32+
}
33+
}

src/test/java/ReadmeExamples.java

+86
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
import graphql.GraphQL;
2+
import graphql.schema.DataFetcher;
3+
import graphql.schema.DataFetchingEnvironment;
4+
import graphql.schema.GraphQLSchema;
15
import org.dataloader.BatchLoader;
26
import org.dataloader.DataLoader;
7+
import org.dataloader.DataLoaderRegistry;
38
import org.dataloader.fixtures.User;
49
import org.dataloader.fixtures.UserManager;
10+
import org.dataloader.graphql.DataLoaderDispatcherInstrumentation;
511

612
import java.util.List;
713
import java.util.concurrent.CompletableFuture;
814
import java.util.concurrent.CompletionStage;
915
import java.util.stream.Collectors;
1016

17+
@SuppressWarnings("ALL")
1118
public class ReadmeExamples {
1219

20+
1321
UserManager userManager = new UserManager();
1422

1523
public static void main(String[] args) {
@@ -68,4 +76,82 @@ public CompletionStage<List<User>> load(List<Long> userIds) {
6876
userLoader.dispatchAndJoin();
6977
}
7078

79+
80+
class StarWarsCharacter {
81+
List<String> getFriendIds() {
82+
return null;
83+
}
84+
}
85+
86+
void starWarsExample() {
87+
88+
// a batch loader function that will be called with N or more keys for batch loading
89+
BatchLoader<String, Object> characterBatchLoader = new BatchLoader<String, Object>() {
90+
@Override
91+
public CompletionStage<List<Object>> load(List<String> keys) {
92+
//
93+
// we use supplyAsync() of values here for maximum parellisation
94+
//
95+
return CompletableFuture.supplyAsync(() -> getCharacterDataViaBatchHTTPApi(keys));
96+
}
97+
};
98+
99+
// a data loader for characters that points to the character batch loader
100+
DataLoader characterDataLoader = new DataLoader<String, Object>(characterBatchLoader);
101+
102+
//
103+
// use this data loader in the data fetchers associated with characters and put them into
104+
// the graphql schema (not shown)
105+
//
106+
DataFetcher heroDataFetcher = new DataFetcher() {
107+
@Override
108+
public Object get(DataFetchingEnvironment environment) {
109+
return characterDataLoader.load("2001"); // R2D2
110+
}
111+
};
112+
113+
DataFetcher friendsDataFetcher = new DataFetcher() {
114+
@Override
115+
public Object get(DataFetchingEnvironment environment) {
116+
StarWarsCharacter starWarsCharacter = environment.getSource();
117+
List<String> friendIds = starWarsCharacter.getFriendIds();
118+
return characterDataLoader.loadMany(friendIds);
119+
}
120+
};
121+
122+
//
123+
// DataLoaderRegistry is a place to register all data loaders in that needs to be dispatched together
124+
// in this case there is 1 but you can have many
125+
//
126+
DataLoaderRegistry registry = new DataLoaderRegistry();
127+
registry.register(characterDataLoader);
128+
129+
//
130+
// this instrumentation implementation will dispatched all the dataloaders
131+
// as each level fo the graphql query is executed and hence make batched objects
132+
// available to the query and the associated DataFetchers
133+
//
134+
DataLoaderDispatcherInstrumentation dispatcherInstrumentation
135+
= new DataLoaderDispatcherInstrumentation(registry);
136+
137+
//
138+
// now build your graphql object and execute queries on it.
139+
// the data loader will be invoked via the data fetchers on the
140+
// schema fields
141+
//
142+
GraphQL graphQL = GraphQL.newGraphQL(buildSchema())
143+
.instrumentation(dispatcherInstrumentation)
144+
.build();
145+
146+
}
147+
148+
private GraphQLSchema buildSchema() {
149+
return null;
150+
}
151+
152+
private List<Object> getCharacterDataViaBatchHTTPApi(List<String> keys) {
153+
return null;
154+
}
155+
156+
71157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.dataloader;
2+
3+
import org.junit.Test;
4+
5+
import java.util.concurrent.CompletableFuture;
6+
7+
import static java.util.Arrays.asList;
8+
import static org.hamcrest.Matchers.equalTo;
9+
import static org.junit.Assert.assertThat;
10+
11+
public class DataLoaderRegistryTest {
12+
final BatchLoader<Object, Object> identityBatchLoader = CompletableFuture::completedFuture;
13+
14+
@Test
15+
public void registration_works() throws Exception {
16+
DataLoader<Object, Object> dlA = new DataLoader<>(identityBatchLoader);
17+
DataLoader<Object, Object> dlB = new DataLoader<>(identityBatchLoader);
18+
DataLoader<Object, Object> dlC = new DataLoader<>(identityBatchLoader);
19+
20+
DataLoaderRegistry registry = new DataLoaderRegistry();
21+
22+
registry.register(dlA).register(dlB).register(dlC);
23+
24+
assertThat(registry.getDataLoaders(), equalTo(asList(dlA, dlB, dlC)));
25+
26+
// the same dl twice is one add
27+
28+
29+
registry = new DataLoaderRegistry();
30+
31+
registry.register(dlA).register(dlB).register(dlC).register(dlA).register(dlB);
32+
33+
assertThat(registry.getDataLoaders(), equalTo(asList(dlA, dlB, dlC)));
34+
35+
36+
// and unregister
37+
registry.unregister(dlC);
38+
39+
assertThat(registry.getDataLoaders(), equalTo(asList(dlA, dlB)));
40+
}
41+
}

0 commit comments

Comments
 (0)