786 lines
33 KiB
Plaintext
786 lines
33 KiB
Plaintext
[[execution]]
|
|
= Request Execution
|
|
|
|
`ExecutionGraphQlService` is the main Spring abstraction to call GraphQL Java to execute
|
|
requests. Underlying transports, such as the xref:transports.adoc#server.transports.http[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.
|
|
|
|
|
|
|
|
[[execution.graphqlsource]]
|
|
== `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 xref:boot-starter.adoc[Boot Starter] creates an instance of this builder and further initializes it
|
|
to xref:request-execution.adoc#execution.graphqlsource.schema-resources[load schema files] from a configurable location,
|
|
to {spring-boot-ref-docs}/application-properties.html#appendix.application-properties.web[expose properties]
|
|
to apply to `GraphQlSource.Builder`, to detect
|
|
xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[`RuntimeWiringConfigurer`] beans,
|
|
https://www.graphql-java.com/documentation/instrumentation[Instrumentation] beans for
|
|
{spring-boot-ref-docs}/actuator.html#actuator.metrics.supported.spring-graphql[GraphQL metrics],
|
|
and `DataFetcherExceptionResolver` and `SubscriptionExceptionResolver` beans for
|
|
xref:request-execution.adoc#execution.exceptions[exception resolution]. For further customizations, you can also
|
|
declare a `GraphQlSourceBuilderCustomizer` bean, for example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration(proxyBeanMethods = false)
|
|
class GraphQlConfig {
|
|
|
|
@Bean
|
|
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
|
|
return (builder) ->
|
|
builder.configureGraphQl(graphQlBuilder ->
|
|
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
|
|
[[execution.graphqlsource.schema-resources]]
|
|
=== 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
|
|
{spring-boot-ref-docs}/web.html#web.graphql.schema[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.
|
|
|
|
TIP: Use `classpath*:graphql/**/` to find schema files across multiple classpath
|
|
locations, e.g. across multiple modules.
|
|
|
|
|
|
[[execution.graphqlsource.schema-creation]]
|
|
=== 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, e.g. for federation, you can register a `schemaFactory` callback:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
GraphQlSource.Builder builder = ...
|
|
|
|
builder.schemaResources(..)
|
|
.configureRuntimeWiring(..)
|
|
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
|
|
// create GraphQLSchema
|
|
})
|
|
----
|
|
|
|
The xref:request-execution.adoc#execution.graphqlsource[GraphQlSource section] explains how to configure that with Spring Boot.
|
|
|
|
For an example with Apollo Federation, see
|
|
https://github.com/apollographql/federation-jvm-spring-example[federation-jvm-spring-example].
|
|
|
|
|
|
[[execution.graphqlsource.runtimewiring-configurer]]
|
|
=== `RuntimeWiringConfigurer`
|
|
|
|
You can use `RuntimeWiringConfigurer` to register:
|
|
|
|
- Custom scalar types.
|
|
- xref:request-execution.adoc#execution.graphqlsource.directives[Directives] handling code.
|
|
- Default xref:request-execution.adoc#execution.graphqlsource.default-type-resolver[`TypeResolver`] for interface and union types.
|
|
- `DataFetcher` for a field although applications will typically use xref:controllers.adoc[Annotated Controllers], and
|
|
those are detected and registered as ``DataFetcher``s by `AnnotatedControllerConfigurer`,
|
|
which is a `RuntimeWiringConfigurer`. The xref:boot-starter.adoc[Boot Starter] automatically registers
|
|
`AnnotatedControllerConfigurer`.
|
|
|
|
NOTE: 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 https://www.graphql-java.com/documentation/scalars/[custom scalar types].
|
|
|
|
The xref:boot-starter.adoc[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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@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.
|
|
|
|
|
|
[[execution.graphqlsource.default-type-resolver]]
|
|
=== `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 xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[`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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
GraphQlSource.Builder builder = ...
|
|
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
|
|
classNameTypeResolver.setClassNameExtractor((klass) -> {
|
|
// Implement Custom ClassName Extractor here
|
|
});
|
|
builder.defaultTypeResolver(classNameTypeResolver);
|
|
----
|
|
|
|
The xref:request-execution.adoc#execution.graphqlsource[GraphQlSource section] explains how to configure that with Spring Boot.
|
|
|
|
|
|
[[execution.graphqlsource.directives]]
|
|
=== 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
|
|
{graphql-java-docs}/sdl-directives/[Schema Directives] in the
|
|
GraphQL Java documentation.
|
|
|
|
In Spring GraphQL you can register a `SchemaDirectiveWiring` through a
|
|
xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[`RuntimeWiringConfigurer`]. The xref:boot-starter.adoc[Boot Starter] detects
|
|
such beans, so you might have something like:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
public class GraphQlConfig {
|
|
|
|
@Bean
|
|
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
|
|
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
TIP: For an example of directives support check out the
|
|
https://github.com/graphql-java/graphql-java-extended-validation[Extended Validation for Graphql Java]
|
|
library.
|
|
|
|
|
|
[[execution.graphqlsource.execution-strategy]]
|
|
=== `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
|
|
xref:request-execution.adoc#execution.exceptions[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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@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));
|
|
}
|
|
----
|
|
|
|
|
|
|
|
|
|
[[execution.graphqlsource.schema-transformation]]
|
|
=== 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 xref:request-execution.adoc#execution.graphqlsource.schema-traversal[Schema Traversal] so generally
|
|
prefer traversal to transformation unless you need to make schema changes.
|
|
|
|
|
|
[[execution.graphqlsource.schema-traversal]]
|
|
=== 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
|
|
xref:request-execution.adoc#execution.graphqlsource.schema-transformation[Schema Transformation], if you need to make changes to the schema.
|
|
|
|
|
|
[[execution.graphqlsource.schema-mapping-inspection]]
|
|
=== 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 on schema types returned
|
|
by an operation that are covered neither explicitly through a `DataFetcher`
|
|
registration, nor implicitly by the default `PropertyDataFetcher`, which looks for a
|
|
matching Java object property, will always be `null`.
|
|
|
|
GraphQL Java does not perform checks to ensure every schema field is covered, and that
|
|
can result in gaps that might not be discovered depending on test coverage. At runtime
|
|
you may get a "silent" `null`, or an error if the field is not nullable. As a lower level
|
|
library, GraphQL Java simply does not know enough about `DataFetcher` implementations and
|
|
their return types, and therefore can't compare schema type structure against Java object
|
|
structure.
|
|
|
|
Spring for GraphQL defines the `SelfDescribingDataFetcher` interface to allow a
|
|
`DataFetcher` to expose return type information. All Spring `DataFetcher` implementations
|
|
implement this interface. That includes those for xref:controllers.adoc[Annotated Controllers], and those for
|
|
xref:data.adoc#data.querydsl[Querydsl] and xref:data.adoc#data.querybyexample[Query by Example] Spring Data repositories. For annotated
|
|
controllers, the return type is derived from the declared return type on a
|
|
`@SchemaMapping` method.
|
|
|
|
On startup, Spring for GraphQL can inspect schema fields, `DataFetcher` registrations,
|
|
and the properties of Java objects returned from `DataFetcher` implementations to check
|
|
if all schema fields are covered either by an explicitly registered `DataFetcher`, or
|
|
a matching Java object property. The inspection also performs a reverse check looking for
|
|
`DataFetcher` registrations against schema fields that don't exist.
|
|
|
|
To enable inspection of schema mappings:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
GraphQlSource.Builder builder = ...
|
|
|
|
builder.schemaResources(..)
|
|
.inspectSchemaMappings(report -> {
|
|
logger.debug(report);
|
|
})
|
|
----
|
|
|
|
Below is an example report:
|
|
|
|
----
|
|
GraphQL schema inspection:
|
|
Unmapped fields: {Book=[title], Author[firstName, lastName]} // <1>
|
|
Unmapped registrations: {Book.reviews=BookController#reviews[1 args]} <2>
|
|
Skipped types: [BookOrAuthor] // <3>
|
|
----
|
|
|
|
<1> List of schema fields and their source types that are not mapped
|
|
<2> List of `DataFetcher` registrations on fields that don't exist
|
|
<3> List of schema types that are skipped, as explained next
|
|
|
|
There are limits to what schema field inspection can do, in particular when there is
|
|
insufficient Java type information. This is the case if an annotated controller method is
|
|
declared to return `java.lang.Object`, or if the return type has an unspecified generic
|
|
parameter such as `List<?>`, or if the `DataFetcher` does not implement
|
|
`SelfDescribingDataFetcher` and the return type is not even known. In such cases, the
|
|
Java object type structure remains unknown, and the schema type is listed as skipped in
|
|
the resulting report. For every skipped type, a DEBUG message is logged to indicate why
|
|
it was skipped.
|
|
|
|
Schema union types are always skipped because there is no way for a controller method to
|
|
declare such a return type in Java, and the Java type structure is unknown.
|
|
|
|
Schema interface types are supported only as far as fields declared directly, which are
|
|
compared against properties on the Java type declared by a `SelfDescribingDataFetcher`.
|
|
Additional fields on concrete implementations are not inspected. This could be improved
|
|
in a future release to also inspect schema `interface` implementation types and to try
|
|
to find a match among subtypes of the declared Java return type.
|
|
|
|
|
|
[[execution.graphqlsource.operation-caching]]
|
|
=== 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}/execution/#query-caching[GraphQL Java docs] provide more details on
|
|
query caching through a `PreparsedDocumentProvider`.
|
|
|
|
In Spring GraphQL you can register a `PreparsedDocumentProvider` through
|
|
`GraphQlSource.Builder#configureGraphQl`:
|
|
.
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
// 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))
|
|
----
|
|
|
|
The xref:request-execution.adoc#execution.graphqlsource[GraphQlSource section] explains how to configure that with Spring Boot.
|
|
|
|
|
|
|
|
|
|
[[execution.thread-model]]
|
|
== 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:
|
|
|
|
- <<execution.reactive-datafetcher, Reactive data fetchers>>, and those are
|
|
adapted to `CompletionStage` as expected by `AsyncExecutionStrategy`.
|
|
- `CompletionStage` as return value.
|
|
- Controller methods that are Kotlin coroutine methods.
|
|
- xref:controllers.adoc#controllers.schema-mapping[@SchemaMapping] and
|
|
xref:controllers.adoc#controllers.schema-mapping[@BatchMapping] methods can return
|
|
`Callable` that is submitted to an `Executor` such as the Spring Framework
|
|
`VirtualThreadTaskExecutor`. To enable this, you must configure an `Executor` on
|
|
`AnnotatedControllerConfigurer`.
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
[[execution.reactive-datafetcher]]
|
|
== 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
|
|
xref:request-execution.adoc#execution.context.webflux[WebFlux Context].
|
|
|
|
|
|
|
|
[[execution.context]]
|
|
== Context Propagation
|
|
|
|
Spring for GraphQL provides support to transparently propagate context from the
|
|
xref:transports.adoc#server.transports.http[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.
|
|
|
|
|
|
[[execution.context.webmvc]]
|
|
=== 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
|
|
xref:transports.adoc#server.interception[`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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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.
|
|
|
|
|
|
[[execution.context.webflux]]
|
|
=== WebFlux
|
|
|
|
A xref:request-execution.adoc#execution.reactive-datafetcher[Reactive `DataFetcher`] can rely on access to Reactor context that
|
|
originates from the WebFlux request handling chain. This includes Reactor context
|
|
added by xref:transports.adoc#server.interception[WebGraphQlInterceptor] components.
|
|
|
|
|
|
|
|
[[execution.exceptions]]
|
|
== 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 xref:request-execution.adoc#execution.graphqlsource[`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 xref:controllers.adoc[Annotated Controllers] programming model enables handling data fetching exceptions with
|
|
annotated exception handler methods with a flexible method signature, see
|
|
xref:controllers.adoc#controllers.exception-handler[`@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.
|
|
|
|
|
|
[[execution.exceptions.request]]
|
|
=== 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 xref:transports.adoc#server.interception.web[`WebGraphQlInterceptor`].
|
|
|
|
|
|
[[execution.exceptions.subscription]]
|
|
=== 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.
|
|
|
|
|
|
|
|
[[execution.pagination]]
|
|
== Pagination
|
|
|
|
The GraphQL https://relay.dev/graphql/connections.htm[Cursor Connection specification]
|
|
defines a way to navigate large result sets by returning a subset of items at a time where
|
|
each item is paired with a cursor that clients can use to request more items before or
|
|
after the referenced item.
|
|
|
|
The specification calls the pattern _"Connections"_. A schema type with a name that ends
|
|
on Connection is a _Connection Type_ that represents a paginated result set. All `~Connection`
|
|
types contain an "edges" field where `~Edge` type pairs the actual item with a cursor, as
|
|
well as a "pageInfo" field with boolean flags to indicate if there are more items forward
|
|
and backward.
|
|
|
|
|
|
[[execution.pagination.types]]
|
|
=== Connection Types
|
|
|
|
`Connection` type definitions must be created for every type that needs pagination, adding
|
|
boilerplate and noise to the schema. Spring for GraphQL provides
|
|
`ConnectionTypeDefinitionConfigurer` to add these types on startup, if not already
|
|
present in the parsed schema files. That means in the schema you only need this:
|
|
|
|
[source,graphql,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
Query {
|
|
books(first:Int, after:String, last:Int, before:String): BookConnection
|
|
}
|
|
|
|
type Book {
|
|
id: ID!
|
|
title: String!
|
|
}
|
|
----
|
|
|
|
Note the spec-defined forward pagination arguments `first` and `after` that clients can use
|
|
to request the first N items after the given cursor, while `last` and `before` are backward
|
|
pagination arguments to request the last N items before the given cursor.
|
|
|
|
Next, configure `ConnectionTypeDefinitionConfigurer` as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
GraphQlSource.schemaResourceBuilder()
|
|
.schemaResources(..)
|
|
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
|
|
----
|
|
|
|
and the following type definitions will be transparently added to the schema:
|
|
[source,graphql,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
type BookConnection {
|
|
edges: [BookEdge]!
|
|
pageInfo: PageInfo!
|
|
}
|
|
|
|
type BookEdge {
|
|
node: Book!
|
|
cursor: String!
|
|
}
|
|
|
|
type PageInfo {
|
|
hasPreviousPage: Boolean!
|
|
hasNextPage: Boolean!
|
|
startCursor: String
|
|
endCursor: String
|
|
}
|
|
----
|
|
|
|
The xref:boot-starter.adoc[Boot Starter] registers `ConnectionTypeDefinitionConfigurer` by default.
|
|
|
|
|
|
[[execution.pagination.adapters]]
|
|
=== `ConnectionAdapter`
|
|
|
|
Once xref:request-execution.adoc#execution.pagination.types[Connection Types] are available in the schema, you also need
|
|
equivalent Java types. GraphQL Java provides those, including generic `Connection` and
|
|
`Edge`, as well as a `PageInfo`.
|
|
|
|
One option is to populate a `Connection` and return it from your controller method or
|
|
`DataFetcher`. However, this requires boilerplate code to create the `Connection`,
|
|
creating cursors, wrapping each item as an `Edge`, and creating the `PageInfo`.
|
|
Moreover, you may already have an underlying pagination mechanism such as when using
|
|
Spring Data repositories.
|
|
|
|
Spring for GraphQL defines the `ConnectionAdapter` contract to adapt a container of items
|
|
to `Connection`. Adapters are applied through a `DataFetcher` decorator that is in turn
|
|
installed through a `ConnectionFieldTypeVisitor`. You can configure it as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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 xref:data.adoc#data.pagination.scroll[built-in] ``ConnectionAdapter``s for Spring Data's
|
|
`Window` and `Slice`. You can also create your own custom adapter. `ConnectionAdapter`
|
|
implementations rely on a xref:request-execution.adoc#execution.pagination.cursor.strategy[`CursorStrategy`] to
|
|
create cursors for returned items. The same strategy is also used to support the
|
|
xref:controllers.adoc#controllers.schema-mapping.subrange[`Subrange`] controller method argument that contains
|
|
pagination input.
|
|
|
|
|
|
[[execution.pagination.cursor.strategy]]
|
|
=== `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 xref:request-execution.adoc#execution.pagination.adapters[`ConnectionAdapter`] uses this to encode cursors for returned items.
|
|
xref:controllers.adoc[Annotated Controllers] methods, xref:data.adoc#data.querydsl[Querydsl] repositories, and xref:data.adoc#data.querybyexample[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 xref:data.adoc#data.pagination.scroll[built-in] `CursorStrategy` for the Spring Data
|
|
`ScrollPosition`. The xref:boot-starter.adoc[Boot Starter] registers a `CursorStrategy<ScrollPosition>` with
|
|
`Base64Encoder` when Spring Data is present.
|
|
|
|
|
|
[[execution.pagination.sort.strategy]]
|
|
=== 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 xref:data.adoc#data.pagination.sort[built-in] support for Spring Data's `Sort` as a controller
|
|
method argument. For this to work, you need to have a `SortStrategy` bean.
|
|
|
|
|
|
[[execution.batching]]
|
|
== 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.
|
|
|
|
|
|
[[execution.batching.dataloader]]
|
|
=== `DataLoader`
|
|
|
|
GraphQL Java provides a `DataLoader` mechanism for batch loading of related entities.
|
|
You can find the full details in the
|
|
{graphql-java-docs}/batching/[GraphQL Java docs]. Below is a
|
|
summary of how it works:
|
|
|
|
1. Register ``DataLoader``'s in the `DataLoaderRegistry` that can load entities, given unique keys.
|
|
2. ``DataFetcher``'s can access ``DataLoader``'s and use them to load entities by id.
|
|
3. A `DataLoader` defers loading by returning a future so it can be done in a batch.
|
|
4. ``DataLoader``'s maintain a per request cache of loaded entities that can further
|
|
improve efficiency.
|
|
|
|
|
|
[[execution.batching.batch-loader-registry]]
|
|
=== `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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@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 xref:boot-starter.adoc[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
|
|
xref:controllers.adoc#controllers.schema-mapping.data-loader[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
|
|
xref:controllers.adoc#controllers.batch-mapping[@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 xref:request-execution.adoc#execution.context[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.
|
|
|
|
|
|
[[execution.batching.testing]]
|
|
=== Testing Batch Loading
|
|
|
|
Start by having `BatchLoaderRegistry` perform registrations on a `DataLoaderRegistry`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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("...");
|
|
// ...
|
|
----
|