4. Request Execution
ExecutionGraphQlService
is the main Spring abstraction to call GraphQL Java to execute
requests. Underlying transports, such as the Server Transports, delegate to
ExecutionGraphQlService
to handle requests.
The main implementation, DefaultExecutionGraphQlService
, is configured with a
GraphQlSource
for access to the graphql.GraphQL
instance to invoke.
4.1. GraphQLSource
GraphQlSource
is a core Spring abstraction for access to the
graphql.GraphQL
instance to use for request execution. It provides a builder API to
initialize GraphQL Java and build a GraphQlSource
.
The default GraphQlSource
builder, accessible via
GraphQlSource.schemaResourceBuilder()
, enables support for
Reactive DataFetcher
, Context Propagation, and Exception Resolution.
The Spring Boot starter initializes a
GraphQlSource
instance through the default GraphQlSource.Builder
and also enables
the following:
-
Load schema files from a configurable location.
-
Expose properties that apply to
GraphQlSource.Builder
. -
Detect
RuntimeWiringConfigurer
beans. -
Detect Instrumentation beans for GraphQL metrics.
-
Detect
DataFetcherExceptionResolver
beans for exception resolution. -
Detect
SubscriptionExceptionResolver
beans for subscription exception resolution.
For further customizations, you can declare your own GraphQlSourceBuilderCustomizer
beans;
for example, for configuring your own ExecutionIdProvider
:
@Configuration(proxyBeanMethods = false)
class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl(graphQlBuilder ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
4.1.1. 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 Spring 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.
|
4.1.2. Schema Creation
By default, GraphQlSource.Builder
uses the GraphQL Java GraphQLSchemaGenerator
to
create the graphql.schema.GraphQLSchema
. This works for most applications, but if
necessary, you can hook into the schema creation through the builder:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
The primary reason for this is to create the schema through a federation library.
The GraphQlSource section explains how to configure that with Spring Boot.
4.1.3. RuntimeWiringConfigurer
You can use RuntimeWiringConfigurer
to register:
-
Custom scalar types.
-
Directives handling code.
-
TypeResolver
, if you need to override the DefaultTypeResolver
for a type. -
DataFetcher
for a field, although most applications will simply configureAnnotatedControllerConfigurer
, which detects annotated,DataFetcher
handler methods. The Spring Boot starter adds theAnnotatedControllerConfigurer
by default.
Unlike web frameworks, GraphQL does not use Jackson annotations to drive JSON serialization/deserialization. Custom data types and their serialization must be described as Scalars. |
The Spring 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 = ... ;
DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();
return wiringBuilder -> wiringBuilder
.scalar(scalarType)
.directiveWiring(directiveWiring)
.type("Query", builder -> builder.dataFetcher("book", dataFetcher));
}
}
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.
4.1.4. Default 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);
The GraphQlSource section explains how to configure that with Spring Boot.
4.1.5. 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 = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
The GraphQlSource section explains how to configure that with Spring Boot.
4.1.6. 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 Spring 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. |
4.2. 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.
4.3. Context Propagation
Spring for GraphQL provides support to transparently propagate context from the
Server Transports, 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.
4.3.1. 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 create a ThreadLocalAccessor
to extract
ThreadLocal
values of interest:
public class RequestAttributesAccessor implements ThreadLocalAccessor {
private static final String KEY = RequestAttributesAccessor.class.getName();
@Override
public void extractValues(Map<String, Object> container) {
container.put(KEY, RequestContextHolder.getRequestAttributes());
}
@Override
public void restoreValues(Map<String, Object> values) {
if (values.containsKey(KEY)) {
RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY));
}
}
@Override
public void resetValues(Map<String, Object> values) {
RequestContextHolder.resetRequestAttributes();
}
}
A ThreadLocalAccessor
can be registered in the WebGraphHandler
builder. The Boot starter detects beans of this type and automatically registers them for
Spring MVC application, see the
Web Endpoints section.
4.3.2. 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.
4.4. Exception Resolution
A GraphQL Java application can register a DataFetcherExceptionHandler
to decide how to
represent exceptions from the data layer in the "errors" section of the GraphQL response.
Spring for GraphQL has a built-in DataFetcherExceptionHandler
that is configured for use
by the default GraphQLSource
builder. It allows applications to register
one or more Spring DataFetcherExceptionResolver
components that are invoked sequentially
until one resolves the Exception
to a (possibly empty) list of graphql.GraphQLError
objects.
DataFetcherExceptionResolver
is an asynchronous contract. For most implementations, it
would be sufficient to extend DataFetcherExceptionResolverAdapter
and override
one of its resolveToSingleError
or resolveToMultipleErrors
methods that
resolve exceptions synchronously.
A GraphQLError
can be assigned to a category via graphql.ErrorClassification
.
In Spring GraphQL, you can also assign via ErrorType
which has the following common
classifications that applications can use to categorize errors:
-
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.
4.4.1. 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
.
4.4.2. 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.
4.5. 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.
4.5.1. 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.
4.5.2. 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 Spring 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 DataLoader
options.
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.
s
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.
4.5.3. 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("...");
// ...