Skip to content

Commit 19a935f

Browse files
committed
Add batch loading info to SelfDescribingDataFetcher
Prior to this commit, the `SelfDescribingDataFetcher` would augment the `DataFetcher` contract and provide more information about the data fetcher itself. This commit adds a new `isBatchLoading` method to indicate whether the current data fetcher is using a `DataLoader` for fetching elements. In Spring for GraphQL, this can typically happen if the method is annotated with `@BatchMapping` or if the `@SchemaMapping` method as a `DataLoader` parameter. This change is required for instrumentation purposes: such data fetchers should not be instrumented as data fetching operations, but instead delegate to the `DataLoaderRegistry` being itself instrumented. Closes gh-1176
1 parent 462e73e commit 19a935f

File tree

5 files changed

+123
-5
lines changed

5 files changed

+123
-5
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

+23
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,8 @@ static class SchemaMappingDataFetcher implements SelfDescribingDataFetcher<Objec
446446

447447
private final boolean subscription;
448448

449+
private final boolean usesDataLoader;
450+
449451
SchemaMappingDataFetcher(
450452
DataFetcherMappingInfo info, HandlerMethodArgumentResolverComposite argumentResolvers,
451453
@Nullable ValidationHelper helper, HandlerDataFetcherExceptionResolver exceptionResolver,
@@ -462,6 +464,17 @@ static class SchemaMappingDataFetcher implements SelfDescribingDataFetcher<Objec
462464
this.executor = executor;
463465
this.invokeAsync = invokeAsync;
464466
this.subscription = this.mappingInfo.getCoordinates().getTypeName().equalsIgnoreCase("Subscription");
467+
this.usesDataLoader = hasDataLoaderParameter();
468+
}
469+
470+
private boolean hasDataLoaderParameter() {
471+
Method handlerMethod = this.mappingInfo.getHandlerMethod().getMethod();
472+
for (Class<?> parameterType : handlerMethod.getParameterTypes()) {
473+
if (DataLoader.class.equals(parameterType)) {
474+
return true;
475+
}
476+
}
477+
return false;
465478
}
466479

467480
@Override
@@ -551,6 +564,11 @@ private <T> Publisher<T> handleSubscriptionError(
551564
.switchIfEmpty(Mono.error(ex));
552565
}
553566

567+
@Override
568+
public boolean isBatchLoading() {
569+
return this.usesDataLoader;
570+
}
571+
554572
@Override
555573
public String toString() {
556574
return getDescription();
@@ -595,6 +613,11 @@ public Object get(DataFetchingEnvironment env) {
595613
dataLoader.load(env.getSource()));
596614
}
597615

