[[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}/appendix/application-properties/index.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 xref:observability.adoc#observability[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: include-code::GraphQlConfig[] [[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}/reference/web/spring-graphql.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, you can register a `schemaFactory` callback: [source,java,indent=0,subs="verbatim,quotes"] ---- GraphQlSource.Builder builder = ... builder.schemaResources(..) .configureRuntimeWiring(..) .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> { // create GraphQLSchema }) ---- See the xref:request-execution.adoc#execution.graphqlsource[GraphQlSource] section for how to configure this with Spring Boot. If interested in federation, please see the xref:federation.adoc[Federation] section. [[execution.graphqlsource.runtimewiring-configurer]] === `RuntimeWiringConfigurer` A `RuntimeWiringConfigurer` is useful to register the following: - Custom scalar types. - Code that handles xref:request-execution.adoc#execution.graphqlsource.directives[Directives]. - Direct `DataFetcher` registrations. - and more... TIP: 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`. 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 = ... ; 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`. 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); ---- See the xref:request-execution.adoc#execution.graphqlsource[GraphQlSource] section for how to configure this 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 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 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 xref:controllers.adoc[controller methods], for xref:data.adoc#data.querydsl[Querydsl] and for xref:data.adoc#data.querybyexample[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 corresponding `Class` 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: [source,java,indent=0,subs="verbatim,quotes"] ---- 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. [[execution.graphqlsource.schema-mapping-inspection-unions-interfaces]] ==== 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_ the `Class` 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 xref:request-execution.adoc#execution.graphqlsource.default-type-resolver[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: [source,java,indent=0,subs="verbatim,quotes"] ---- GraphQlSource.Builder builder = ... builder.schemaResources(..) .inspectSchemaMappings( initializer -> initializer.classMapping("Author", Author.class) logger::debug); ---- [[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)) ---- See the xref:request-execution.adoc#execution.graphqlsource[GraphQlSource] section for how to configure this 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: - <>, 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.timeout]] == GraphQL Request Timeout GraphQL clients can send requests that will consume lots of resources on the server side. There are many ways to protect against this, and one of them is to configure a request timeout. This ensures that requests are closed on the server side if the response takes too long to materialize. Spring for GraphQL provides a `TimeoutWebGraphQlInterceptor` for the web transports. Applications can configure this interceptor with a timeout duration; if the request times out, the server errors with a specific HTTP status. In this case, the interceptor will send a "cancel" signal up the chain and reactive data fetchers will automatically cancel any ongoing work. This interceptor can be configured on the `WebGraphQlHandler`: include-code::WebGraphQlHandlerTimeout[tag=interceptor,indent=0] In a Spring Boot application, contributing the interceptor as a bean is enough: include-code::HttpTimeoutConfiguration[] For more transport-specific timeouts, there are dedicated properties on the handler implementations like `GraphQlWebSocketHandler` and `GraphQlSseHandler`. [[execution.reactivedatafetcher]] == 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]. 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`: include-code::GraphQlConfig[] [[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 { @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`. 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 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. [[execution.pagination.types]] === 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: [source,graphql,indent=0,subs="verbatim,quotes"] ---- 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. NOTE: 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: [source,java,indent=0,subs="verbatim,quotes"] ---- GraphQlSource.schemaResourceBuilder() .schemaResources(..) .typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer) ---- The above will add the following type definitions: [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` In addition to xref:request-execution.adoc#execution.pagination.types[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: [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 built-in 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` 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 }); // 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`. 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 loader = dataLoaderRegistry.getDataLoader(Book.class.getName()); loader.load(1L); loader.loadMany(Arrays.asList(2L, 3L)); List books = loader.dispatchAndJoin(); // actual loading assertThat(books).hasSize(3); assertThat(books.get(0).getName()).isEqualTo("..."); // ... ----