Skip to content

Commit cf1c99f

Browse files
committed
Support DataLoader in EntityMapping methods
Closes gh-1095
1 parent d51dbfb commit cf1c99f

File tree

3 files changed

+76
-5
lines changed

3 files changed

+76
-5
lines changed

spring-graphql-docs/modules/ROOT/pages/federation.adoc

+34-5
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,8 @@ xref:federation.adoc#federation.entity-mapping.signature[Method Signature] for s
8080
method argument and return value types.
8181
<2> `@SchemaMapping` methods can be used for the rest of the graph.
8282

83-
An `@EntityMapping` method can batch load federated entities of a given type. To do that,
84-
declare the `@Argument` method parameter as a list, and return the corresponding entity
85-
instances as a list in the same order.
86-
87-
For example:
83+
You can load federated entities of the same type together by accepting a `List` of id's,
84+
and returning a `List` or `Flux` of entities:
8885

8986
[source,java,indent=0,subs="verbatim,quotes"]
9087
----
@@ -108,6 +105,35 @@ look up the correct value in the "representation" input map. You can also set th
108105
argument name through the annotation.
109106
<2> `@BatchMapping` methods can be used for the rest of the graph.
110107

108+
You can load federated entities with a `DataLoader`:
109+
110+
[source,java,indent=0,subs="verbatim,quotes"]
111+
----
112+
@Controller
113+
private static class BookController {
114+
115+
@Autowired
116+
public DataLoaderBookController(BatchLoaderRegistry registry) { // <1>
117+
registry.forTypePair(Integer.class, Book.class).registerBatchLoader((bookIds, environment) -> {
118+
// load entities...
119+
});
120+
}
121+
122+
@EntityMapping
123+
public Future<Book> book(@Argument int id, DataLoader<Integer, Book> dataLoader) { // <2>
124+
return dataLoader.load(id);
125+
}
126+
127+
@BatchMapping
128+
public Map<Book, Author> author(List<Book> books) { // <3>
129+
// ...
130+
}
131+
}
132+
----
133+
134+
<1> Register a batch loader for the federated entity type.
135+
<2> Declare a `DataLoader` argument to the `@EntityMapping` method.
136+
<3> `@BatchMapping` methods can be used for the rest of the graph.
111137

112138

113139
[[federation.entity-mapping.signature]]
@@ -153,6 +179,9 @@ Entity mapping methods support the following arguments:
153179
| `DataFetchingEnvironment`
154180
| For direct access to the underlying `DataFetchingEnvironment`.
155181

182+
| `DataLoader<I, E>`
183+
| To load federated entities with a `DataLoader` where `I` is the id type, and `E` is the entity type.
184+
156185
|===
157186

158187
`@EntityMapping` methods can return `Mono`, `CompletableFuture`, `Callable`, or the actual entity.

spring-graphql/src/main/java/org/springframework/graphql/data/federation/FederationSchemaFactory.java

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.graphql.data.method.annotation.support.ContextValueMethodArgumentResolver;
4949
import org.springframework.graphql.data.method.annotation.support.ContinuationHandlerMethodArgumentResolver;
5050
import org.springframework.graphql.data.method.annotation.support.DataFetchingEnvironmentMethodArgumentResolver;
51+
import org.springframework.graphql.data.method.annotation.support.DataLoaderMethodArgumentResolver;
5152
import org.springframework.graphql.data.method.annotation.support.LocalContextValueMethodArgumentResolver;
5253
import org.springframework.graphql.data.method.annotation.support.PrincipalMethodArgumentResolver;
5354
import org.springframework.graphql.execution.ClassNameTypeResolver;
@@ -125,6 +126,7 @@ protected HandlerMethodArgumentResolverComposite initArgumentResolvers() {
125126

126127
// Type based
127128
resolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver());
129+
resolvers.addResolver(new DataLoaderMethodArgumentResolver());
128130
if (springSecurityPresent) {
129131
ApplicationContext context = obtainApplicationContext();
130132
resolvers.addResolver(new PrincipalMethodArgumentResolver());

spring-graphql/src/test/java/org/springframework/graphql/data/federation/EntityMappingInvocationTests.java

+40
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,19 @@
1919
import java.util.Collections;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.concurrent.Future;
2223

2324
import graphql.GraphQLError;
2425
import graphql.GraphqlErrorBuilder;
2526
import graphql.schema.DataFetchingEnvironment;
27+
import org.dataloader.DataLoader;
2628
import org.junit.jupiter.api.Test;
2729
import org.junit.jupiter.params.ParameterizedTest;
2830
import org.junit.jupiter.params.provider.ValueSource;
2931
import reactor.core.publisher.Flux;
3032
import reactor.core.publisher.Mono;
3133

34+
import org.springframework.beans.factory.annotation.Autowired;
3235
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3336
import org.springframework.core.io.ClassPathResource;
3437
import org.springframework.core.io.Resource;
@@ -176,6 +179,19 @@ void batchingWithoutResult(Class<?> controllerClass) {
176179
assertError(helper, 2, "INTERNAL_ERROR", "Entity fetcher returned null or completed empty");
177180
}
178181

182+
@Test
183+
void dataLoader() {
184+
Map<String, Object> variables =
185+
Map.of("representations", List.of(
186+
Map.of("__typename", "Book", "id", "3"),
187+
Map.of("__typename", "Book", "id", "5")));
188+
189+
ResponseHelper helper = executeWith(DataLoaderBookController.class, variables);
190+
191+
assertAuthor(0, "Joseph", "Heller", helper);
192+
assertAuthor(1, "George", "Orwell", helper);
193+
}
194+
179195
@Test
180196
void unmappedEntity() {
181197
assertThatIllegalStateException().isThrownBy(() -> executeWith(EmptyController.class, Map.of()))
@@ -306,6 +322,30 @@ public GraphQLError handle(IllegalArgumentException ex, DataFetchingEnvironment
306322
}
307323

308324

325+
@SuppressWarnings("unused")
326+
@Controller
327+
private static class DataLoaderBookController {
328+
329+
@Autowired
330+
public DataLoaderBookController(BatchLoaderRegistry batchLoaderRegistry) {
331+
batchLoaderRegistry.forTypePair(Integer.class, Book.class)
332+
.registerBatchLoader((ids, env) ->
333+
Flux.fromIterable(ids).map(id -> new Book((long) id, null, (Long) null)));
334+
}
335+
336+
@Nullable
337+
@EntityMapping
338+
public Future<Book> book(@Argument int id, DataLoader<Integer, Book> dataLoader) {
339+
return dataLoader.load(id);
340+
}
341+
342+
@BatchMapping
343+
public Flux<Author> author(List<Book> books) {
344+
return Flux.fromIterable(books).map(book -> BookSource.getBook(book.getId()).getAuthor());
345+
}
346+
}
347+
348+
309349
@SuppressWarnings("unused")
310350
@Controller
311351
private static class EmptyController {

0 commit comments

Comments
 (0)