This version is still in development and is not considered stable yet. For the latest stable version, please use Spring GraphQL 1.3.3! |
Request Execution
ExecutionGraphQlService
is the main Spring abstraction to call GraphQL Java to execute
requests. Underlying transports, such as the HTTP, delegate to
ExecutionGraphQlService
to handle requests.
The main implementation, DefaultExecutionGraphQlService
, is configured with a
GraphQlSource
for access to the graphql.GraphQL
instance to invoke.
GraphQLSource
GraphQlSource
is a contract to expose the graphql.GraphQL
instance to use that also
includes a builder API to build that instance. The default builder is available via
GraphQlSource.schemaResourceBuilder()
.
The Boot Starter creates an instance of this builder and further initializes it
to load schema files from a configurable location,
to expose properties
to apply to GraphQlSource.Builder
, to detect
RuntimeWiringConfigurer
beans,
Instrumentation beans for
GraphQL metrics,
and DataFetcherExceptionResolver
and SubscriptionExceptionResolver
beans for
exception resolution. For further customizations, you can also
declare a GraphQlSourceBuilderCustomizer
bean, for example:
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl((graphQlBuilder) ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
Schema Resources
GraphQlSource.Builder
can be configured with one or more Resource
instances to be
parsed and merged together. That means schema files can be loaded from just about any
location.
By default, the Boot starter
looks for schema files with extensions
".graphqls" or ".gqls" under the location classpath:graphql/**
, which is typically
src/main/resources/graphql
. You can also use a file system location, or any location
supported by the Spring Resource
hierarchy, including a custom implementation that
loads schema files from remote locations, from storage, or from memory.
Use classpath*:graphql/**/ to find schema files across multiple classpath
locations, e.g. across multiple modules.
|
Schema Creation
By default, GraphQlSource.Builder
uses the GraphQL Java SchemaGenerator
to create the
graphql.schema.GraphQLSchema
. This works for typical use, but if you need to use a
different generator, you can register a schemaFactory
callback:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
See the GraphQlSource section for how to configure this with Spring Boot.
If interested in federation, please see the Federation section.
RuntimeWiringConfigurer
A RuntimeWiringConfigurer
is useful to register the following:
-
Custom scalar types.
-
Code that handles Directives.
-
Direct
DataFetcher
registrations. -
and more…
Spring applications typically do not need to perform direct DataFetcher registrations.
Instead, controller method are registered as DataFetcher s via
AnnotatedControllerConfigurer , which is a RuntimeWiringConfigurer .
|
GraphQL Java, server applications use Jackson only for serialization to and from maps of data. Client input is parsed into a map. Server output is assembled into a map based on the field selection set. This means you can’t rely on Jackson serialization/deserialization annotations. Instead, you can use custom scalar types. |
The Boot Starter detects beans of type RuntimeWiringConfigurer
and
registers them in the GraphQlSource.Builder
. That means in most cases, you’ll' have
something like the following in your configuration:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {
GraphQLScalarType scalarType = ... ;
SchemaDirectiveWiring directiveWiring = ... ;
return wiringBuilder -> wiringBuilder
.scalar(scalarType)
.directiveWiring(directiveWiring);
}
}
If you need to add a WiringFactory
, e.g. to make registrations that take into account
schema definitions, implement the alternative configure
method that accepts both the
RuntimeWiring.Builder
and an output List<WiringFactory>
. This allows you to add any
number of factories that are then invoked in sequence.
TypeResolver
GraphQlSource.Builder
registers ClassNameTypeResolver
as the default TypeResolver
to use for GraphQL Interfaces and Unions that don’t already have such a registration
through a RuntimeWiringConfigurer
. The purpose of
a TypeResolver
in GraphQL Java is to determine the GraphQL Object type for values
returned from the DataFetcher
for a GraphQL Interface or Union field.
ClassNameTypeResolver
tries to match the simple class name of the value to a GraphQL
Object Type and if it is not successful, it also navigates its super types including
base classes and interfaces, looking for a match. ClassNameTypeResolver
provides an
option to configure a name extracting function along with Class
to GraphQL Object type
name mappings that should help to cover more corner cases:
GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);
See the GraphQlSource section for how to configure this with Spring Boot.
Directives
The GraphQL language supports directives that "describe alternate runtime execution and type validation behavior in a GraphQL document". Directives are similar to annotations in Java but declared on types, fields, fragments and operations in a GraphQL document.
GraphQL Java provides the SchemaDirectiveWiring
contract to help applications detect
and handle directives. For more details, see
Schema Directives in the
GraphQL Java documentation.
In Spring GraphQL you can register a SchemaDirectiveWiring
through a
RuntimeWiringConfigurer
. The Boot Starter detects
such beans, so you might have something like:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
For an example of directives support check out the Extended Validation for Graphql Java library. |
ExecutionStrategy
An ExecutionStrategy
in GraphQL Java drives the fetching of requested fields.
To create an ExecutionStrategy
, you need to provide a DataFetcherExceptionHandler
.
By default, Spring for GraphQL creates the exception handler to use as described in
Exceptions and sets it on the
GraphQL.Builder
. GraphQL Java then uses that to create AsyncExecutionStrategy
instances with the configured exception handler.
If you need to create a custom ExecutionStrategy
, you can detect
DataFetcherExceptionResolver
s and create an exception handler in the same way, and use
it to create the custom ExecutionStrategy
. For example, in a Spring Boot application:
@Bean
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer(
ObjectProvider<DataFetcherExceptionResolver> resolvers) {
DataFetcherExceptionHandler exceptionHandler =
DataFetcherExceptionResolver.createExceptionHandler(resolvers.stream().toList());
AsyncExecutionStrategy strategy = new CustomAsyncExecutionStrategy(exceptionHandler);
return sourceBuilder -> sourceBuilder.configureGraphQl(builder ->
builder.queryExecutionStrategy(strategy).mutationExecutionStrategy(strategy));
}
Schema Transformation
You can register a graphql.schema.GraphQLTypeVisitor
via
builder.schemaResources(..).typeVisitorsToTransformSchema(..)
if you want to traverse
and transform the schema after it is created, and make changes to the schema. Keep in mind
that this is more expensive than Schema Traversal so generally
prefer traversal to transformation unless you need to make schema changes.
Schema Traversal
You can register a graphql.schema.GraphQLTypeVisitor
via
builder.schemaResources(..).typeVisitors(..)
if you want to traverse the schema after
it is created, and possibly apply changes to the GraphQLCodeRegistry
. Keep in mind,
however, that such a visitor cannot change the schema. See
Schema Transformation, if you need to make changes to the schema.
Schema Mapping Inspection
If a query, mutation, or subscription operation does not have a DataFetcher
, it won’t
return any data, and won’t do anything useful. Likewise, fields of schema types that are
neither covered explicitly through a DataFetcher
registration, nor implicitly by the
default PropertyDataFetcher
that finds matching Class
properties, will always be null
.
GraphQL Java does not perform checks to ensure every schema field is covered, and as a
lower level library, GraphQL Java simply does not know what a DataFetcher
can return
or what arguments it depends on, and therefore cannot perform such verifications. This can
result in gaps that depending on test coverage may not be discovered until runtime when
clients may experience "silent" null
values, or non-null field errors.
The SelfDescribingDataFetcher
interface in Spring for GraphQL allows a DataFetcher
to
expose information such as return type and expected arguments. All built-in, Spring
DataFetcher
implementations for controller methods, for
Querydsl and for Query by Example
are implementations of this interface. For annotated controllers, the return type and
expected arguments are based on the controller method signature. This makes it possible
to inspect schema mappings on startup to ensure the following:
-
Schema fields have either a
DataFetcher
registration or a correspondingClass
property. -
DataFetcher
registrations refer to a schema field that exists. -
DataFetcher
arguments have matching schema field arguments.
To enable schema inspection, customize GraphQlSource.Builder
as shown below.
In this case the report is simply logged, but you can choose to take any action:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(report -> {
logger.debug(report);
});
An example report:
GraphQL schema inspection: Unmapped fields: {Book=[title], Author[firstName, lastName]} (1) Unmapped registrations: {Book.reviews=BookController#reviews[1 args]} (2) Unmapped arguments: {BookController#bookSearch[1 args]=[myAuthor]} (3) Skipped types: [BookOrAuthor] (4)
1 | Schema fields that are not covered in any way |
2 | DataFetcher registrations to fields that don’t exist |
3 | DataFetcher expected arguments that don’t exist |
4 | Schema types that have been skipped (explained next) |
In some cases, the Class
type for a schema type is unknown. Maybe the DataFetcher
does not
implement SelfDescribingDataFetcher
, or the declared return type is too general
(e.g. Object
) or unknown (e.g. List<?>
), or a DataFetcher
could be missing altogether.
In such cases, the schema type is listed as skipped as it could not be verified. For every
skipped type, a DEBUG message explains why it was skipped.
Unions and Interfaces
For unions, the inspection iterates over member types and tries to find the corresponding classes. For interfaces, the inspection iterates over implementation types and looks for the corresponding classes.
By default, corresponding Java classes can be detected out-of-the-box in the following cases:
-
The
Class
's simple name matches the GraphQL union member of interface implementation type name, and theClass
is located in the same package as the return type of the controller method, or controller class, mapped to the union or interface field. -
The
Class
is inspected in other parts of the schema where the mapped field is of a concrete union member or interface implementation type. -
You have registered a TypeResolver that has explicit
Class
to GraphQL type mappings .
In none the above help, and GraphQL types are reported as skipped in the schema inspection report, you can make the following customizations:
-
Explicitly map a GraphQL type name to a Java class or classes.
-
Configure a function that customizes how a GraphQL type name is adapted to a simple
Class
name. This can help with a specific Java class naming conventions. -
Provide a
ClassNameTypeResolver
to map a GraphQL type a Java classes.
For example:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(
initializer -> initializer.classMapping("Author", Author.class)
logger::debug);
Operation Caching
GraphQL Java must parse and validate an operation before executing it. This may impact
performance significantly. To avoid the need to re-parse and validate, an application may
configure a PreparsedDocumentProvider
that caches and reuses Document instances. The
GraphQL Java docs provide more details on
query caching through a PreparsedDocumentProvider
.
In Spring GraphQL you can register a PreparsedDocumentProvider
through
GraphQlSource.Builder#configureGraphQl
:
.
// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...
// Create provider
PreparsedDocumentProvider provider =
new ApolloPersistedQuerySupport(new InMemoryPersistedQueryCache(Collections.emptyMap()));
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
See the GraphQlSource section for how to configure this with Spring Boot.
Thread Model
Most GraphQL requests benefit from concurrent execution in fetching nested fields. This is
why most applications today rely on GraphQL Java’s AsyncExecutionStrategy
, which allows
data fetchers to return CompletionStage
and to execute concurrently rather than serially.
Java 21 and virtual threads add an important ability to use more threads efficiently, but it is still necessary to execute concurrently rather than serially in order for request execution to complete more quickly.
Spring for GraphQL supports:
-
Reactive data fetchers, and those are adapted to
CompletionStage
as expected byAsyncExecutionStrategy
. -
CompletionStage
as return value. -
Controller methods that are Kotlin coroutine methods.
-
@SchemaMapping and @BatchMapping methods can return
Callable
that is submitted to anExecutor
such as the Spring FrameworkVirtualThreadTaskExecutor
. To enable this, you must configure anExecutor
onAnnotatedControllerConfigurer
.
Spring for GraphQL runs on either Spring MVC or WebFlux as the transport. Spring MVC
uses async request execution, unless the resulting CompletableFuture
is done
immediately after the GraphQL Java engine returns, which would be the case if the
request is simple enough and did not require asynchronous data fetching.
Reactive DataFetcher
The default GraphQlSource
builder enables support for a DataFetcher
to return Mono
or Flux
which adapts those to a CompletableFuture
where Flux
values are aggregated
and turned into a List, unless the request is a GraphQL subscription request,
in which case the return value remains a Reactive Streams Publisher
for streaming
GraphQL responses.
A reactive DataFetcher
can rely on access to Reactor context propagated from the
transport layer, such as from a WebFlux request handling, see
WebFlux Context.
In the case of subscription requests, GraphQL Java will produce items as soon as they
are available and all their requested fields were fetched. Because this involves several
layers of asynchronous data fetching, items might be sent over the wire out of their
original order. If you wish GraphQL Java to buffer items and retain the original order,
you can do so by setting the SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED
configuration flag in the GraphQLContext
. This can be done, for example, with a custom
Instrumentation
:
import graphql.ExecutionResult;
import graphql.execution.SubscriptionExecutionStrategy;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentationContext;
import graphql.execution.instrumentation.SimplePerformantInstrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class GraphQlConfig {
@Bean
public SubscriptionOrderInstrumentation subscriptionOrderInstrumentation() {
return new SubscriptionOrderInstrumentation();
}
static class SubscriptionOrderInstrumentation extends SimplePerformantInstrumentation {
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
InstrumentationState state) {
// Enable option for keeping subscription results in upstream order
parameters.getGraphQLContext().put(SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED, true);
return SimpleInstrumentationContext.noOp();
}
}
}
Context Propagation
Spring for GraphQL provides support to transparently propagate context from the
HTTP transport, through GraphQL Java, and to
DataFetcher
and other components it invokes. This includes both ThreadLocal
context
from the Spring MVC request handling thread and Reactor Context
from the WebFlux
processing pipeline.
WebMvc
A DataFetcher
and other components invoked by GraphQL Java may not always execute on
the same thread as the Spring MVC handler, for example if an asynchronous
WebGraphQlInterceptor
or DataFetcher
switches to a
different thread.
Spring for GraphQL supports propagating ThreadLocal
values from the Servlet container
thread to the thread a DataFetcher
and other components invoked by GraphQL Java to
execute on. To do this, an application needs to implement
io.micrometer.context.ThreadLocalAccessor
for a ThreadLocal
values of interest:
public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {
@Override
public Object key() {
return RequestAttributesAccessor.class.getName();
}
@Override
public RequestAttributes getValue() {
return RequestContextHolder.getRequestAttributes();
}
@Override
public void setValue(RequestAttributes attributes) {
RequestContextHolder.setRequestAttributes(attributes);
}
@Override
public void reset() {
RequestContextHolder.resetRequestAttributes();
}
}
You can register a ThreadLocalAccessor
manually on startup with the global
ContextRegistry
instance, which is accessible via
io.micrometer.context.ContextRegistry#getInstance()
. You can also register it
automatically through the java.util.ServiceLoader
mechanism.
WebFlux
A Reactive DataFetcher
can rely on access to Reactor context that
originates from the WebFlux request handling chain. This includes Reactor context
added by WebGraphQlInterceptor components.
Exceptions
In GraphQL Java, DataFetcherExceptionHandler
decides how to represent exceptions from
data fetching in the "errors" section of the response. An application can register a
single handler only.
Spring for GraphQL registers a DataFetcherExceptionHandler
that provides default
handling and enables the DataFetcherExceptionResolver
contract. An application can
register any number of resolvers via GraphQLSource
builder and those are in
order until one them resolves the Exception
to a List<graphql.GraphQLError>
.
The Spring Boot starter detects beans of this type.
DataFetcherExceptionResolverAdapter
is a convenient base class with protected methods
resolveToSingleError
and resolveToMultipleErrors
.
The Annotated Controllers programming model enables handling data fetching exceptions with
annotated exception handler methods with a flexible method signature, see
@GraphQlExceptionHandler
for details.
A GraphQLError
can be assigned to a category based on the GraphQL Java
graphql.ErrorClassification
, or the Spring GraphQL ErrorType
, which defines the following:
-
BAD_REQUEST
-
UNAUTHORIZED
-
FORBIDDEN
-
NOT_FOUND
-
INTERNAL_ERROR
If an exception remains unresolved, by default it is categorized as an INTERNAL_ERROR
with a generic message that includes the category name and the executionId
from
DataFetchingEnvironment
. The message is intentionally opaque to avoid leaking
implementation details. Applications can use a DataFetcherExceptionResolver
to customize
error details.
Unresolved exception are logged at ERROR level along with the executionId
to correlate
to the error sent to the client. Resolved exceptions are logged at DEBUG level.
Request Exceptions
The GraphQL Java engine may run into validation or other errors when parsing the request
and that in turn prevent request execution. In such cases, the response contains a
"data" key with null
and one or more request-level "errors" that are global, i.e. not
having a field path.
DataFetcherExceptionResolver
cannot handle such global errors because they are raised
before execution begins and before any DataFetcher
is invoked. An application can use
transport level interceptors to inspect and transform errors in the ExecutionResult
.
See examples under WebGraphQlInterceptor
.
Subscription Exceptions
The Publisher
for a subscription request may complete with an error signal in which case
the underlying transport (e.g. WebSocket) sends a final "error" type message with a list
of GraphQL errors.
DataFetcherExceptionResolver
cannot resolve errors from a subscription Publisher
,
since the data DataFetcher
only creates the Publisher
initially. After that, the
transport subscribes to the Publisher
that may then complete with an error.
An application can register a SubscriptionExceptionResolver
in order to resolve
exceptions from a subscription Publisher
in order to resolve those to GraphQL errors
to send to the client.
Pagination
The GraphQL Cursor Connection specification defines a way to navigate large result sets by returning a subset of items at a time in which each item is paired with a cursor that clients can use to request more items before or after the referenced item.
The specification calls this pattern "Connections", and schema types whose name end
with ~Connection
are a connection type that represents a paginated result set.
All connection types contain a field called "edges" where an ~Edge
type contains
the actual item, a cursor, and a field called "pageInfo" that indicates if more
items exist forward and backward.
Connection Types
Connection types require boilerplate definitions that Spring for GraphQL’s
ConnectionTypeDefinitionConfigurer
can add transparently on startup, if not explicitly
declared. That means you only need the below, and the connection and edge types will
be added for you:
Query {
books(first:Int, after:String, last:Int, before:String): BookConnection
}
type Book {
id: ID!
title: String!
}
The spec defined first
and after
arguments for forward pagination allow clients to
request the "first" N items "after" a given cursor. Similarly, the last
and before
arguments for backward pagination arguments allow requesting the "last" N items "before"
a given cursor.
The spec discourages including both first and last and also states the outcome
for pagination becomes unclear. In Spring for GraphQL if first or after are present,
then last and before are ignored.
|
To have connection types generated, configure ConnectionTypeDefinitionConfigurer
as follows:
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
The above will add the following type definitions:
type BookConnection {
edges: [BookEdge]!
pageInfo: PageInfo!
}
type BookEdge {
node: Book!
cursor: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
The Boot Starter registers ConnectionTypeDefinitionConfigurer
by default.
ConnectionAdapter
In addition to
Connection Types in the schema,
you will also need equivalent Java types. GraphQL Java provides those, including generic
Connection
and Edge
types, and PageInfo
.
You can return Connection
from a controller method, but it requires boilerplate code
to adapt your underlying data pagination mechanism to Connection
, to create cursors,
add ~Edge
wrappers, and create a PageInfo
.
Spring for GraphQL defines the ConnectionAdapter
contract to adapt a container of items
to Connection
. Adapters are invoked from a DataFetcher
decorator that is in turn
added by a ConnectionFieldTypeVisitor
. You can configure it as follows:
ConnectionAdapter adapter = ... ;
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) (1)
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)) (2)
1 | Create type visitor with one or more ConnectionAdapter s. |
2 | Resister the type visitor. |
There are built-in built-in ConnectionAdapter
s
for Spring Data’s Window
and Slice
. You can also create your own custom adapter.
ConnectionAdapter
implementations rely on a
CursorStrategy
to
create cursors for returned items. The same strategy is also used to support the
Subrange
controller method
argument that contains pagination input.
CursorStrategy
CursorStrategy
is a contract to encode and decode a String cursor that refers to the
position of an item within a large result set. The cursor can be based on an index or
on a keyset.
A ConnectionAdapter
uses this to encode cursors for returned items.
Annotated Controllers methods, Querydsl repositories, and Query by Example
repositories use it to decode cursors from pagination requests, and create a Subrange
.
CursorEncoder
is a related contract that further encodes and decodes String cursors to
make them opaque to clients. EncodingCursorStrategy
combines CursorStrategy
with a
CursorEncoder
. You can use Base64CursorEncoder
, NoOpEncoder
or create your own.
There is a built-in CursorStrategy
for the Spring Data
ScrollPosition
. The Boot Starter registers a CursorStrategy<ScrollPosition>
with
Base64Encoder
when Spring Data is present.
Sort
There is no standard way to provide sort information in a GraphQL request. However, pagination depends on a stable sort order. You can use a default order, or otherwise expose input types and extract sort details from GraphQL arguments.
There is built-in support for Spring Data’s Sort
as a controller
method argument. For this to work, you need to have a SortStrategy
bean.
Batch Loading
Given a Book
and its Author
, we can create one DataFetcher
for a book and another
for its author. This allows selecting books with or without authors, but it means books
and authors aren’t loaded together, which is especially inefficient when querying multiple
books as the author for each book is loaded individually. This is known as the N+1 select
problem.
DataLoader
GraphQL Java provides a DataLoader
mechanism for batch loading of related entities.
You can find the full details in the
GraphQL Java docs. Below is a
summary of how it works:
-
Register
DataLoader
's in theDataLoaderRegistry
that can load entities, given unique keys. -
DataFetcher
's can accessDataLoader
's and use them to load entities by id. -
A
DataLoader
defers loading by returning a future so it can be done in a batch. -
DataLoader
's maintain a per request cache of loaded entities that can further improve efficiency.
BatchLoaderRegistry
The complete batching loading mechanism in GraphQL Java requires implementing one of
several BatchLoader
interface, then wrapping and registering those as DataLoader
s
with a name in the DataLoaderRegistry
.
The API in Spring GraphQL is slightly different. For registration, there is only one,
central BatchLoaderRegistry
exposing factory methods and a builder to create and
register any number of batch loading functions:
@Configuration
public class MyConfig {
public MyConfig(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Mono<Map<Long, Author>
});
// more registrations ...
}
}
The Boot Starter declares a BatchLoaderRegistry
bean that you can inject into
your configuration, as shown above, or into any component such as a controller in order
register batch loading functions. In turn the BatchLoaderRegistry
is injected into
DefaultExecutionGraphQlService
where it ensures DataLoader
registrations per request.
By default, the DataLoader
name is based on the class name of the target entity.
This allows an @SchemaMapping
method to declare a
DataLoader argument with a generic type, and
without the need for specifying a name. The name, however, can be customized through the
BatchLoaderRegistry
builder, if necessary, along with other DataLoaderOptions
.
To configure default DataLoaderOptions
globally, to use as a starting point for any
registration, you can override Boot’s BatchLoaderRegistry
bean and use the constructor
for DefaultBatchLoaderRegistry
that accepts Supplier<DataLoaderOptions>
.
For many cases, when loading related entities, you can use
@BatchMapping controller methods, which are a shortcut
for and replace the need to use BatchLoaderRegistry
and DataLoader
directly.
BatchLoaderRegistry
provides other important benefits too. It supports access to
the same GraphQLContext
from batch loading functions and from @BatchMapping
methods,
as well as ensures Context Propagation to them. This is why applications are expected
to use it. It is possible to perform your own DataLoader
registrations directly but
such registrations would forgo the above benefits.
Testing Batch Loading
Start by having BatchLoaderRegistry
perform registrations on a DataLoaderRegistry
:
BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...
DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);
Now you can access and test individual DataLoader
's as follows:
DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading
assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...