Spring Boot: Data Loading & Integration
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
This page covers the JVM-specific machinery Spring for GraphQL adds on top of the language-agnostic execution
model: batching resolver calls with @BatchMapping/BatchLoaderRegistry, resolving federated entities with
@EntityMapping, and auto-registering data fetchers straight from Spring Data repositories.
Why this needs a Spring-specific fix
Execution & Resolvers explains why nested fields are
resolved independently, and Performance & N+1 covers the
generic DataLoader pattern — batch and cache — that fixes the resulting round-trip explosion. Spring for
GraphQL gives that pattern two JVM entry points: @BatchMapping, a declarative shortcut for the common case,
and BatchLoaderRegistry, the lower-level API @BatchMapping itself is built on and the one to reach for when
a batch function needs arguments, filtering, or other context the shortcut cannot express.
@BatchMapping: the declarative shortcut
A @BatchMapping method receives the list of parent objects already resolved for the current tick, instead
of resolving its field once per parent. Spring for GraphQL correlates the returned values back to each parent
automatically:
@Controller
public class BookController {
private final AuthorRepository authorRepository;
public BookController(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
List<Long> authorIds = books.stream().map(Book::authorId).distinct().toList();
return Flux.fromIterable(authorRepository.findAllById(authorIds))
.collectMap(Author::id)
.map(byId -> books.stream()
.collect(Collectors.toMap(book -> book, book -> byId.get(book.authorId()))));
}
}
This registers a batch loading function for the Book.author field: Book instances are the keys, Author
instances the values, and one DataFetcher is transparently bound to that field for every query that selects
it. Beyond the List<K> of parents, a @BatchMapping method may also accept java.security.Principal,
@ContextValue, the raw GraphQLContext, or BatchLoaderEnvironment. The return type is equally flexible:
Mono<Map<K,V>> and Flux<V> for reactive implementations (a Flux result is matched back to parents by
position), or the imperative Map<K,V>/Collection<V> and their Callable-wrapped async variants for
non-reactive code — Execution & Resolvers covers the
same sync/async duality for ordinary resolvers. Link:
Spring for GraphQL — Batch Mapping.
Registering batch loaders directly with BatchLoaderRegistry
@BatchMapping is a shortcut over BatchLoaderRegistry, a bean the Boot starter declares automatically and
that any configuration class or controller can inject to register a batch function for a key/value type pair
directly — the option to reach for when the field needs an @Argument, or when a controller method wants to
call dataLoader.load(id) itself instead of letting Spring bind the field automatically:
@Configuration
public class DataLoaderConfig {
public DataLoaderConfig(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
Flux.fromIterable(authorRepository.findAllById(authorIds))
.collectMap(Author::id));
}
}
@Controller
public class BookController {
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> authorLoader) {
return authorLoader.load(book.authorId());
}
}
By default the DataLoader name matches the target entity’s simple class name, which is why the
@SchemaMapping method above can declare a generically typed DataLoader argument without naming it
explicitly. BatchLoaderRegistry also re-creates every registration once per request — so a batch function
never leaks state across requests — and propagates the same GraphQLContext to both the batch function and
any @SchemaMapping/@BatchMapping method reading it, which is the main reason applications register batch
loaders through it rather than building a raw DataLoaderRegistry by hand. Link:
Spring
for GraphQL — BatchLoaderRegistry.
The diagram below is specific to Spring’s per-request wiring — contrast it with the generic batch-and-cache
picture in Performance & N+1, which shows the same
dispatch behavior without any framework attached to it:
@EntityMapping: resolving federated entity references
A federation gateway resolves a type owned by another subgraph through an _entities query carrying a
"representation" — the type’s @key fields — for each instance it needs. @EntityMapping methods are how a
Spring for GraphQL subgraph answers that query for its own owned types:
@Controller
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@EntityMapping
public Book book(@Argument Long id) {
return bookRepository.findById(id).orElse(null);
}
@EntityMapping
public List<Book> book(@Argument List<Long> idList, DataLoader<Long, Book> bookLoader) {
return idList.stream().map(bookLoader::load).map(CompletableFuture::join).toList();
}
@SchemaMapping
public Author author(Book book) {
return book.author();
}
}
Each @Argument resolves one field out of the representation map — or a method may take the full
Map<String, Object> directly for a composite key — and the List-typed overload above is the batch variant
gateways use when a single _entities query asks for several instances of the same type at once, exactly the
same batching shape @BatchMapping uses for ordinary fields. This page only covers the resolver-side
signature; schema declaration, @key directives, and gateway composition belong to
Federation. Link:
Spring for GraphQL — Entity Mapping.
Spring Data integration: auto-registered data fetchers
Beyond hand-written controllers, Spring for GraphQL can wire a DataFetcher straight from a Spring Data
repository, provided the repository is annotated @GraphQlRepository and implements
QuerydslPredicateExecutor or QueryByExampleExecutor. Auto-registration only fills in a field that has no
existing DataFetcher, matching the repository’s domain type simple name to the schema type name by default
(override it with typeName when they differ), and it covers single-value, list, and paginated query shapes
alike:
@GraphQlRepository
public interface AuthorRepository extends JpaRepository<Author, Long>,
QuerydslPredicateExecutor<Author> {
}
Spring Data JPA covers what a Spring Data repository interface
is and how its derived queries, Specification, and QuerydslPredicateExecutor support work; the rest of this
section only covers the part specific to auto-registering that repository as a GraphQL data fetcher.
QuerydslDataFetcher
For a QuerydslPredicateExecutor repository, Spring for GraphQL derives a Predicate from the query’s
arguments using the generated Q* metamodel type, the same one
Spring Data JPA introduces for dynamic Criteria-free queries,
and a QuerydslBinderCustomizer declared on the repository itself is picked up automatically to customize how
individual arguments bind to predicate operators:
public interface AuthorRepository extends Repository<Author, Long>,
QuerydslPredicateExecutor<Author>, QuerydslBinderCustomizer<QAuthor> {
@Override
default void customize(QuerydslBindings bindings, QAuthor author) {
bindings.bind(author.name).first((path, value) -> path.containsIgnoreCase(value));
}
}
QueryByExampleDataFetcher
For a QueryByExampleExecutor repository, the same auto-registration builds an Example<T> from the query’s
arguments instead of a Predicate, matching only the non-null properties a client actually supplied — useful
when the schema’s field set already mirrors the entity closely enough that a generated Q* type would be
overkill:
public interface AuthorRepository extends Repository<Author, Long>,
QueryByExampleExecutor<Author> {
}
Pagination: Window, Slice, and keyset scrolling
Pagination documents the Relay Cursor Connections shape a schema
exposes to clients; on the resolver side, a Spring for GraphQL controller method binds that shape to a
ScrollSubrange argument and returns a Spring Data Window or Slice, which built-in ConnectionAdapter
implementations translate into Connection/Edge/PageInfo values automatically:
@Configuration
public class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer connectionCustomizer() {
CursorStrategy<ScrollPosition> strategy =
CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
new WindowConnectionAdapter(strategy),
new SliceConnectionAdapter(strategy)));
return builder -> builder.typeVisitors(List.of(visitor));
}
}
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(ScrollSubrange subrange, Sort sort) {
ScrollPosition position = subrange.position().orElse(ScrollPosition.keyset());
Limit limit = subrange.count().map(Limit::of).orElse(Limit.unlimited());
return bookRepository.findBy(sort, position, limit);
}
}
ScrollSubrange carries the decoded forward/backward cursor as an offset- or keyset-based ScrollPosition
plus the requested page size, mirroring the first/after/last/before arguments
Pagination documents at the schema level. Link:
Spring for GraphQL — Scroll.
Keyset cursors
With KeysetScrollPosition, the cursor encodes a keyset — a Map<String, Object> of the sort-key values
identifying the last row seen — rather than a numeric offset, which keeps pagination stable under concurrent
inserts and deletes the same way offset pagination cannot. The default JsonKeysetCursorStrategy serializes
that keyset to JSON but needs an explicit allow-list to restore anything beyond basic types safely:
@Bean
public JsonKeysetCursorStrategy keysetCursorStrategy() {
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.allowIfSubType(UUID.class)
.allowIfSubType(Number.class)
.allowIfSubType(Enum.class)
.allowIfSubType("java.time.")
.build();
return new JsonKeysetCursorStrategy(Jackson2ObjectMapperBuilder.json()
.polymorphicTypeValidator(validator)
.build());
}
Sorting: binding a Sort argument
A Sort controller method parameter is populated from GraphQL arguments through a SortStrategy bean — AbstractSortStrategy implements the plumbing and leaves only the two extraction methods (which properties
were requested, and in which direction) abstract:
@Bean
public SortStrategy sortStrategy() {
return new AbstractSortStrategy() {
@Override
protected List<String> getProperties(Object source) {
return List.of(((Map<?, ?>) source).get("field").toString());
}
@Override
protected boolean isDescending(Object source) {
return "DESC".equals(((Map<?, ?>) source).get("direction"));
}
};
}
No SortStrategy bean means no Sort argument binding — a controller method that declares one without a
strategy registered simply never has it populated. Link:
Spring for GraphQL — Sort.
Related pages
-
Performance & N+1— the language-agnostic batch-and- cache pattern@BatchMappingandBatchLoaderRegistryimplement on the JVM. -
Execution & Resolvers— the resolver signature and sync/async return shapes@BatchMapping/@SchemaMappingmethods build on. -
Federation— schema-level@keydeclarations and gateway composition behind the@EntityMappingmethods this page covers on the resolver side. -
Pagination— the RelayConnection/Edge/PageInfoshape thatWindow/Sliceresults are adapted into. -
Spring Data JPA— repository basics, derived queries, andQuerydslPredicateExecutor/Specificationsupport that this page’s auto-registered data fetchers build on. -
Spring Boot: Controllers— the@QueryMapping/@SchemaMappingannotations@BatchMappingand@EntityMappingsit alongside on the same controller class.