From 992b3f43443b9a132e749ce1fa42811fa6d27e17 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 15 May 2023 10:46:03 +0200 Subject: [PATCH] Extract large sections of ref docs into includes --- .../docs/asciidoc/includes/controllers.adoc | 756 ++++++ .../src/docs/asciidoc/includes/data.adoc | 482 ++++ .../asciidoc/includes/request-execution.adoc | 718 ++++++ .../docs/asciidoc/includes/transports.adoc | 147 ++ .../src/docs/asciidoc/index.adoc | 2117 +---------------- 5 files changed, 2107 insertions(+), 2113 deletions(-) create mode 100644 spring-graphql-docs/src/docs/asciidoc/includes/controllers.adoc create mode 100644 spring-graphql-docs/src/docs/asciidoc/includes/data.adoc create mode 100644 spring-graphql-docs/src/docs/asciidoc/includes/request-execution.adoc create mode 100644 spring-graphql-docs/src/docs/asciidoc/includes/transports.adoc diff --git a/spring-graphql-docs/src/docs/asciidoc/includes/controllers.adoc b/spring-graphql-docs/src/docs/asciidoc/includes/controllers.adoc new file mode 100644 index 00000000..47f7b93a --- /dev/null +++ b/spring-graphql-docs/src/docs/asciidoc/includes/controllers.adoc @@ -0,0 +1,756 @@ +[[controllers]] += Annotated Controllers + +Spring for GraphQL provides an annotation-based programming model where `@Controller` +components use annotations to declare handler methods with flexible method signatures to +fetch the data for specific GraphQL fields. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class GreetingController { + + @QueryMapping // <1> + public String hello() { // <2> + return "Hello, world!"; + } + + } +---- +<1> Bind this method to a query, i.e. a field under the Query type. +<2> Determine the query from the method name if not declared on the annotation. + +Spring for GraphQL uses `RuntimeWiring.Builder` to register the above handler method as a +`graphql.schema.DataFetcher` for the query named "hello". + + +[[controllers-declaration]] +== Declaration + +You can define `@Controller` beans as standard Spring bean definitions. The +`@Controller` stereotype allows for auto-detection, aligned with Spring general +support for detecting `@Controller` and `@Component` classes on the classpath and +auto-registering bean definitions for them. It also acts as a stereotype for the annotated +class, indicating its role as a data fetching component in a GraphQL application. + +`AnnotatedControllerConfigurer` detects `@Controller` beans and registers their +annotated handler methods as ``DataFetcher``s via `RuntimeWiring.Builder`. It is an +implementation of `RuntimeWiringConfigurer` which can be added to `GraphQlSource.Builder`. +The <> automatically declares `AnnotatedControllerConfigurer` as a bean +and adds all `RuntimeWiringConfigurer` beans to `GraphQlSource.Builder` and that enables +support for annotated ``DataFetcher``s, see the +{spring-boot-ref-docs}/web.html#web.graphql.runtimewiring[GraphQL RuntimeWiring] section +in the Boot starter documentation. + + +[[controllers.schema-mapping]] +== `@SchemaMapping` + +The `@SchemaMapping` annotation maps a handler method to a field in the GraphQL schema +and declares it to be the `DataFetcher` for that field. The annotation can specify the +parent type name, and the field name: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @SchemaMapping(typeName="Book", field="author") + public Author getAuthor(Book book) { + // ... + } + } +---- + +The `@SchemaMapping` annotation can also leave out those attributes, in which case the +field name defaults to the method name, while the type name defaults to the simple class +name of the source/parent object injected into the method. For example, the below +defaults to type "Book" and field "author": + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @SchemaMapping + public Author author(Book book) { + // ... + } + } +---- + +The `@SchemaMapping` annotation can be declared at the class level to specify a default +type name for all handler methods in the class. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + @SchemaMapping(typeName="Book") + public class BookController { + + // @SchemaMapping methods for fields of the "Book" type + + } +---- + +`@QueryMapping`, `@MutationMapping`, and `@SubscriptionMapping` are meta annotations that +are themselves annotated with `@SchemaMapping` and have the typeName preset to `Query`, +`Mutation`, or `Subscription` respectively. Effectively, these are shortcut annotations +for fields under the Query, Mutation, and Subscription types respectively. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @QueryMapping + public Book bookById(@Argument Long id) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInput bookInput) { + // ... + } + + @SubscriptionMapping + public Flux newPublications() { + // ... + } + } +---- + +`@SchemaMapping` handler methods have flexible signatures and can choose from a range of +method arguments and return values.. + + +[[controllers.schema-mapping.signature]] +=== Method Signature + +Schema mapping handler methods can have any of the following method arguments: + +[cols="1,2"] +|=== +| Method Argument | Description + +| `@Argument` +| For access to a named field argument bound to a higher-level, typed Object. + +See <>. + +| `@Argument Map` +| For access to the raw argument value. + +See <>. + +| `ArgumentValue` +| For access to a named field argument bound to a higher-level, typed Object along +with a flag to indicate if the input argument was omitted vs set to `null`. + +See <>. + +| `@Arguments` +| For access to all field arguments bound to a higher-level, typed Object. + +See <>. + +| `@Arguments Map` +| For access to the raw map of arguments. + +| `@ProjectedPayload` Interface +| For access to field arguments through a project interface. + +See <>. + +| "Source" +| For access to the source (i.e. parent/container) instance of the field. + +See <>. + +| `Subrange` and `ScrollSubrange` +| For access to pagination arguments. + +See <>, <>, <>. + +| `Sort` +| For access to sort details. + +See <>, <>. + +| `DataLoader` +| For access to a `DataLoader` in the `DataLoaderRegistry`. + +See <>. + +| `@ContextValue` +| For access to an attribute from the main `GraphQLContext` in `DataFetchingEnvironment`. + +| `@LocalContextValue` +| For access to an attribute from the local `GraphQLContext` in `DataFetchingEnvironment`. + +| `GraphQLContext` +| For access to the context from the `DataFetchingEnvironment`. + +| `java.security.Principal` +| Obtained from the Spring Security context, if available. + +| `@AuthenticationPrincipal` +| For access to `Authentication#getPrincipal()` from the Spring Security context. + +| `DataFetchingFieldSelectionSet` +| For access to the selection set for the query through the `DataFetchingEnvironment`. + +| `Locale`, `Optional` +| For access to the `Locale` from the `DataFetchingEnvironment`. + +| `DataFetchingEnvironment` +| For direct access to the underlying `DataFetchingEnvironment`. + +|=== + +Schema mapping handler methods can return: + +- A resolved value of any type. +- `Mono` and `Flux` for asynchronous value(s). Supported for controller methods and for + any `DataFetcher` as described in <>. +- `java.util.concurrent.Callable` to have the value(s) produced asynchronously. + For this to work, `AnnotatedControllerConfigurer` must be configured with an `Executor`. + + +[[controllers.schema-mapping.argument]] +=== `@Argument` + +In GraphQL Java, `DataFetchingEnvironment` provides access to a map of field-specific +argument values. The values can be simple scalar values (e.g. String, Long), a `Map` of +values for more complex input, or a `List` of values. + +Use the `@Argument` annotation to have an argument bound to a target object and +injected into the handler method. Binding is performed by mapping argument values to a +primary data constructor of the expected method parameter type, or by using a default +constructor to create the object and then map argument values to its properties. This is +repeated recursively, using all nested argument values and creating nested target objects +accordingly. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @QueryMapping + public Book bookById(@Argument Long id) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInput bookInput) { + // ... + } + } +---- + +TIP: If the target object doesn't have setters, and you can't change that, you can use a +property on `AnnotatedControllerConfigurer` to allow falling back on binding via direct +field access. + +By default, if the method parameter name is available (requires the `-parameters` compiler +flag with Java 8+ or debugging info from the compiler), it is used to look up the argument. +If needed, you can customize the name through the annotation, e.g. `@Argument("bookInput")`. + +TIP: The `@Argument` annotation does not have a "required" flag, nor the option to +specify a default value. Both of these can be specified at the GraphQL schema level and +are enforced by GraphQL Java. + +If binding fails, a `BindException` is raised with binding issues accumulated as field +errors where the `field` of each error is the argument path where the issue occurred. + +You can use `@Argument` with a `Map` argument, to obtain the raw value of +the argument. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @MutationMapping + public Book addBook(@Argument Map bookInput) { + // ... + } + } +---- + +NOTE: Prior to 1.2, `@Argument Map` returned the full arguments map if +the annotation did not specify a name. After 1.2, `@Argument` with +`Map` always returns the raw argument value, matching either to the name +specified in the annotation, or to the parameter name. For access to the full arguments +map, please use <> instead. + + +[[controllers.schema-mapping.argument-value]] +=== `ArgumentValue` + +By default, input arguments in GraphQL are nullable and optional, which means an argument +can be set to the `null` literal, or not provided at all. This distinction is useful for +partial updates with a mutation where the underlying data may also be, either set to +`null` or not changed at all accordingly. When using <> +there is no way to make such a distinction, because you would get `null` or an empty +`Optional` in both cases. + +If you want to know not whether a value was not provided at all, you can declare an +`ArgumentValue` method parameter, which is a simple container for the resulting value, +along with a flag to indicate whether the input argument was omitted altogether. You +can use this instead of `@Argument`, in which case the argument name is determined from +the method parameter name, or together with `@Argument` to specify the argument name. + +For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @MutationMapping + public void addBook(ArgumentValue bookInput) { + if (!bookInput.isOmitted()) { + BookInput value = bookInput.value(); + // ... + } + } + } +---- + +`ArgumentValue` is also supported as a field within the object structure of an `@Argument` +method parameter, either initialized via a constructor argument or via a setter, including +as a field of an object nested at any level below the top level object. + + +[[controllers.schema-mapping.arguments]] +=== `@Arguments` + +Use the `@Arguments` annotation, if you want to bind the full arguments map onto a single +target Object, in contrast to `@Argument`, which binds a specific, named argument. + +For example, `@Argument BookInput bookInput` uses the value of the argument "bookInput" +to initialize `BookInput`, while `@Arguments` uses the full arguments map and in that +case, top-level arguments are bound to `BookInput` properties. + +You can use `@Arguments` with a `Map` argument, to obtain the raw map of +all argument values. + + +[[controllers.schema-mapping.projectedpayload.argument]] +=== `@ProjectedPayload` Interface + +As an alternative to using complete Objects with <>, +you can also use a projection interface to access GraphQL request arguments through a +well-defined, minimal interface. Argument projections are provided by +https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces[Spring Data's Interface projections] +when Spring Data is on the class path. + +To make use of this, create an interface annotated with `@ProjectedPayload` and declare +it as a controller method parameter. If the parameter is annotated with `@Argument`, +it applies to an individual argument within the `DataFetchingEnvironment.getArguments()` +map. When declared without `@Argument`, the projection works on top-level arguments in +the complete arguments map. + +For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @QueryMapping + public Book bookById(BookIdProjection bookId) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInputProjection bookInput) { + // ... + } + } + + @ProjectedPayload + interface BookIdProjection { + + Long getId(); + } + + @ProjectedPayload + interface BookInputProjection { + + String getName(); + + @Value("#{target.author + ' ' + target.name}") + String getAuthorAndName(); + } +---- + + + +[[controllers.schema-mapping.source]] +=== Source + +In GraphQL Java, the `DataFetchingEnvironment` provides access to the source (i.e. +parent/container) instance of the field. To access this, simply declare a method parameter +of the expected target type. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @SchemaMapping + public Author author(Book book) { + // ... + } + } +---- + +The source method argument also helps to determine the type name for the mapping. +If the simple name of the Java class matches the GraphQL type, then there is no need to +explicitly specify the type name in the `@SchemaMapping` annotation. + +[TIP] +==== +A <> handler method can batch load all authors for a query, +given a list of source/parent books objects. +==== + + +[[controllers.schema-mapping.subrange]] +=== `Subrange` + +When there is a <> bean in Spring configuration, +controller methods support a `Subrange

` argument where `

` is a relative position +converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`. +For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Controller +public class BookController { + + @QueryMapping + public Window books(ScrollSubrange subrange) { + ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial()) + int count = subrange.count().orElse(20); + // ... + } + +} +---- + +See <> for an overview of pagination and of built-in mechanisms. + + +[[controllers.schema-mapping.sort]] +=== `Sort` + +When there is a <> bean in Spring configuration, controller +methods support `Sort` as a method argument. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Controller +public class BookController { + + @QueryMapping + public Window books(Optional optionalSort) { + Sort sort = optionalSort.orElse(Sort.by(..)); + } + +} +---- + + + +[[controllers.schema-mapping.data-loader]] +=== `DataLoader` + +When you register a batch loading function for an entity, as explained in +<>, you can access the `DataLoader` for the entity by declaring a +method argument of type `DataLoader` and use it to load the entity: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Controller +public class BookController { + + public BookController(BatchLoaderRegistry registry) { + registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { + // return Map + }); + } + + @SchemaMapping + public CompletableFuture author(Book book, DataLoader loader) { + return loader.load(book.getAuthorId()); + } + +} +---- + +By default, `BatchLoaderRegistry` uses the full class name of the value type (e.g. the +class name for `Author`) for the key of the registration, and therefore simply declaring +the `DataLoader` method argument with generic types provides enough information +to locate it in the `DataLoaderRegistry`. As a fallback, the `DataLoader` method argument +resolver will also try the method argument name as the key but typically that should not +be necessary. + +Note that for many cases with loading related entities, where the `@SchemaMapping` simply +delegates to a `DataLoader`, you can reduce boilerplate by using a +<> method as described in the next section. + + +[[controllers.schema-mapping.validation]] +=== Validation + +When a `javax.validation.Validator` bean is found, `AnnotatedControllerConfigurer` enables support for +{spring-framework-ref-docs}/core.html#validation-beanvalidation-overview[Bean Validation] +on annotated controller methods. Typically, the bean is of type `LocalValidatorFactoryBean`. + +Bean validation lets you declare constraints on types: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +public class BookInput { + + @NotNull + private String title; + + @NotNull + @Size(max=13) + private String isbn; +} +---- + +You can then annotate a controller method parameter with `@Valid` to validate it before +method invocation: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @MutationMapping + public Book addBook(@Argument @Valid BookInput bookInput) { + // ... + } + } +---- + +If an error occurs during validation, a `ConstraintViolationException` is raised. +You can use the <> chain to decide how to present that to clients +by turning it into an error to include in the GraphQL response. + +TIP: In addition to `@Valid`, you can also use Spring's `@Validated` that allows +specifying validation groups. + +Bean validation is useful for <>, +<>, and +<> +method parameters, but applies more generally to any method parameter. + +[WARNING] +.Validation and Kotlin Coroutines +==== +Hibernate Validator is not compatible with Kotlin Coroutine methods and fails when +introspecting their method parameters. Please see +https://github.com/spring-projects/spring-graphql/issues/344#issuecomment-1082814093[spring-projects/spring-graphql#344 (comment)] +for links to relevant issues and a suggested workaround. +==== + + + +[[controllers.batch-mapping]] +== `@BatchMapping` + +<> addresses the N+1 select problem through the use of an +`org.dataloader.DataLoader` to defer the loading of individual entity instances, so they +can be loaded together. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Controller +public class BookController { + + public BookController(BatchLoaderRegistry registry) { + registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { + // return Map + }); + } + + @SchemaMapping + public CompletableFuture author(Book book, DataLoader loader) { + return loader.load(book.getAuthorId()); + } + +} +---- + +For the straight-forward case of loading an associated entity, shown above, the +`@SchemaMapping` method does nothing more than delegate to the `DataLoader`. This is +boilerplate that can be avoided with a `@BatchMapping` method. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @BatchMapping + public Mono> author(List books) { + // ... + } + } +---- + +The above becomes a batch loading function in the `BatchLoaderRegistry` +where keys are `Book` instances and the loaded values their authors. In addition, a +`DataFetcher` is also transparently bound to the `author` field of the type `Book`, which +simply delegates to the `DataLoader` for authors, given its source/parent `Book` instance. + +[TIP] +==== +To be used as a unique key, `Book` must implement `hashcode` and `equals`. +==== + +By default, the field name defaults to the method name, while the type name defaults to +the simple class name of the input `List` element type. Both can be customized through +annotation attributes. The type name can also be inherited from a class level +`@SchemaMapping`. + + +[[controllers.batch-mapping.signature]] +=== Method Signature + +Batch mapping methods support the following arguments: + +[cols="1,2"] +|=== +| Method Argument | Description + +| `List` +| The source/parent objects. + +| `java.security.Principal` +| Obtained from Spring Security context, if available. + +| `@ContextValue` +| For access to a value from the `GraphQLContext` of `BatchLoaderEnvironment`, + which is the same context as the one from the `DataFetchingEnvironment`. + +| `GraphQLContext` +| For access to the context from the `BatchLoaderEnvironment`, + which is the same context as the one from the `DataFetchingEnvironment`. + +| `BatchLoaderEnvironment` +| The environment that is available in GraphQL Java to a +`org.dataloader.BatchLoaderWithContext`. + + +|=== + +Batch mapping methods can return: + +[cols="1,2"] +|=== +| Return Type | Description + +| `Mono>` +| A map with parent objects as keys, and batch loaded objects as values. + +| `Flux` +| A sequence of batch loaded objects that must be in the same order as the source/parent + objects passed into the method. + +| `Map`, `Collection` +| Imperative variants, e.g. without remote calls to make. + +| `Callable>`, `Callable>` +| Imperative variants to be invoked asynchronously. For this to work, + `AnnotatedControllerConfigurer` must be configured with an `Executor`. + +|=== + + + +[[controllers.exception-handler]] +== `@GraphQlExceptionHandler` + +Use `@GraphQlExceptionHandler` methods to handle exceptions from data fetching with a +flexible <>. When declared in a +controller, exception handler methods apply to exceptions from the same controller: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @QueryMapping + public Book bookById(@Argument Long id) { + // ... + } + + @GraphQlExceptionHandler + public GraphQLError handle(BindException ex) { + return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); + } + + } +---- + +When declared in an `@ControllerAdvice`, exception handler methods apply across controllers: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @ControllerAdvice + public class GlobalExceptionHandler { + + @GraphQlExceptionHandler + public GraphQLError handle(BindException ex) { + return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); + } + + } +---- + +Exception handling via `@GraphQlExceptionHandler` methods is applied automatically to +controller invocations. To handle exceptions from other `graphql.schema.DataFetcher` +implementations, not based on controller methods, obtain a +`DataFetcherExceptionResolver` from `AnnotatedControllerConfigurer`, and register it in +`GraphQlSource.Builder` as a <>. + + + + +[[controllers.exception-handler.signature]] +=== Method Signature + +Exception handler methods support a flexible method signature with method arguments +resolved from a `DataFetchingEnvironment,` and matching to those of +<>. + +Supported return types are listed below: + +[cols="1,2"] +|=== +| Return Type | Description + +| `graphql.GraphQLError` +| Resolve the exception to a single field error. + +| `Collection` +| Resolve the exception to multiple field errors. + +| `void` +| Resolve the exception without response errors. + +| `Object` +| Resolve the exception to a single error, to multiple errors, or none. + The return value must be `GraphQLError`, `Collection`, or `null`. + +| `Mono` +| For asynchronous resolution where `` is one of the supported, synchronous, return types. + +|=== \ No newline at end of file diff --git a/spring-graphql-docs/src/docs/asciidoc/includes/data.adoc b/spring-graphql-docs/src/docs/asciidoc/includes/data.adoc new file mode 100644 index 00000000..44bac43f --- /dev/null +++ b/spring-graphql-docs/src/docs/asciidoc/includes/data.adoc @@ -0,0 +1,482 @@ +[[data]] += Data Integration + +Spring for GraphQL lets you leverage existing Spring technology, following common +programming models to expose underlying data sources through GraphQL. + +This section discusses an integration layer for Spring Data that provides an easy way to +adapt a Querydsl or a Query by Example repository to a `DataFetcher`, including the +option for automated detection and GraphQL Query registration for repositories marked +with `@GraphQlRepository`. + + + +[[data.querydsl]] +== Querydsl + +Spring for GraphQL supports use of http://www.querydsl.com/[Querydsl] to fetch data through +the Spring Data +https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions[Querydsl extension]. +Querydsl provides a flexible yet typesafe approach to express query predicates by +generating a meta-model using annotation processors. + +For example, declare a repository as `QuerydslPredicateExecutor`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +public interface AccountRepository extends Repository, + QuerydslPredicateExecutor { +} +---- + +Then use it to create a `DataFetcher`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + // For single result queries + DataFetcher dataFetcher = + QuerydslDataFetcher.builder(repository).single(); + + // For multi-result queries + DataFetcher> dataFetcher = + QuerydslDataFetcher.builder(repository).many(); + + // For paginated queries + DataFetcher> dataFetcher = + QuerydslDataFetcher.builder(repository).scrollable(); +---- + +You can now register the above `DataFetcher` through a +<>. + +The `DataFetcher` builds a Querydsl `Predicate` from GraphQL arguments, and uses it to +fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, and LDAP. + +NOTE: For a single argument that is a GraphQL input type, `QuerydslDataFetcher` nests one +level down, and uses the values from the argument sub-map. + +If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns +`DataFetcher>` or `DataFetcher>`. Spring Data supports this +variant for MongoDB. + + +[[data.querydsl.build]] +=== Build Setup + +To configure Querydsl in your build, follow the +https://querydsl.com/static/querydsl/latest/reference/html/ch02.html[official reference documentation]: + +For example: + +[source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"] +.Gradle +---- +dependencies { + //... + + annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa", + 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final', + 'javax.annotation:javax.annotation-api:1.3.2' +} + +compileJava { + options.annotationProcessorPath = configurations.annotationProcessor +} +---- +[source,xml,indent=0,subs="verbatim,quotes,attributes",role="secondary"] +.Maven +---- + + + + com.querydsl + querydsl-apt + ${querydsl.version} + jpa + provided + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.2.Final + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + + com.mysema.maven + apt-maven-plugin + ${apt-maven-plugin.version} + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + + +---- + +The {github-10x-branch}/samples/webmvc-http[webmvc-http] sample uses Querydsl for +`artifactRepositories`. + + +[[data.querydsl.customizations]] +=== Customizations + +`QuerydslDataFetcher` supports customizing how GraphQL arguments are bound onto properties +to create a Querydsl `Predicate`. By default, arguments are bound as "is equal to" for +each available property. To customize that, you can use `QuerydslDataFetcher` builder +methods to provide a `QuerydslBinderCustomizer`. + +A repository may itself be an instance of `QuerydslBinderCustomizer`. This is auto-detected +and transparently applied during <>. However, when manually +building a `QuerydslDataFetcher` you will need to use builder methods to apply it. + +`QuerydslDataFetcher` supports interface and DTO projections to transform query results +before returning these for further GraphQL processing. + +TIP: To learn what projections are, please refer to the +https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data docs]. +To understand how to use projections in GraphQL, please see <>. + +To use Spring Data projections with Querydsl repositories, create either a projection interface +or a target DTO class and configure it through the `projectAs` method to obtain a +`DataFetcher` producing the target type: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + class Account { + + String name, identifier, description; + + Person owner; + } + + interface AccountProjection { + + String getName(); + + String getIdentifier(); + } + + // For single result queries + DataFetcher dataFetcher = + QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); + + // For multi-result queries + DataFetcher> dataFetcher = + QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); +---- + + + +[[data.querydsl.registration]] +=== Auto-Registration + +If a repository is annotated with `@GraphQlRepository`, it is automatically registered +for queries that do not already have a registered `DataFetcher` and whose return type +matches that of the repository domain type. This includes single value queries, multi-value +queries, and <> queries. + +By default, the name of the GraphQL type returned by the query must match the simple name +of the repository domain type. If needed, you can use the `typeName` attribute of +`@GraphQlRepository` to specify the target GraphQL type name. + +For paginated queries, the simple name of the repository domain type must match the +`Connection` type name without the `Connection` ending (e.g. `**Book**` matches +`**Books**Connection`). For auto-registration, pagination is offset-based with 20 items +per page. + +Auto-registration detects if a given repository implements `QuerydslBinderCustomizer` and +transparently applies that through `QuerydslDataFetcher` builder methods. + +Auto-registration is performed through a built-in `RuntimeWiringConfigurer` that can be +obtained from `QuerydslDataFetcher`. The <> automatically +detects `@GraphQlRepository` beans and uses them to initialize the +`RuntimeWiringConfigurer` with. + +Auto-registration applies <> +by calling `customize(Builder)` on the repository instance if your repository +implements `QuerydslBuilderCustomizer` or `ReactiveQuerydslBuilderCustomizer` +respectively. + + +[[data.querybyexample]] +== Query by Example + +Spring Data supports the use of +https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example[Query by Example] +to fetch data. Query by Example (QBE) is a simple querying technique that does not require +you to write queries through store-specific query languages. + +Start by declaring a repository that is `QueryByExampleExecutor`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +public interface AccountRepository extends Repository, + QueryByExampleExecutor { +} +---- + +Use `QueryByExampleDataFetcher` to turn the repository into a `DataFetcher`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + // For single result queries + DataFetcher dataFetcher = + QueryByExampleDataFetcher.builder(repository).single(); + + // For multi-result queries + DataFetcher> dataFetcher = + QueryByExampleDataFetcher.builder(repository).many(); + + // For paginated queries + DataFetcher> dataFetcher = + QueryByExampleDataFetcher.builder(repository).scrollable(); +---- + +You can now register the above `DataFetcher` through a +<>. + +The `DataFetcher` uses the GraphQL arguments map to create the domain type of the +repository and use that as the example object to fetch data with. Spring Data supports +`QueryByExampleDataFetcher` for JPA, MongoDB, Neo4j, and Redis. + +NOTE: For a single argument that is a GraphQL input type, `QueryByExampleDataFetcher` +nests one level down, and binds with the values from the argument sub-map. + +If the repository is `ReactiveQueryByExampleExecutor`, the builder returns +`DataFetcher>` or `DataFetcher>`. Spring Data supports this +variant for MongoDB, Neo4j, Redis, and R2dbc. + + +[[data.querybyexample.build]] +=== Build Setup + +Query by Example is already included in the Spring Data modules for the data stores where +it is supported, so no extra setup is required to enable it. + + +[[data.querybyexample.customizations]] +=== Customizations + +`QueryByExampleDataFetcher` supports interface and DTO projections to transform query +results before returning these for further GraphQL processing. + +TIP: To learn what projections are, please refer to the +https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data documentation]. +To understand the role of projections in GraphQL, please see <>. + +To use Spring Data projections with Query by Example repositories, create either a projection interface +or a target DTO class and configure it through the `projectAs` method to obtain a +`DataFetcher` producing the target type: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + class Account { + + String name, identifier, description; + + Person owner; + } + + interface AccountProjection { + + String getName(); + + String getIdentifier(); + } + + // For single result queries + DataFetcher dataFetcher = + QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); + + // For multi-result queries + DataFetcher> dataFetcher = + QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); +---- + + +[[data.querybyexample.registration]] +=== Auto-Registration + +If a repository is annotated with `@GraphQlRepository`, it is automatically registered +for queries that do not already have a registered `DataFetcher` and whose return type +matches that of the repository domain type. This includes single value queries, multi-value +queries, and <> queries. + +By default, the name of the GraphQL type returned by the query must match the simple name +of the repository domain type. If needed, you can use the `typeName` attribute of +`@GraphQlRepository` to specify the target GraphQL type name. + +For paginated queries, the simple name of the repository domain type must match the +`Connection` type name without the `Connection` ending (e.g. `**Book**` matches +`**Books**Connection`). For auto-registration, pagination is offset-based with 20 items +per page. + +Auto-registration is performed through a built-in `RuntimeWiringConfigurer` that can be +obtained from `QueryByExampleDataFetcher`. The <> automatically +detects `@GraphQlRepository` beans and uses them to initialize the +`RuntimeWiringConfigurer` with. + +Auto-registration applies <> +by calling `customize(Builder)` on the repository instance if your repository +implements `QueryByExampleBuilderCustomizer` or +`ReactiveQueryByExampleBuilderCustomizer` respectively. + + + +[[data.projections]] +== Selection Set vs Projections + +A common question that arises is, how GraphQL selection sets compare to +https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data projections] +and what role does each play? + +The short answer is that Spring for GraphQL is not a data gateway that translates GraphQL +queries directly into SQL or JSON queries. Instead, it lets you leverage existing Spring +technology and does not assume a one for one mapping between the GraphQL schema and the +underlying data model. That is why client-driven selection and server-side transformation +of the data model can play complementary roles. + +To better understand, consider that Spring Data promotes domain-driven (DDD) design as +the recommended approach to manage complexity in the data layer. In DDD, it is important +to adhere to the constraints of an aggregate. By definition an aggregate is valid only if +loaded in its entirety, since a partially loaded aggregate may impose limitations on +aggregate functionality. + +In Spring Data you can choose whether you want your aggregate be exposed as is, or +whether to apply transformations to the data model before returning it as a GraphQL +result. Sometimes it's enough to do the former, and by default the +<> and the <> integrations turn the GraphQL +selection set into property path hints that the underlying Spring Data module uses to +limit the selection. + +In other cases, it's useful to reduce or even transform the underlying data model in +order to adapt to the GraphQL schema. Spring Data supports this through Interface +and DTO Projections. + +Interface projections define a fixed set of properties to expose where properties may or +may not be `null`, depending on the data store query result. There are two kinds of +interface projections both of which determine what properties to load from the underlying +data source: + +- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.closed[Closed interface projections] +are helpful if you cannot partially materialize the aggregate object, but you still +want to expose a subset of properties. +- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.open[Open interface projections] +leverage Spring's `@Value` annotation and +{spring-framework-ref-docs}/core.html#expressions[SpEL] expressions to apply lightweight +data transformations, such as concatenations, computations, or applying static functions +to a property. + +DTO projections offer a higher level of customization as you can place transformation +code either in the constructor or in getter methods. + +DTO projections materialize from a query where the individual properties are +determined by the projection itself. DTO projections are commonly used with full-args +constructors (e.g. Java records), and therefore they can only be constructed if all +required fields (or columns) are part of the database query result. + + + +[[data.pagination.scroll]] +== Scroll + +As explained in <>, the GraphQL Cursor Connection spec defines a +mechanism for pagination with `Connection`, `Edge`, and `PageInfo` schema types, while +GraphQL Java provides the equivalent Java type representations. + +Spring for GraphQL provides built-in ``ConnectionAdapter`` implementations to adapt the +Spring Data pagination types `Window` and `Slice` transparently. You can configure that +as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +CursorStrategy strategy = CursorStrategy.withEncoder( + new ScrollPositionCursorStrategy(), + CursorEncoder.base64()); // <1> + +GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of( + new WindowConnectionAdapter(strategy), + new SliceConnectionAdapter(strategy))); // <2> + +GraphQlSource.schemaResourceBuilder() + .schemaResources(..) + .typeDefinitionConfigurer(..) + .typeVisitors(List.of(visitor)); // <3> +---- + +<1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor. +<2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s. +<3> Register the type visitor. + +On the request side, a controller method can declare a +<> method argument to paginate forward +or backward. For this to work, you must declare a <> +supports `ScrollPosition` as a bean. + +The <> declares a `CursorStrategy` bean, and registers the +`ConnectionFieldTypeVisitor` as shown above if Spring Data is on the classpath. + + +[[data.pagination.scroll.keyset]] +== Keyset Position + +For `KeysetScrollPosition`, the cursor needs to be created from a keyset, which is +essentially a `Map` of key-value pairs. To decide how to create a cursor from a keyset, +you can configure `ScrollPositionCursorStrategy` with `CursorStrategy>`. +By default, `JsonKeysetCursorStrategy` writes the keyset `Map` to JSON. That works for +simple like String, Boolean, Integer, and Double, but others cannot be restored back to the +same type without target type information. The Jackson library has a default typing feature +that can include type information in the JSON. To use it safely you must specify a list of +allowed types. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() + .allowIfBaseType(Map.class) + .allowIfSubType(ZonedDateTime.class) + .build(); + + ObjectMapper mapper = new ObjectMapper(); + mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL); +---- + +You can then create `JsonKeysetCursorStrategy`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + ObjectMapper mapper = ... ; + + CodecConfigurer configurer = ServerCodecConfigurer.create(); + configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper)); + configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper)); + + JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer); +---- + +By default, if `JsonKeysetCursorStrategy` is created without a `CodecConfigurer` and the +Jackson library is on the classpath, customizations like the above are applied for +`Date`, `Calendar`, and any type from `java.time`. + + + +[[data.pagination.sort]] +== Sort + +Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments. +`AbstractSortStrategy` implements the contract with abstract methods to extract the sort +direction and properties. To enable support for `Sort` as a controller method argument, +you need to declare a `SortStrategy` bean. \ No newline at end of file diff --git a/spring-graphql-docs/src/docs/asciidoc/includes/request-execution.adoc b/spring-graphql-docs/src/docs/asciidoc/includes/request-execution.adoc new file mode 100644 index 00000000..be050898 --- /dev/null +++ b/spring-graphql-docs/src/docs/asciidoc/includes/request-execution.adoc @@ -0,0 +1,718 @@ +[[execution]] += Request Execution + +`ExecutionGraphQlService` is the main Spring abstraction to call GraphQL Java to execute +requests. Underlying transports, such as the <>, 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 <> creates an instance of this builder and further initializes it +to <> 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 +<> 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 +<>. 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 <> 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. +- <> handling code. +- Default <> for interface and union types. +- `DataFetcher` for a field although applications will typically use <>, and +those are detected and registered as `DataFetcher`s by `AnnotatedControllerConfigurer`, +which is a `RuntimeWiringConfigurer`. The <> 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 <> 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`. 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 <>. 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 <> 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 +<>. The <> 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.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 <> 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 +<>, 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 <>, and those for +<> and <> 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 = ... + +builder.schemaResources(..) + .configureRuntimeWiring(..) + .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider)) +---- + +The <> explains how to configure that with Spring Boot. + + + +[[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 +<>. + + + +[[execution.context]] +== Context Propagation + +Spring for GraphQL provides support to transparently propagate context from the +<>, 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 +<> 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 <> can rely on access to Reactor context that +originates from the WebFlux request handling chain. This includes Reactor context +added by <> 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 <> 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 <> programming model enables handling data fetching exceptions with +annotated exception handler methods with a flexible method signature, see +<> 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 <>. + + +[[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 <> registers `ConnectionTypeDefinitionConfigurer` by default. + + +[[execution.pagination.adapters]] +=== `ConnectionAdapter` + +Once <> 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 <> ``ConnectionAdapter``s for Spring Data's +`Window` and `Slice`. You can also create your own custom adapter. `ConnectionAdapter` +implementations rely on a <> to +create cursors for returned items. The same strategy is also used to support the +<> 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 <> uses this to encode cursors for returned items. +<> methods, <> repositories, and <> +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 <> `CursorStrategy` for the Spring Data +`ScrollPosition`. The <> 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 <> 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 <> 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 +<> 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 +<> 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 <> 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("..."); +// ... +---- diff --git a/spring-graphql-docs/src/docs/asciidoc/includes/transports.adoc b/spring-graphql-docs/src/docs/asciidoc/includes/transports.adoc new file mode 100644 index 00000000..8ee41a26 --- /dev/null +++ b/spring-graphql-docs/src/docs/asciidoc/includes/transports.adoc @@ -0,0 +1,147 @@ +[[server.transports]] += Server Transports + +Spring for GraphQL supports server handling of GraphQL requests over HTTP, WebSocket, and +RSocket. + + +[[server.transports.http]] += HTTP + +`GraphQlHttpHandler` handles GraphQL over HTTP requests and delegates to the +<> chain for request execution. There are two variants, one for +Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have +equivalent functionality, but rely on blocking vs non-blocking I/O respectively for +writing the HTTP response. + +Requests must use HTTP POST with `"application/json"` as content type and GraphQL request details +included as JSON in the request body, as defined in the proposed +https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md[GraphQL over HTTP] specification. +Once the JSON body has been successfully decoded, the HTTP response status is always 200 (OK), +and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response. +The default and preferred choice of media type is `"application/graphql-response+json"`, but `"application/json"` +is also supported, as described in the specification. + +`GraphQlHttpHandler` can be exposed as an HTTP endpoint by declaring a `RouterFunction` +bean and using the `RouterFunctions` from Spring MVC or WebFlux to create the route. The +<> does this, see the +{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints] section for +details, or check `GraphQlWebMvcAutoConfiguration` or `GraphQlWebFluxAutoConfiguration` +it contains, for the actual config. + +The 1.0.x branch of this repository contains a Spring MVC +{github-10x-branch}/samples/webmvc-http[HTTP sample] application. + + + +[[server.transports.websocket]] +== WebSocket + +`GraphQlWebSocketHandler` handles GraphQL over WebSocket requests based on the +https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[protocol] defined in the +https://github.com/enisdenjo/graphql-ws[graphql-ws] library. The main reason to use +GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL +responses, but it can also be used for regular queries with a single response. +The handler delegates every request to the <> chain for further +request execution. + +[TIP] +.GraphQL Over WebSocket Protocols +==== +There are two such protocols, one in the +https://github.com/apollographql/subscriptions-transport-ws[subscriptions-transport-ws] +library and another in the +https://github.com/enisdenjo/graphql-ws[graphql-ws] library. The former is not active and +succeeded by the latter. Read this +https://the-guild.dev/blog/graphql-over-websockets[blog post] for the history. +==== + +There are two variants of `GraphQlWebSocketHandler`, one for Spring MVC and one for +Spring WebFlux. Both handle requests asynchronously and have equivalent functionality. +The WebFlux handler also uses non-blocking I/O and back pressure to stream messages, +which works well since in GraphQL Java a subscription response is a Reactive Streams +`Publisher`. + +The `graphql-ws` project lists a number of +https://github.com/enisdenjo/graphql-ws#recipes[recipes] for client use. + +`GraphQlWebSocketHandler` can be exposed as a WebSocket endpoint by declaring a +`SimpleUrlHandlerMapping` bean and using it to map the handler to a URL path. By default, +the <> does not expose a GraphQL over WebSocket endpoint, but it's easy to +enable it by adding a property for the endpoint path. Please, see the +{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints] +section for details, or check the `GraphQlWebMvcAutoConfiguration` or the +`GraphQlWebFluxAutoConfiguration` for the actual Boot starter config. + +The 1.0.x branch of this repository contains a WebFlux +{github-10x-branch}/samples/webflux-websocket[WebSocket sample] application. + + + +[[server.transports.rsocket]] +== RSocket + +`GraphQlRSocketHandler` handles GraphQL over RSocket requests. Queries and mutations are +expected and handled as an RSocket `request-response` interaction while subscriptions are +handled as `request-stream`. + +`GraphQlRSocketHandler` can be used a delegate from an `@Controller` that is mapped to +the route for GraphQL requests. For example: + +include::code:GraphQlRSocketController[] + + + + +[[server.interception]] +== Interception + +Server transports allow intercepting requests before and after the GraphQL Java engine is +called to process a request. + + +[[server.interception.web]] +=== `WebGraphQlInterceptor` + +<> and <> transports invoke a chain of +0 or more `WebGraphQlInterceptor`, followed by an `ExecutionGraphQlService` that calls +the GraphQL Java engine. `WebGraphQlInterceptor` allows an application to intercept +incoming requests and do one of the following: + +- Check HTTP request details +- Customize the `graphql.ExecutionInput` +- Add HTTP response headers +- Customize the `graphql.ExecutionResult` + +For example, an interceptor can pass an HTTP request header to a `DataFetcher`: + +include::code:RequestHeaderInterceptor[] +<1> Interceptor adds HTTP request header value into GraphQLContext +<2> Data controller method accesses the value + +Reversely, an interceptor can access values added to the `GraphQLContext` by a controller: + +include::code:ResponseHeaderInterceptor[] +<1> Controller adds value to the `GraphQLContext` +<2> Interceptor uses the value to add an HTTP response header + +`WebGraphQlHandler` can modify the `ExecutionResult`, for example, to inspect and modify +request validation errors that are raised before execution begins and which cannot be +handled with a `DataFetcherExceptionResolver`: + +include::code:RequestErrorInterceptor[] +<1> Return the same if `ExecutionResult` has a "data" key with non-null value +<2> Check and transform the GraphQL errors +<3> Update the `ExecutionResult` with the modified errors + +Use `WebGraphQlHandler` to configure the `WebGraphQlInterceptor` chain. This is supported +by the <>, see +{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints]. + + +[[server.interception.rsocket]] +=== `RSocketQlInterceptor` + +Similar to <>, an `RSocketQlInterceptor` allows intercepting +GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use +this to customize the `graphql.ExecutionInput` and the `graphql.ExecutionResult`. \ No newline at end of file diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index ae221df6..e259e1ec 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -24,2123 +24,19 @@ To get started, see the <> and <> sections. +include::includes/transports.adoc[leveloffset=+1] -[[server.transports]] -== Server Transports -Spring for GraphQL supports server handling of GraphQL requests over HTTP, WebSocket, and -RSocket. +include::includes/request-execution.adoc[leveloffset=+1] -[[server.transports.http]] -=== HTTP -`GraphQlHttpHandler` handles GraphQL over HTTP requests and delegates to the -<> chain for request execution. There are two variants, one for -Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have -equivalent functionality, but rely on blocking vs non-blocking I/O respectively for -writing the HTTP response. -Requests must use HTTP POST with `"application/json"` as content type and GraphQL request details -included as JSON in the request body, as defined in the proposed -https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md[GraphQL over HTTP] specification. -Once the JSON body has been successfully decoded, the HTTP response status is always 200 (OK), -and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response. -The default and preferred choice of media type is `"application/graphql-response+json"`, but `"application/json"` -is also supported, as described in the specification. +include::includes/data.adoc[leveloffset=+1] -`GraphQlHttpHandler` can be exposed as an HTTP endpoint by declaring a `RouterFunction` -bean and using the `RouterFunctions` from Spring MVC or WebFlux to create the route. The -<> does this, see the -{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints] section for -details, or check `GraphQlWebMvcAutoConfiguration` or `GraphQlWebFluxAutoConfiguration` -it contains, for the actual config. -The 1.0.x branch of this repository contains a Spring MVC -{github-10x-branch}/samples/webmvc-http[HTTP sample] application. - - - -[[server.transports.websocket]] -=== WebSocket - -`GraphQlWebSocketHandler` handles GraphQL over WebSocket requests based on the -https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[protocol] defined in the -https://github.com/enisdenjo/graphql-ws[graphql-ws] library. The main reason to use -GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL -responses, but it can also be used for regular queries with a single response. -The handler delegates every request to the <> chain for further -request execution. - -[TIP] -.GraphQL Over WebSocket Protocols -==== -There are two such protocols, one in the -https://github.com/apollographql/subscriptions-transport-ws[subscriptions-transport-ws] -library and another in the -https://github.com/enisdenjo/graphql-ws[graphql-ws] library. The former is not active and -succeeded by the latter. Read this -https://the-guild.dev/blog/graphql-over-websockets[blog post] for the history. -==== - -There are two variants of `GraphQlWebSocketHandler`, one for Spring MVC and one for -Spring WebFlux. Both handle requests asynchronously and have equivalent functionality. -The WebFlux handler also uses non-blocking I/O and back pressure to stream messages, -which works well since in GraphQL Java a subscription response is a Reactive Streams -`Publisher`. - -The `graphql-ws` project lists a number of -https://github.com/enisdenjo/graphql-ws#recipes[recipes] for client use. - -`GraphQlWebSocketHandler` can be exposed as a WebSocket endpoint by declaring a -`SimpleUrlHandlerMapping` bean and using it to map the handler to a URL path. By default, -the <> does not expose a GraphQL over WebSocket endpoint, but it's easy to -enable it by adding a property for the endpoint path. Please, see the -{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints] -section for details, or check the `GraphQlWebMvcAutoConfiguration` or the -`GraphQlWebFluxAutoConfiguration` for the actual Boot starter config. - -The 1.0.x branch of this repository contains a WebFlux -{github-10x-branch}/samples/webflux-websocket[WebSocket sample] application. - - - -[[server.transports.rsocket]] -=== RSocket - -`GraphQlRSocketHandler` handles GraphQL over RSocket requests. Queries and mutations are -expected and handled as an RSocket `request-response` interaction while subscriptions are -handled as `request-stream`. - -`GraphQlRSocketHandler` can be used a delegate from an `@Controller` that is mapped to -the route for GraphQL requests. For example: - -include::code:GraphQlRSocketController[] - - - - -[[server.interception]] -=== Interception - -Server transports allow intercepting requests before and after the GraphQL Java engine is -called to process a request. - - -[[server.interception.web]] -==== `WebGraphQlInterceptor` - -<> and <> transports invoke a chain of -0 or more `WebGraphQlInterceptor`, followed by an `ExecutionGraphQlService` that calls -the GraphQL Java engine. `WebGraphQlInterceptor` allows an application to intercept -incoming requests and do one of the following: - -- Check HTTP request details -- Customize the `graphql.ExecutionInput` -- Add HTTP response headers -- Customize the `graphql.ExecutionResult` - -For example, an interceptor can pass an HTTP request header to a `DataFetcher`: - -include::code:RequestHeaderInterceptor[] -<1> Interceptor adds HTTP request header value into GraphQLContext -<2> Data controller method accesses the value - -Reversely, an interceptor can access values added to the `GraphQLContext` by a controller: - -include::code:ResponseHeaderInterceptor[] -<1> Controller adds value to the `GraphQLContext` -<2> Interceptor uses the value to add an HTTP response header - -`WebGraphQlHandler` can modify the `ExecutionResult`, for example, to inspect and modify -request validation errors that are raised before execution begins and which cannot be -handled with a `DataFetcherExceptionResolver`: - -include::code:RequestErrorInterceptor[] -<1> Return the same if `ExecutionResult` has a "data" key with non-null value -<2> Check and transform the GraphQL errors -<3> Update the `ExecutionResult` with the modified errors - -Use `WebGraphQlHandler` to configure the `WebGraphQlInterceptor` chain. This is supported -by the <>, see -{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints]. - - -[[server.interception.rsocket]] -==== `RSocketQlInterceptor` - -Similar to <>, an `RSocketQlInterceptor` allows intercepting -GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use -this to customize the `graphql.ExecutionInput` and the `graphql.ExecutionResult`. - - - - -[[execution]] -== Request Execution - -`ExecutionGraphQlService` is the main Spring abstraction to call GraphQL Java to execute -requests. Underlying transports, such as the <>, 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 <> creates an instance of this builder and further initializes it -to <> 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 -<> 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 -<>. 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 <> 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. -- <> handling code. -- Default <> for interface and union types. -- `DataFetcher` for a field although applications will typically use <>, and -those are detected and registered as `DataFetcher`s by `AnnotatedControllerConfigurer`, -which is a `RuntimeWiringConfigurer`. The <> 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 <> 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`. 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 <>. 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 <> 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 -<>. The <> 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.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 <> 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 -<>, 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 <>, and those for -<> and <> 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 scheam `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 = ... - -builder.schemaResources(..) - .configureRuntimeWiring(..) - .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider)) ----- - -The <> explains how to configure that with Spring Boot. - - - -[[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 -<>. - - - -[[execution.context]] -=== Context Propagation - -Spring for GraphQL provides support to transparently propagate context from the -<>, 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 -<> 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 <> can rely on access to Reactor context that -originates from the WebFlux request handling chain. This includes Reactor context -added by <> 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 <> 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 <> programming model enables handling data fetching exceptions with -annotated exception handler methods with a flexible method signature, see -<> 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 <>. - - -[[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 <> registers `ConnectionTypeDefinitionConfigurer` by default. - - -[[execution.pagination.adapters]] -==== `ConnectionAdapter` - -Once <> 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 <> ``ConnectionAdapter``s for Spring Data's -`Window` and `Slice`. You can also create your own custom adapter. `ConnectionAdapter` -implementations rely on a <> to -create cursors for returned items. The same strategy is also used to support the -<> 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 <> uses this to encode cursors for returned items. -<> methods, <> repositories, and <> -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 <> `CursorStrategy` for the Spring Data -`ScrollPosition`. The <> 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 <> 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 <> 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 -<> 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 -<> 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 <> 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("..."); -// ... ----- - - - - -[[data]] -== Data Integration - -Spring for GraphQL lets you leverage existing Spring technology, following common -programming models to expose underlying data sources through GraphQL. - -This section discusses an integration layer for Spring Data that provides an easy way to -adapt a Querydsl or a Query by Example repository to a `DataFetcher`, including the -option for automated detection and GraphQL Query registration for repositories marked -with `@GraphQlRepository`. - - - -[[data.querydsl]] -=== Querydsl - -Spring for GraphQL supports use of http://www.querydsl.com/[Querydsl] to fetch data through -the Spring Data -https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions[Querydsl extension]. -Querydsl provides a flexible yet typesafe approach to express query predicates by -generating a meta-model using annotation processors. - -For example, declare a repository as `QuerydslPredicateExecutor`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -public interface AccountRepository extends Repository, - QuerydslPredicateExecutor { -} ----- - -Then use it to create a `DataFetcher`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - // For single result queries - DataFetcher dataFetcher = - QuerydslDataFetcher.builder(repository).single(); - - // For multi-result queries - DataFetcher> dataFetcher = - QuerydslDataFetcher.builder(repository).many(); - - // For paginated queries - DataFetcher> dataFetcher = - QuerydslDataFetcher.builder(repository).scrollable(); ----- - -You can now register the above `DataFetcher` through a -<>. - -The `DataFetcher` builds a Querydsl `Predicate` from GraphQL arguments, and uses it to -fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, and LDAP. - -NOTE: For a single argument that is a GraphQL input type, `QuerydslDataFetcher` nests one -level down, and uses the values from the argument sub-map. - -If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns -`DataFetcher>` or `DataFetcher>`. Spring Data supports this -variant for MongoDB. - - -[[data.querydsl.build]] -==== Build Setup - -To configure Querydsl in your build, follow the -https://querydsl.com/static/querydsl/latest/reference/html/ch02.html[official reference documentation]: - -For example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"] -.Gradle ----- -dependencies { - //... - - annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa", - 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final', - 'javax.annotation:javax.annotation-api:1.3.2' -} - -compileJava { - options.annotationProcessorPath = configurations.annotationProcessor -} ----- -[source,xml,indent=0,subs="verbatim,quotes,attributes",role="secondary"] -.Maven ----- - - - - com.querydsl - querydsl-apt - ${querydsl.version} - jpa - provided - - - org.hibernate.javax.persistence - hibernate-jpa-2.1-api - 1.0.2.Final - - - javax.annotation - javax.annotation-api - 1.3.2 - - - - - - com.mysema.maven - apt-maven-plugin - ${apt-maven-plugin.version} - - - - process - - - target/generated-sources/java - com.querydsl.apt.jpa.JPAAnnotationProcessor - - - - - ----- - -The {github-10x-branch}/samples/webmvc-http[webmvc-http] sample uses Querydsl for -`artifactRepositories`. - - -[[data.querydsl.customizations]] -==== Customizations - -`QuerydslDataFetcher` supports customizing how GraphQL arguments are bound onto properties -to create a Querydsl `Predicate`. By default, arguments are bound as "is equal to" for -each available property. To customize that, you can use `QuerydslDataFetcher` builder -methods to provide a `QuerydslBinderCustomizer`. - -A repository may itself be an instance of `QuerydslBinderCustomizer`. This is auto-detected -and transparently applied during <>. However, when manually -building a `QuerydslDataFetcher` you will need to use builder methods to apply it. - -`QuerydslDataFetcher` supports interface and DTO projections to transform query results -before returning these for further GraphQL processing. - -TIP: To learn what projections are, please refer to the -https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data docs]. -To understand how to use projections in GraphQL, please see <>. - -To use Spring Data projections with Querydsl repositories, create either a projection interface -or a target DTO class and configure it through the `projectAs` method to obtain a -`DataFetcher` producing the target type: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - class Account { - - String name, identifier, description; - - Person owner; - } - - interface AccountProjection { - - String getName(); - - String getIdentifier(); - } - - // For single result queries - DataFetcher dataFetcher = - QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); - - // For multi-result queries - DataFetcher> dataFetcher = - QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); ----- - - - -[[data.querydsl.registration]] -==== Auto-Registration - -If a repository is annotated with `@GraphQlRepository`, it is automatically registered -for queries that do not already have a registered `DataFetcher` and whose return type -matches that of the repository domain type. This includes single value queries, multi-value -queries, and <> queries. - -By default, the name of the GraphQL type returned by the query must match the simple name -of the repository domain type. If needed, you can use the `typeName` attribute of -`@GraphQlRepository` to specify the target GraphQL type name. - -For paginated queries, the simple name of the repository domain type must match the -`Connection` type name without the `Connection` ending (e.g. `**Book**` matches -`**Books**Connection`). For auto-registration, pagination is offset-based with 20 items -per page. - -Auto-registration detects if a given repository implements `QuerydslBinderCustomizer` and -transparently applies that through `QuerydslDataFetcher` builder methods. - -Auto-registration is performed through a built-in `RuntimeWiringConfigurer` that can be -obtained from `QuerydslDataFetcher`. The <> automatically -detects `@GraphQlRepository` beans and uses them to initialize the -`RuntimeWiringConfigurer` with. - -Auto-registration applies <> -by calling `customize(Builder)` on the repository instance if your repository -implements `QuerydslBuilderCustomizer` or `ReactiveQuerydslBuilderCustomizer` -respectively. - - -[[data.querybyexample]] -=== Query by Example - -Spring Data supports the use of -https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example[Query by Example] -to fetch data. Query by Example (QBE) is a simple querying technique that does not require -you to write queries through store-specific query languages. - -Start by declaring a repository that is `QueryByExampleExecutor`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -public interface AccountRepository extends Repository, - QueryByExampleExecutor { -} ----- - -Use `QueryByExampleDataFetcher` to turn the repository into a `DataFetcher`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - // For single result queries - DataFetcher dataFetcher = - QueryByExampleDataFetcher.builder(repository).single(); - - // For multi-result queries - DataFetcher> dataFetcher = - QueryByExampleDataFetcher.builder(repository).many(); - - // For paginated queries - DataFetcher> dataFetcher = - QueryByExampleDataFetcher.builder(repository).scrollable(); ----- - -You can now register the above `DataFetcher` through a -<>. - -The `DataFetcher` uses the GraphQL arguments map to create the domain type of the -repository and use that as the example object to fetch data with. Spring Data supports -`QueryByExampleDataFetcher` for JPA, MongoDB, Neo4j, and Redis. - -NOTE: For a single argument that is a GraphQL input type, `QueryByExampleDataFetcher` -nests one level down, and binds with the values from the argument sub-map. - -If the repository is `ReactiveQueryByExampleExecutor`, the builder returns -`DataFetcher>` or `DataFetcher>`. Spring Data supports this -variant for MongoDB, Neo4j, Redis, and R2dbc. - - -[[data.querybyexample.build]] -==== Build Setup - -Query by Example is already included in the Spring Data modules for the data stores where -it is supported, so no extra setup is required to enable it. - - -[[data.querybyexample.customizations]] -==== Customizations - -`QueryByExampleDataFetcher` supports interface and DTO projections to transform query -results before returning these for further GraphQL processing. - -TIP: To learn what projections are, please refer to the -https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data documentation]. -To understand the role of projections in GraphQL, please see <>. - -To use Spring Data projections with Query by Example repositories, create either a projection interface -or a target DTO class and configure it through the `projectAs` method to obtain a -`DataFetcher` producing the target type: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - class Account { - - String name, identifier, description; - - Person owner; - } - - interface AccountProjection { - - String getName(); - - String getIdentifier(); - } - - // For single result queries - DataFetcher dataFetcher = - QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); - - // For multi-result queries - DataFetcher> dataFetcher = - QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); ----- - - -[[data.querybyexample.registration]] -==== Auto-Registration - -If a repository is annotated with `@GraphQlRepository`, it is automatically registered -for queries that do not already have a registered `DataFetcher` and whose return type -matches that of the repository domain type. This includes single value queries, multi-value -queries, and <> queries. - -By default, the name of the GraphQL type returned by the query must match the simple name -of the repository domain type. If needed, you can use the `typeName` attribute of -`@GraphQlRepository` to specify the target GraphQL type name. - -For paginated queries, the simple name of the repository domain type must match the -`Connection` type name without the `Connection` ending (e.g. `**Book**` matches -`**Books**Connection`). For auto-registration, pagination is offset-based with 20 items -per page. - -Auto-registration is performed through a built-in `RuntimeWiringConfigurer` that can be -obtained from `QueryByExampleDataFetcher`. The <> automatically -detects `@GraphQlRepository` beans and uses them to initialize the -`RuntimeWiringConfigurer` with. - -Auto-registration applies <> -by calling `customize(Builder)` on the repository instance if your repository -implements `QueryByExampleBuilderCustomizer` or -`ReactiveQueryByExampleBuilderCustomizer` respectively. - - - -[[data.projections]] -=== Selection Set vs Projections - -A common question that arises is, how GraphQL selection sets compare to -https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data projections] -and what role does each play? - -The short answer is that Spring for GraphQL is not a data gateway that translates GraphQL -queries directly into SQL or JSON queries. Instead, it lets you leverage existing Spring -technology and does not assume a one for one mapping between the GraphQL schema and the -underlying data model. That is why client-driven selection and server-side transformation -of the data model can play complementary roles. - -To better understand, consider that Spring Data promotes domain-driven (DDD) design as -the recommended approach to manage complexity in the data layer. In DDD, it is important -to adhere to the constraints of an aggregate. By definition an aggregate is valid only if -loaded in its entirety, since a partially loaded aggregate may impose limitations on -aggregate functionality. - -In Spring Data you can choose whether you want your aggregate be exposed as is, or -whether to apply transformations to the data model before returning it as a GraphQL -result. Sometimes it's enough to do the former, and by default the -<> and the <> integrations turn the GraphQL -selection set into property path hints that the underlying Spring Data module uses to -limit the selection. - -In other cases, it's useful to reduce or even transform the underlying data model in -order to adapt to the GraphQL schema. Spring Data supports this through Interface -and DTO Projections. - -Interface projections define a fixed set of properties to expose where properties may or -may not be `null`, depending on the data store query result. There are two kinds of -interface projections both of which determine what properties to load from the underlying -data source: - -- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.closed[Closed interface projections] -are helpful if you cannot partially materialize the aggregate object, but you still -want to expose a subset of properties. -- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.open[Open interface projections] -leverage Spring's `@Value` annotation and -{spring-framework-ref-docs}/core.html#expressions[SpEL] expressions to apply lightweight -data transformations, such as concatenations, computations, or applying static functions -to a property. - -DTO projections offer a higher level of customization as you can place transformation -code either in the constructor or in getter methods. - -DTO projections materialize from a query where the individual properties are -determined by the projection itself. DTO projections are commonly used with full-args -constructors (e.g. Java records), and therefore they can only be constructed if all -required fields (or columns) are part of the database query result. - - - -[[data.pagination.scroll]] -=== Scroll - -As explained in <>, the GraphQL Cursor Connection spec defines a -mechanism for pagination with `Connection`, `Edge`, and `PageInfo` schema types, while -GraphQL Java provides the equivalent Java type representations. - -Spring for GraphQL provides built-in ``ConnectionAdapter`` implementations to adapt the -Spring Data pagination types `Window` and `Slice` transparently. You can configure that -as follows: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -CursorStrategy strategy = CursorStrategy.withEncoder( - new ScrollPositionCursorStrategy(), - CursorEncoder.base64()); // <1> - -GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of( - new WindowConnectionAdapter(strategy), - new SliceConnectionAdapter(strategy))); // <2> - -GraphQlSource.schemaResourceBuilder() - .schemaResources(..) - .typeDefinitionConfigurer(..) - .typeVisitors(List.of(visitor)); // <3> ----- - -<1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor. -<2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s. -<3> Register the type visitor. - -On the request side, a controller method can declare a -<> method argument to paginate forward -or backward. For this to work, you must declare a <> -supports `ScrollPosition` as a bean. - -The <> declares a `CursorStrategy` bean, and registers the -`ConnectionFieldTypeVisitor` as shown above if Spring Data is on the classpath. - - -[[data.pagination.scroll.keyset]] -=== Keyset Position - -For `KeysetScrollPosition`, the cursor needs to be created from a keyset, which is -essentially a `Map` of key-value pairs. To decide how to create a cursor from a keyset, -you can configure `ScrollPositionCursorStrategy` with `CursorStrategy>`. -By default, `JsonKeysetCursorStrategy` writes the keyset `Map` to JSON. That works for -simple like String, Boolean, Integer, and Double, but others cannot be restored back to the -same type without target type information. The Jackson library has a default typing feature -that can include type information in the JSON. To use it safely you must specify a list of -allowed types. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() - .allowIfBaseType(Map.class) - .allowIfSubType(ZonedDateTime.class) - .build(); - - ObjectMapper mapper = new ObjectMapper(); - mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL); ----- - -You can then create `JsonKeysetCursorStrategy`: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - ObjectMapper mapper = ... ; - - CodecConfigurer configurer = ServerCodecConfigurer.create(); - configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper)); - configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper)); - - JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer); ----- - -By default, if `JsonKeysetCursorStrategy` is created without a `CodecConfigurer` and the -Jackson library is on the classpath, customizations like the above are applied for -`Date`, `Calendar`, and any type from `java.time`. - - - -[[data.pagination.sort]] -=== Sort - -Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments. -`AbstractSortStrategy` implements the contract with abstract methods to extract the sort -direction and properties. To enable support for `Sort` as a controller method argument, -you need to declare a `SortStrategy` bean. - - - - -[[controllers]] -== Annotated Controllers - -Spring for GraphQL provides an annotation-based programming model where `@Controller` -components use annotations to declare handler methods with flexible method signatures to -fetch the data for specific GraphQL fields. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class GreetingController { - - @QueryMapping // <1> - public String hello() { // <2> - return "Hello, world!"; - } - - } ----- -<1> Bind this method to a query, i.e. a field under the Query type. -<2> Determine the query from the method name if not declared on the annotation. - -Spring for GraphQL uses `RuntimeWiring.Builder` to register the above handler method as a -`graphql.schema.DataFetcher` for the query named "hello". - - -[[controllers-declaration]] -=== Declaration - -You can define `@Controller` beans as standard Spring bean definitions. The -`@Controller` stereotype allows for auto-detection, aligned with Spring general -support for detecting `@Controller` and `@Component` classes on the classpath and -auto-registering bean definitions for them. It also acts as a stereotype for the annotated -class, indicating its role as a data fetching component in a GraphQL application. - -`AnnotatedControllerConfigurer` detects `@Controller` beans and registers their -annotated handler methods as ``DataFetcher``s via `RuntimeWiring.Builder`. It is an -implementation of `RuntimeWiringConfigurer` which can be added to `GraphQlSource.Builder`. -The <> automatically declares `AnnotatedControllerConfigurer` as a bean -and adds all `RuntimeWiringConfigurer` beans to `GraphQlSource.Builder` and that enables -support for annotated ``DataFetcher``s, see the -{spring-boot-ref-docs}/web.html#web.graphql.runtimewiring[GraphQL RuntimeWiring] section -in the Boot starter documentation. - - -[[controllers.schema-mapping]] -=== `@SchemaMapping` - -The `@SchemaMapping` annotation maps a handler method to a field in the GraphQL schema -and declares it to be the `DataFetcher` for that field. The annotation can specify the -parent type name, and the field name: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @SchemaMapping(typeName="Book", field="author") - public Author getAuthor(Book book) { - // ... - } - } ----- - -The `@SchemaMapping` annotation can also leave out those attributes, in which case the -field name defaults to the method name, while the type name defaults to the simple class -name of the source/parent object injected into the method. For example, the below -defaults to type "Book" and field "author": - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @SchemaMapping - public Author author(Book book) { - // ... - } - } ----- - -The `@SchemaMapping` annotation can be declared at the class level to specify a default -type name for all handler methods in the class. - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - @SchemaMapping(typeName="Book") - public class BookController { - - // @SchemaMapping methods for fields of the "Book" type - - } ----- - -`@QueryMapping`, `@MutationMapping`, and `@SubscriptionMapping` are meta annotations that -are themselves annotated with `@SchemaMapping` and have the typeName preset to `Query`, -`Mutation`, or `Subscription` respectively. Effectively, these are shortcut annotations -for fields under the Query, Mutation, and Subscription types respectively. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @QueryMapping - public Book bookById(@Argument Long id) { - // ... - } - - @MutationMapping - public Book addBook(@Argument BookInput bookInput) { - // ... - } - - @SubscriptionMapping - public Flux newPublications() { - // ... - } - } ----- - -`@SchemaMapping` handler methods have flexible signatures and can choose from a range of -method arguments and return values.. - - -[[controllers.schema-mapping.signature]] -==== Method Signature - -Schema mapping handler methods can have any of the following method arguments: - -[cols="1,2"] -|=== -| Method Argument | Description - -| `@Argument` -| For access to a named field argument bound to a higher-level, typed Object. - -See <>. - -| `@Argument Map` -| For access to the raw argument value. - -See <>. - -| `ArgumentValue` -| For access to a named field argument bound to a higher-level, typed Object along -with a flag to indicate if the input argument was omitted vs set to `null`. - -See <>. - -| `@Arguments` -| For access to all field arguments bound to a higher-level, typed Object. - -See <>. - -| `@Arguments Map` -| For access to the raw map of arguments. - -| `@ProjectedPayload` Interface -| For access to field arguments through a project interface. - -See <>. - -| "Source" -| For access to the source (i.e. parent/container) instance of the field. - -See <>. - -| `Subrange` and `ScrollSubrange` -| For access to pagination arguments. - -See <>, <>, <>. - -| `Sort` -| For access to sort details. - -See <>, <>. - -| `DataLoader` -| For access to a `DataLoader` in the `DataLoaderRegistry`. - -See <>. - -| `@ContextValue` -| For access to an attribute from the main `GraphQLContext` in `DataFetchingEnvironment`. - -| `@LocalContextValue` -| For access to an attribute from the local `GraphQLContext` in `DataFetchingEnvironment`. - -| `GraphQLContext` -| For access to the context from the `DataFetchingEnvironment`. - -| `java.security.Principal` -| Obtained from the Spring Security context, if available. - -| `@AuthenticationPrincipal` -| For access to `Authentication#getPrincipal()` from the Spring Security context. - -| `DataFetchingFieldSelectionSet` -| For access to the selection set for the query through the `DataFetchingEnvironment`. - -| `Locale`, `Optional` -| For access to the `Locale` from the `DataFetchingEnvironment`. - -| `DataFetchingEnvironment` -| For direct access to the underlying `DataFetchingEnvironment`. - -|=== - -Schema mapping handler methods can return: - -- A resolved value of any type. -- `Mono` and `Flux` for asynchronous value(s). Supported for controller methods and for - any `DataFetcher` as described in <>. -- `java.util.concurrent.Callable` to have the value(s) produced asynchronously. - For this to work, `AnnotatedControllerConfigurer` must be configured with an `Executor`. - - -[[controllers.schema-mapping.argument]] -==== `@Argument` - -In GraphQL Java, `DataFetchingEnvironment` provides access to a map of field-specific -argument values. The values can be simple scalar values (e.g. String, Long), a `Map` of -values for more complex input, or a `List` of values. - -Use the `@Argument` annotation to have an argument bound to a target object and -injected into the handler method. Binding is performed by mapping argument values to a -primary data constructor of the expected method parameter type, or by using a default -constructor to create the object and then map argument values to its properties. This is -repeated recursively, using all nested argument values and creating nested target objects -accordingly. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @QueryMapping - public Book bookById(@Argument Long id) { - // ... - } - - @MutationMapping - public Book addBook(@Argument BookInput bookInput) { - // ... - } - } ----- - -TIP: If the target object doesn't have setters, and you can't change that, you can use a -property on `AnnotatedControllerConfigurer` to allow falling back on binding via direct -field access. - -By default, if the method parameter name is available (requires the `-parameters` compiler -flag with Java 8+ or debugging info from the compiler), it is used to look up the argument. -If needed, you can customize the name through the annotation, e.g. `@Argument("bookInput")`. - -TIP: The `@Argument` annotation does not have a "required" flag, nor the option to -specify a default value. Both of these can be specified at the GraphQL schema level and -are enforced by GraphQL Java. - -If binding fails, a `BindException` is raised with binding issues accumulated as field -errors where the `field` of each error is the argument path where the issue occurred. - -You can use `@Argument` with a `Map` argument, to obtain the raw value of -the argument. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @MutationMapping - public Book addBook(@Argument Map bookInput) { - // ... - } - } ----- - -NOTE: Prior to 1.2, `@Argument Map` returned the full arguments map if -the annotation did not specify a name. After 1.2, `@Argument` with -`Map` always returns the raw argument value, matching either to the name -specified in the annotation, or to the parameter name. For access to the full arguments -map, please use <> instead. - - -[[controllers.schema-mapping.argument-value]] -==== `ArgumentValue` - -By default, input arguments in GraphQL are nullable and optional, which means an argument -can be set to the `null` literal, or not provided at all. This distinction is useful for -partial updates with a mutation where the underlying data may also be, either set to -`null` or not changed at all accordingly. When using <> -there is no way to make such a distinction, because you would get `null` or an empty -`Optional` in both cases. - -If you want to know not whether a value was not provided at all, you can declare an -`ArgumentValue` method parameter, which is a simple container for the resulting value, -along with a flag to indicate whether the input argument was omitted altogether. You -can use this instead of `@Argument`, in which case the argument name is determined from -the method parameter name, or together with `@Argument` to specify the argument name. - -For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @MutationMapping - public void addBook(ArgumentValue bookInput) { - if (!bookInput.isOmitted()) { - BookInput value = bookInput.value(); - // ... - } - } - } ----- - -`ArgumentValue` is also supported as a field within the object structure of an `@Argument` -method parameter, either initialized via a constructor argument or via a setter, including -as a field of an object nested at any level below the top level object. - - -[[controllers.schema-mapping.arguments]] -==== `@Arguments` - -Use the `@Arguments` annotation, if you want to bind the full arguments map onto a single -target Object, in contrast to `@Argument`, which binds a specific, named argument. - -For example, `@Argument BookInput bookInput` uses the value of the argument "bookInput" -to initialize `BookInput`, while `@Arguments` uses the full arguments map and in that -case, top-level arguments are bound to `BookInput` properties. - -You can use `@Arguments` with a `Map` argument, to obtain the raw map of -all argument values. - - -[[controllers.schema-mapping.projectedpayload.argument]] -==== `@ProjectedPayload` Interface - -As an alternative to using complete Objects with <>, -you can also use a projection interface to access GraphQL request arguments through a -well-defined, minimal interface. Argument projections are provided by -https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces[Spring Data's Interface projections] -when Spring Data is on the class path. - -To make use of this, create an interface annotated with `@ProjectedPayload` and declare -it as a controller method parameter. If the parameter is annotated with `@Argument`, -it applies to an individual argument within the `DataFetchingEnvironment.getArguments()` -map. When declared without `@Argument`, the projection works on top-level arguments in -the complete arguments map. - -For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @QueryMapping - public Book bookById(BookIdProjection bookId) { - // ... - } - - @MutationMapping - public Book addBook(@Argument BookInputProjection bookInput) { - // ... - } - } - - @ProjectedPayload - interface BookIdProjection { - - Long getId(); - } - - @ProjectedPayload - interface BookInputProjection { - - String getName(); - - @Value("#{target.author + ' ' + target.name}") - String getAuthorAndName(); - } ----- - - - -[[controllers.schema-mapping.source]] -==== Source - -In GraphQL Java, the `DataFetchingEnvironment` provides access to the source (i.e. -parent/container) instance of the field. To access this, simply declare a method parameter -of the expected target type. - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @SchemaMapping - public Author author(Book book) { - // ... - } - } ----- - -The source method argument also helps to determine the type name for the mapping. -If the simple name of the Java class matches the GraphQL type, then there is no need to -explicitly specify the type name in the `@SchemaMapping` annotation. - -[TIP] -==== -A <> handler method can batch load all authors for a query, -given a list of source/parent books objects. -==== - - -[[controllers.schema-mapping.subrange]] -==== `Subrange` - -When there is a <> bean in Spring configuration, -controller methods support a `Subrange

` argument where `

` is a relative position -converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`. -For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -@Controller -public class BookController { - - @QueryMapping - public Window books(ScrollSubrange subrange) { - ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial()) - int count = subrange.count().orElse(20); - // ... - } - -} ----- - -See <> for an overview of pagination and of built-in mechanisms. - - -[[controllers.schema-mapping.sort]] -==== `Sort` - -When there is a <> bean in Spring configuration, controller -methods support `Sort` as a method argument. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -@Controller -public class BookController { - - @QueryMapping - public Window books(Optional optionalSort) { - Sort sort = optionalSort.orElse(Sort.by(..)); - } - -} ----- - - - -[[controllers.schema-mapping.data-loader]] -==== `DataLoader` - -When you register a batch loading function for an entity, as explained in -<>, you can access the `DataLoader` for the entity by declaring a -method argument of type `DataLoader` and use it to load the entity: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -@Controller -public class BookController { - - public BookController(BatchLoaderRegistry registry) { - registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { - // return Map - }); - } - - @SchemaMapping - public CompletableFuture author(Book book, DataLoader loader) { - return loader.load(book.getAuthorId()); - } - -} ----- - -By default, `BatchLoaderRegistry` uses the full class name of the value type (e.g. the -class name for `Author`) for the key of the registration, and therefore simply declaring -the `DataLoader` method argument with generic types provides enough information -to locate it in the `DataLoaderRegistry`. As a fallback, the `DataLoader` method argument -resolver will also try the method argument name as the key but typically that should not -be necessary. - -Note that for many cases with loading related entities, where the `@SchemaMapping` simply -delegates to a `DataLoader`, you can reduce boilerplate by using a -<> method as described in the next section. - - -[[controllers.schema-mapping.validation]] -==== Validation - -When a `javax.validation.Validator` bean is found, `AnnotatedControllerConfigurer` enables support for -{spring-framework-ref-docs}/core.html#validation-beanvalidation-overview[Bean Validation] -on annotated controller methods. Typically, the bean is of type `LocalValidatorFactoryBean`. - -Bean validation lets you declare constraints on types: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -public class BookInput { - - @NotNull - private String title; - - @NotNull - @Size(max=13) - private String isbn; -} ----- - -You can then annotate a controller method parameter with `@Valid` to validate it before -method invocation: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @MutationMapping - public Book addBook(@Argument @Valid BookInput bookInput) { - // ... - } - } ----- - -If an error occurs during validation, a `ConstraintViolationException` is raised. -You can use the <> chain to decide how to present that to clients -by turning it into an error to include in the GraphQL response. - -TIP: In addition to `@Valid`, you can also use Spring's `@Validated` that allows -specifying validation groups. - -Bean validation is useful for <>, -<>, and -<> -method parameters, but applies more generally to any method parameter. - -[WARNING] -.Validation and Kotlin Coroutines -==== -Hibernate Validator is not compatible with Kotlin Coroutine methods and fails when -introspecting their method parameters. Please see -https://github.com/spring-projects/spring-graphql/issues/344#issuecomment-1082814093[spring-projects/spring-graphql#344 (comment)] -for links to relevant issues and a suggested workaround. -==== - - - -[[controllers.batch-mapping]] -=== `@BatchMapping` - -<> addresses the N+1 select problem through the use of an -`org.dataloader.DataLoader` to defer the loading of individual entity instances, so they -can be loaded together. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- -@Controller -public class BookController { - - public BookController(BatchLoaderRegistry registry) { - registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { - // return Map - }); - } - - @SchemaMapping - public CompletableFuture author(Book book, DataLoader loader) { - return loader.load(book.getAuthorId()); - } - -} ----- - -For the straight-forward case of loading an associated entity, shown above, the -`@SchemaMapping` method does nothing more than delegate to the `DataLoader`. This is -boilerplate that can be avoided with a `@BatchMapping` method. For example: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @BatchMapping - public Mono> author(List books) { - // ... - } - } ----- - -The above becomes a batch loading function in the `BatchLoaderRegistry` -where keys are `Book` instances and the loaded values their authors. In addition, a -`DataFetcher` is also transparently bound to the `author` field of the type `Book`, which -simply delegates to the `DataLoader` for authors, given its source/parent `Book` instance. - -[TIP] -==== -To be used as a unique key, `Book` must implement `hashcode` and `equals`. -==== - -By default, the field name defaults to the method name, while the type name defaults to -the simple class name of the input `List` element type. Both can be customized through -annotation attributes. The type name can also be inherited from a class level -`@SchemaMapping`. - - -[[controllers.batch-mapping.signature]] -==== Method Signature - -Batch mapping methods support the following arguments: - -[cols="1,2"] -|=== -| Method Argument | Description - -| `List` -| The source/parent objects. - -| `java.security.Principal` -| Obtained from Spring Security context, if available. - -| `@ContextValue` -| For access to a value from the `GraphQLContext` of `BatchLoaderEnvironment`, - which is the same context as the one from the `DataFetchingEnvironment`. - -| `GraphQLContext` -| For access to the context from the `BatchLoaderEnvironment`, - which is the same context as the one from the `DataFetchingEnvironment`. - -| `BatchLoaderEnvironment` -| The environment that is available in GraphQL Java to a -`org.dataloader.BatchLoaderWithContext`. - - -|=== - -Batch mapping methods can return: - -[cols="1,2"] -|=== -| Return Type | Description - -| `Mono>` -| A map with parent objects as keys, and batch loaded objects as values. - -| `Flux` -| A sequence of batch loaded objects that must be in the same order as the source/parent - objects passed into the method. - -| `Map`, `Collection` -| Imperative variants, e.g. without remote calls to make. - -| `Callable>`, `Callable>` -| Imperative variants to be invoked asynchronously. For this to work, - `AnnotatedControllerConfigurer` must be configured with an `Executor`. - -|=== - - - -[[controllers.exception-handler]] -=== `@GraphQlExceptionHandler` - -Use `@GraphQlExceptionHandler` methods to handle exceptions from data fetching with a -flexible <>. When declared in a -controller, exception handler methods apply to exceptions from the same controller: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @QueryMapping - public Book bookById(@Argument Long id) { - // ... - } - - @GraphQlExceptionHandler - public GraphQLError handle(BindException ex) { - return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); - } - - } ----- - -When declared in an `@ControllerAdvice`, exception handler methods apply across controllers: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - @ControllerAdvice - public class GlobalExceptionHandler { - - @GraphQlExceptionHandler - public GraphQLError handle(BindException ex) { - return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); - } - - } ----- - -Exception handling via `@GraphQlExceptionHandler` methods is applied automatically to -controller invocations. To handle exceptions from other `graphql.schema.DataFetcher` -implementations, not based on controller methods, obtain a -`DataFetcherExceptionResolver` from `AnnotatedControllerConfigurer`, and register it in -`GraphQlSource.Builder` as a <>. - - - - -[[controllers.exception-handler.signature]] -==== Method Signature - -Exception handler methods support a flexible method signature with method arguments -resolved from a `DataFetchingEnvironment,` and matching to those of -<>. - -Supported return types are listed below: - -[cols="1,2"] -|=== -| Return Type | Description - -| `graphql.GraphQLError` -| Resolve the exception to a single field error. - -| `Collection` -| Resolve the exception to multiple field errors. - -| `void` -| Resolve the exception without response errors. - -| `Object` -| Resolve the exception to a single error, to multiple errors, or none. - The return value must be `GraphQLError`, `Collection`, or `null`. - -| `Mono` -| For asynchronous resolution where `` is one of the supported, synchronous, return types. - -|=== +include::includes/controllers.adoc[leveloffset=+1] @@ -2163,27 +59,22 @@ The 1.0.x branch of this repository contains samples for - include::includes/observability.adoc[leveloffset=+1] - include::includes/graalvm-native.adoc[leveloffset=+1] - include::includes/client.adoc[leveloffset=+1] - include::includes/testing.adoc[leveloffset=+1] - [[boot-starter]] == Boot Starter