616+
@Override
617+
public boolean isBatchLoading() {
618+
return true;
619+
}
620+
598621
@Override
599622
public String toString() {
600623
return getDescription();

spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.graphql.execution;
1818

1919
import java.util.List;
20+
import java.util.Map;
2021

2122
import graphql.ExecutionInput;
2223
import graphql.GraphQLContext;
@@ -40,6 +41,7 @@
4041
import reactor.core.publisher.Flux;
4142
import reactor.core.publisher.Mono;
4243

44+
import org.springframework.core.ResolvableType;
4345
import org.springframework.lang.Nullable;
4446
import org.springframework.util.Assert;
4547

@@ -51,11 +53,13 @@
5153
* <li>Re-establish Reactor Context passed via {@link ExecutionInput}.
5254
* <li>Re-establish ThreadLocal context passed via {@link ExecutionInput}.
5355
* <li>Resolve exceptions from a GraphQL subscription {@link Publisher}.
56+
* <li>Propagate the cancellation signal to {@code DataFetcher} from the transport layer.
5457
* </ul>
5558
*
5659
* @author Rossen Stoyanchev
60+
* @author Brian Clozel
5761
*/
58-
final class ContextDataFetcherDecorator implements DataFetcher<Object> {
62+
class ContextDataFetcherDecorator implements DataFetcher<Object> {
5963

6064
private final DataFetcher<?> delegate;
6165

@@ -146,6 +150,17 @@ static GraphQLTypeVisitor createVisitor(List<SubscriptionExceptionResolver> reso
146150
return new ContextTypeVisitor(resolvers);
147151
}
148152

153+
private static ContextDataFetcherDecorator decorate(
154+
DataFetcher<?> delegate, boolean handlesSubscription,
155+
SubscriptionExceptionResolver subscriptionExceptionResolver) {
156+
if (delegate instanceof SelfDescribingDataFetcher<?> selfDescribingDataFetcher) {
157+
return new SelfDescribingDecorator(selfDescribingDataFetcher, handlesSubscription, subscriptionExceptionResolver);
158+
}
159+
else {
160+
return new ContextDataFetcherDecorator(delegate, handlesSubscription, subscriptionExceptionResolver);
161+
}
162+
}
163+
149164

150165
/**
151166
* Type visitor to apply {@link ContextDataFetcherDecorator}.
@@ -171,7 +186,7 @@ public TraversalControl visitGraphQLFieldDefinition(
171186

172187
if (applyDecorator(dataFetcher)) {
173188
boolean handlesSubscription = visitorHelper.isSubscriptionType(parent);
174-
dataFetcher = new ContextDataFetcherDecorator(dataFetcher, handlesSubscription, this.exceptionResolver);
189+
dataFetcher = ContextDataFetcherDecorator.decorate(dataFetcher, handlesSubscription, this.exceptionResolver);
175190
codeRegistry.dataFetcher(fieldCoordinates, dataFetcher);
176191
}
177192

@@ -192,4 +207,36 @@ private boolean applyDecorator(DataFetcher<?> dataFetcher) {
192207
}
193208
}
194209

210+
private static final class SelfDescribingDecorator extends ContextDataFetcherDecorator implements SelfDescribingDataFetcher<Object> {
211+
212+
private final SelfDescribingDataFetcher<?> selfDescribingDataFetcher;
213+
214+
private SelfDescribingDecorator(
215+
SelfDescribingDataFetcher<?> delegate, boolean subscription,
216+
SubscriptionExceptionResolver subscriptionExceptionResolver) {
217+
super(delegate, subscription, subscriptionExceptionResolver);
218+
this.selfDescribingDataFetcher = delegate;
219+
}
220+
221+
@Override
222+
public boolean isBatchLoading() {
223+
return this.selfDescribingDataFetcher.isBatchLoading();
224+
}
225+
226+
@Override
227+
public Map<String, ResolvableType> getArguments() {
228+
return this.selfDescribingDataFetcher.getArguments();
229+
}
230+
231+
@Override
232+
public ResolvableType getReturnType() {
233+
return this.selfDescribingDataFetcher.getReturnType();
234+
}
235+
236+
@Override
237+
public String getDescription() {
238+
return this.selfDescribingDataFetcher.getDescription();
239+
}
240+
}
241+
195242
}

spring-graphql/src/main/java/org/springframework/graphql/execution/SelfDescribingDataFetcher.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -59,4 +59,13 @@ default Map<String, ResolvableType> getArguments() {
5959
return Collections.emptyMap();
6060
}
6161

62+
/**
63+
* Return whether this {@code DataFetcher} is using batch loading.
64+
* @return {@code true} if the data fetcher is batch loading elements
65+
* @since 1.4.0
66+
*/
67+
default boolean isBatchLoading() {
68+
return false;
69+
}
70+
6271
}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingDetectionTests.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@
4141
import org.springframework.graphql.data.method.annotation.SchemaMapping;
4242
import org.springframework.graphql.execution.BatchLoaderRegistry;
4343
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
44+
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
4445
import org.springframework.stereotype.Controller;
4546

4647
import static org.assertj.core.api.Assertions.assertThat;
@@ -76,6 +77,15 @@ void registerWithDefaultCoordinates() {
7677
"Book.authorCallableMap", "Book.authorEnvironment");
7778
}
7879

80+
@Test
81+
void dataFetchersMarkedAsBatchLoading() {
82+
Map<String, Map<String, DataFetcher>> dataFetcherMap =
83+
initRuntimeWiringBuilder(BookController.class).build().getDataFetchers();
84+
assertThat(dataFetcherMap.get("Book").values()).allMatch(dataFetcher ->
85+
dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribingDataFetcher
86+
&& selfDescribingDataFetcher.isBatchLoading());
87+
}
88+
7989
@Test
8090
void registerWithMaxBatchSize() {
8191

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingDetectionTests.java

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,10 +17,12 @@
1717
package org.springframework.graphql.data.method.annotation.support;
1818

1919
import java.util.Map;
20+
import java.util.concurrent.CompletableFuture;
2021

2122
import graphql.schema.DataFetcher;
2223
import graphql.schema.DataFetchingEnvironment;
2324
import graphql.schema.idl.RuntimeWiring;
25+
import org.dataloader.DataLoader;
2426
import org.junit.jupiter.api.Test;
2527
import reactor.core.publisher.Flux;
2628

@@ -32,6 +34,7 @@
3234
import org.springframework.graphql.data.method.annotation.QueryMapping;
3335
import org.springframework.graphql.data.method.annotation.SchemaMapping;
3436
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
37+
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
3538
import org.springframework.stereotype.Controller;
3639
import org.springframework.util.StringUtils;
3740

@@ -81,6 +84,22 @@ void registerWithExplicitCoordinates() {
8184
assertMapping(map, "Book.authorCustomized", "authorWithNonMatchingMethodName");
8285
}
8386

87+
@Test
88+
void batchLoadingDataFetchers() {
89+
Map<String, Map<String, DataFetcher>> map =
90+
initRuntimeWiringBuilder(BookController.class).build().getDataFetchers();
91+
Map<String, DataFetcher> queries = map.get("Book");
92+
assertThat(queries.values()).allMatch(dataFetcher ->
93+
dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribingDataFetcher
94+
&& !selfDescribingDataFetcher.isBatchLoading());
95+
96+
map = initRuntimeWiringBuilder(BatchLoadingController.class).build().getDataFetchers();
97+
queries = map.get("Book");
98+
assertThat(queries.values()).allMatch(dataFetcher ->
99+
dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribingDataFetcher
100+
&& selfDescribingDataFetcher.isBatchLoading());
101+
}
102+
84103
private RuntimeWiring.Builder initRuntimeWiringBuilder(Class<?> handlerType) {
85104
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext();
86105
appContext.registerBean(handlerType);
@@ -152,6 +171,16 @@ public Flux<Book> bookSearchWithNonMatchingMethodName(@Argument String author) {
152171
public Author authorWithNonMatchingMethodName(Book book) {
153172
return null;
154173
}
174+
175+
}
176+
177+
@Controller
178+
private static class BatchLoadingController {
179+
180+
@SchemaMapping
181+
public CompletableFuture<Author> authorBatch(Book book, DataLoader<Long, Author> loader) {
182+
return null;
183+
}
155184
}
156185

157186
}

0 commit comments

Comments
 (0)