Extract large sections of ref docs into includes
This commit is contained in:
756
spring-graphql-docs/src/docs/asciidoc/includes/controllers.adoc
Normal file
756
spring-graphql-docs/src/docs/asciidoc/includes/controllers.adoc
Normal file
@@ -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 <<boot-starter>> 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<Book> 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 <<controllers.schema-mapping.argument>>.
|
||||
|
||||
| `@Argument Map<String, Object>`
|
||||
| For access to the raw argument value.
|
||||
|
||||
See <<controllers.schema-mapping.argument>>.
|
||||
|
||||
| `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 <<controllers.schema-mapping.argument-value>>.
|
||||
|
||||
| `@Arguments`
|
||||
| For access to all field arguments bound to a higher-level, typed Object.
|
||||
|
||||
See <<controllers.schema-mapping.arguments>>.
|
||||
|
||||
| `@Arguments Map<String, Object>`
|
||||
| For access to the raw map of arguments.
|
||||
|
||||
| `@ProjectedPayload` Interface
|
||||
| For access to field arguments through a project interface.
|
||||
|
||||
See <<controllers.schema-mapping.projectedpayload.argument>>.
|
||||
|
||||
| "Source"
|
||||
| For access to the source (i.e. parent/container) instance of the field.
|
||||
|
||||
See <<controllers.schema-mapping.source>>.
|
||||
|
||||
| `Subrange` and `ScrollSubrange`
|
||||
| For access to pagination arguments.
|
||||
|
||||
See <<execution.pagination>>, <<data.pagination.scroll>>, <<controllers.schema-mapping.subrange>>.
|
||||
|
||||
| `Sort`
|
||||
| For access to sort details.
|
||||
|
||||
See <<execution.pagination>>, <<controllers.schema-mapping.sort>>.
|
||||
|
||||
| `DataLoader`
|
||||
| For access to a `DataLoader` in the `DataLoaderRegistry`.
|
||||
|
||||
See <<controllers.schema-mapping.data-loader>>.
|
||||
|
||||
| `@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<Locale>`
|
||||
| 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 <<execution.reactive-datafetcher>>.
|
||||
- `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<String, Object>` 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<String, Object> bookInput) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: Prior to 1.2, `@Argument Map<String, Object>` returned the full arguments map if
|
||||
the annotation did not specify a name. After 1.2, `@Argument` with
|
||||
`Map<String, Object>` 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 <<controllers.schema-mapping.arguments>> 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 <<controllers.schema-mapping.argument>>
|
||||
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> 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<String, Object>` 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 <<controllers.schema-mapping.argument>>,
|
||||
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 <<controllers.batch-mapping>> 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 <<execution.pagination.cursor.strategy>> bean in Spring configuration,
|
||||
controller methods support a `Subrange<P>` argument where `<P>` 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<Book> books(ScrollSubrange subrange) {
|
||||
ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial())
|
||||
int count = subrange.count().orElse(20);
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
See <<execution.pagination>> for an overview of pagination and of built-in mechanisms.
|
||||
|
||||
|
||||
[[controllers.schema-mapping.sort]]
|
||||
=== `Sort`
|
||||
|
||||
When there is a <<data.pagination.scroll,SortStrategy>> 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<Book> books(Optional<Sort> 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
|
||||
<<execution.batching>>, 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<Long, Author>
|
||||
});
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> 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
|
||||
<<controllers.batch-mapping,@BatchMapping>> 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 <<execution.exceptions>> 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 <<controllers.schema-mapping.argument>>,
|
||||
<<controllers.schema-mapping.arguments>>, and
|
||||
<<controllers.schema-mapping.projectedpayload.argument,@ProjectedPayload>>
|
||||
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`
|
||||
|
||||
<<execution.batching>> 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<Long, Author>
|
||||
});
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> 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<Map<Book, Author>> author(List<Book> 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<K>`
|
||||
| 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<Map<K,V>>`
|
||||
| A map with parent objects as keys, and batch loaded objects as values.
|
||||
|
||||
| `Flux<V>`
|
||||
| A sequence of batch loaded objects that must be in the same order as the source/parent
|
||||
objects passed into the method.
|
||||
|
||||
| `Map<K,V>`, `Collection<V>`
|
||||
| Imperative variants, e.g. without remote calls to make.
|
||||
|
||||
| `Callable<Map<K,V>>`, `Callable<Collection<V>>`
|
||||
| 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 <<controllers.exception-handler.signature,method signature>>. 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 <<execution.exceptions,DataFetcherExceptionResolver>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[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
|
||||
<<controllers.schema-mapping.arguments,@SchemaMapping methods>>.
|
||||
|
||||
Supported return types are listed below:
|
||||
|
||||
[cols="1,2"]
|
||||
|===
|
||||
| Return Type | Description
|
||||
|
||||
| `graphql.GraphQLError`
|
||||
| Resolve the exception to a single field error.
|
||||
|
||||
| `Collection<GraphQLError>`
|
||||
| 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<GraphQLError>`, or `null`.
|
||||
|
||||
| `Mono<T>`
|
||||
| For asynchronous resolution where `<T>` is one of the supported, synchronous, return types.
|
||||
|
||||
|===
|
||||
482
spring-graphql-docs/src/docs/asciidoc/includes/data.adoc
Normal file
482
spring-graphql-docs/src/docs/asciidoc/includes/data.adoc
Normal file
@@ -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<Account, Long>,
|
||||
QuerydslPredicateExecutor<Account> {
|
||||
}
|
||||
----
|
||||
|
||||
Then use it to create a `DataFetcher`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// For single result queries
|
||||
DataFetcher<Account> dataFetcher =
|
||||
QuerydslDataFetcher.builder(repository).single();
|
||||
|
||||
// For multi-result queries
|
||||
DataFetcher<Iterable<Account>> dataFetcher =
|
||||
QuerydslDataFetcher.builder(repository).many();
|
||||
|
||||
// For paginated queries
|
||||
DataFetcher<Iterable<Account>> dataFetcher =
|
||||
QuerydslDataFetcher.builder(repository).scrollable();
|
||||
----
|
||||
|
||||
You can now register the above `DataFetcher` through a
|
||||
<<execution.graphqlsource.runtimewiring-configurer>>.
|
||||
|
||||
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<Mono<Account>>` or `DataFetcher<Flux<Account>>`. 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
|
||||
----
|
||||
<dependencies>
|
||||
<!-- ... -->
|
||||
<dependency>
|
||||
<groupId>com.querydsl</groupId>
|
||||
<artifactId>querydsl-apt</artifactId>
|
||||
<version>${querydsl.version}</version>
|
||||
<classifier>jpa</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.javax.persistence</groupId>
|
||||
<artifactId>hibernate-jpa-2.1-api</artifactId>
|
||||
<version>1.0.2.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<plugins>
|
||||
<!-- Annotation processor configuration -->
|
||||
<plugin>
|
||||
<groupId>com.mysema.maven</groupId>
|
||||
<artifactId>apt-maven-plugin</artifactId>
|
||||
<version>${apt-maven-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>process</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>target/generated-sources/java</outputDirectory>
|
||||
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
----
|
||||
|
||||
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 <<data.querydsl.registration>>. 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 <<data.projections>>.
|
||||
|
||||
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<AccountProjection> dataFetcher =
|
||||
QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single();
|
||||
|
||||
// For multi-result queries
|
||||
DataFetcher<Iterable<AccountProjection>> 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 <<execution.pagination,paginated>> 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 <<boot-starter>> automatically
|
||||
detects `@GraphQlRepository` beans and uses them to initialize the
|
||||
`RuntimeWiringConfigurer` with.
|
||||
|
||||
Auto-registration applies <<data.querybyexample.customizations, customizations>>
|
||||
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<Account, Long>,
|
||||
QueryByExampleExecutor<Account> {
|
||||
}
|
||||
----
|
||||
|
||||
Use `QueryByExampleDataFetcher` to turn the repository into a `DataFetcher`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// For single result queries
|
||||
DataFetcher<Account> dataFetcher =
|
||||
QueryByExampleDataFetcher.builder(repository).single();
|
||||
|
||||
// For multi-result queries
|
||||
DataFetcher<Iterable<Account>> dataFetcher =
|
||||
QueryByExampleDataFetcher.builder(repository).many();
|
||||
|
||||
// For paginated queries
|
||||
DataFetcher<Iterable<Account>> dataFetcher =
|
||||
QueryByExampleDataFetcher.builder(repository).scrollable();
|
||||
----
|
||||
|
||||
You can now register the above `DataFetcher` through a
|
||||
<<execution.graphqlsource.runtimewiring-configurer>>.
|
||||
|
||||
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<Mono<Account>>` or `DataFetcher<Flux<Account>>`. 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 <<data.projections>>.
|
||||
|
||||
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<AccountProjection> dataFetcher =
|
||||
QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single();
|
||||
|
||||
// For multi-result queries
|
||||
DataFetcher<Iterable<AccountProjection>> 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 <<execution.pagination,paginated>> 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 <<boot-starter>> automatically
|
||||
detects `@GraphQlRepository` beans and uses them to initialize the
|
||||
`RuntimeWiringConfigurer` with.
|
||||
|
||||
Auto-registration applies <<data.querybyexample.customizations, customizations>>
|
||||
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
|
||||
<<data.querydsl>> and the <<data.querybyexample>> 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 <<execution.pagination>>, 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<ScrollPosition> 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
|
||||
<<controllers.schema-mapping.subrange,ScrollSubrange>> method argument to paginate forward
|
||||
or backward. For this to work, you must declare a <<execution.pagination.cursor.strategy>>
|
||||
supports `ScrollPosition` as a bean.
|
||||
|
||||
The <<boot-starter>> declares a `CursorStrategy<ScrollPosition>` 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<Map<String, Object>>`.
|
||||
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.
|
||||
@@ -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 <<server.transports.http>>, delegate to
|
||||
`ExecutionGraphQlService` to handle requests.
|
||||
|
||||
The main implementation, `DefaultExecutionGraphQlService`, is configured with a
|
||||
`GraphQlSource` for access to the `graphql.GraphQL` instance to invoke.
|
||||
|
||||
|
||||
|
||||
[[execution.graphqlsource]]
|
||||
== `GraphQLSource`
|
||||
|
||||
`GraphQlSource` is a contract to expose the `graphql.GraphQL` instance to use that also
|
||||
includes a builder API to build that instance. The default builder is available via
|
||||
`GraphQlSource.schemaResourceBuilder()`.
|
||||
|
||||
The <<boot-starter>> creates an instance of this builder and further initializes it
|
||||
to <<execution.graphqlsource.schema-resources, load schema files>> from a configurable location,
|
||||
to {spring-boot-ref-docs}/application-properties.html#appendix.application-properties.web[expose properties]
|
||||
to apply to `GraphQlSource.Builder`, to detect
|
||||
<<execution.graphqlsource.runtimewiring-configurer>> 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
|
||||
<<execution.exceptions, exception resolution>>. For further customizations, you can also
|
||||
declare a `GraphQlSourceBuilderCustomizer` bean, for example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class GraphQlConfig {
|
||||
|
||||
@Bean
|
||||
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
|
||||
return (builder) ->
|
||||
builder.configureGraphQl(graphQlBuilder ->
|
||||
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[execution.graphqlsource.schema-resources]]
|
||||
=== Schema Resources
|
||||
|
||||
`GraphQlSource.Builder` can be configured with one or more `Resource` instances to be
|
||||
parsed and merged together. That means schema files can be loaded from just about any
|
||||
location.
|
||||
|
||||
By default, the Boot starter
|
||||
{spring-boot-ref-docs}/web.html#web.graphql.schema[looks for schema files] with extensions
|
||||
".graphqls" or ".gqls" under the location `classpath:graphql/**`, which is typically
|
||||
`src/main/resources/graphql`. You can also use a file system location, or any location
|
||||
supported by the Spring `Resource` hierarchy, including a custom implementation that
|
||||
loads schema files from remote locations, from storage, or from memory.
|
||||
|
||||
TIP: Use `classpath*:graphql/**/` to find schema files across multiple classpath
|
||||
locations, e.g. across multiple modules.
|
||||
|
||||
|
||||
[[execution.graphqlsource.schema-creation]]
|
||||
=== Schema Creation
|
||||
|
||||
By default, `GraphQlSource.Builder` uses the GraphQL Java `SchemaGenerator` to create the
|
||||
`graphql.schema.GraphQLSchema`. This works for typical use, but if you need to use a
|
||||
different generator, e.g. for federation, you can register a `schemaFactory` callback:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
GraphQlSource.Builder builder = ...
|
||||
|
||||
builder.schemaResources(..)
|
||||
.configureRuntimeWiring(..)
|
||||
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
|
||||
// create GraphQLSchema
|
||||
})
|
||||
----
|
||||
|
||||
The <<execution.graphqlsource, GraphQlSource section>> explains how to configure that with Spring Boot.
|
||||
|
||||
For an example with Apollo Federation, see
|
||||
https://github.com/apollographql/federation-jvm-spring-example[federation-jvm-spring-example].
|
||||
|
||||
|
||||
[[execution.graphqlsource.runtimewiring-configurer]]
|
||||
=== `RuntimeWiringConfigurer`
|
||||
|
||||
You can use `RuntimeWiringConfigurer` to register:
|
||||
|
||||
- Custom scalar types.
|
||||
- <<execution.graphqlsource.directives>> handling code.
|
||||
- Default <<execution.graphqlsource.default-type-resolver>> for interface and union types.
|
||||
- `DataFetcher` for a field although applications will typically use <<controllers>>, and
|
||||
those are detected and registered as `DataFetcher`s by `AnnotatedControllerConfigurer`,
|
||||
which is a `RuntimeWiringConfigurer`. The <<boot-starter>> automatically registers
|
||||
`AnnotatedControllerConfigurer`.
|
||||
|
||||
NOTE: GraphQL Java, server applications use Jackson only for serialization to and from maps of data.
|
||||
Client input is parsed into a map. Server output is assembled into a map based on the field selection set.
|
||||
This means you can't rely on Jackson serialization/deserialization annotations.
|
||||
Instead, you can use https://www.graphql-java.com/documentation/scalars/[custom scalar types].
|
||||
|
||||
The <<boot-starter>> detects beans of type `RuntimeWiringConfigurer` and
|
||||
registers them in the `GraphQlSource.Builder`. That means in most cases, you'll' have
|
||||
something like the following in your configuration:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
public class GraphQlConfig {
|
||||
|
||||
@Bean
|
||||
public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {
|
||||
|
||||
GraphQLScalarType scalarType = ... ;
|
||||
SchemaDirectiveWiring directiveWiring = ... ;
|
||||
DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();
|
||||
|
||||
return wiringBuilder -> wiringBuilder
|
||||
.scalar(scalarType)
|
||||
.directiveWiring(directiveWiring)
|
||||
.type("Query", builder -> builder.dataFetcher("book", dataFetcher));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If you need to add a `WiringFactory`, e.g. to make registrations that take into account
|
||||
schema definitions, implement the alternative `configure` method that accepts both the
|
||||
`RuntimeWiring.Builder` and an output `List<WiringFactory>`. This allows you to add any
|
||||
number of factories that are then invoked in sequence.
|
||||
|
||||
|
||||
[[execution.graphqlsource.default-type-resolver]]
|
||||
=== `TypeResolver`
|
||||
|
||||
`GraphQlSource.Builder` registers `ClassNameTypeResolver` as the default `TypeResolver`
|
||||
to use for GraphQL Interfaces and Unions that don't already have such a registration
|
||||
through a <<execution.graphqlsource.runtimewiring-configurer>>. 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 <<execution.graphqlsource, GraphQlSource section>> explains how to configure that with Spring Boot.
|
||||
|
||||
|
||||
[[execution.graphqlsource.directives]]
|
||||
=== Directives
|
||||
|
||||
The GraphQL language supports directives that "describe alternate runtime execution and
|
||||
type validation behavior in a GraphQL document". Directives are similar to annotations in
|
||||
Java but declared on types, fields, fragments and operations in a GraphQL document.
|
||||
|
||||
GraphQL Java provides the `SchemaDirectiveWiring` contract to help applications detect
|
||||
and handle directives. For more details, see
|
||||
{graphql-java-docs}/sdl-directives/[Schema Directives] in the
|
||||
GraphQL Java documentation.
|
||||
|
||||
In Spring GraphQL you can register a `SchemaDirectiveWiring` through a
|
||||
<<execution.graphqlsource.runtimewiring-configurer>>. The <<boot-starter>> detects
|
||||
such beans, so you might have something like:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
public class GraphQlConfig {
|
||||
|
||||
@Bean
|
||||
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
|
||||
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
TIP: For an example of directives support check out the
|
||||
https://github.com/graphql-java/graphql-java-extended-validation[Extended Validation for Graphql Java]
|
||||
library.
|
||||
|
||||
|
||||
[[execution.graphqlsource.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 <<execution.graphqlsource.schema-traversal>> so generally
|
||||
prefer traversal to transformation unless you need to make schema changes.
|
||||
|
||||
|
||||
[[execution.graphqlsource.schema-traversal]]
|
||||
=== Schema Traversal
|
||||
|
||||
You can register a `graphql.schema.GraphQLTypeVisitor` via
|
||||
`builder.schemaResources(..).typeVisitors(..)` if you want to traverse the schema after
|
||||
it is created, and possibly apply changes to the `GraphQLCodeRegistry`. Keep in mind,
|
||||
however, that such a visitor cannot change the schema. See
|
||||
<<execution.graphqlsource.schema-transformation>>, if you need to make changes to the schema.
|
||||
|
||||
|
||||
[[execution.graphqlsource.schema-mapping-inspection]]
|
||||
=== Schema Mapping Inspection
|
||||
|
||||
If a query, mutation, or subscription operation does not have a `DataFetcher`, it won't
|
||||
return any data, and won't do anything useful. Likewise, fields on schema types returned
|
||||
by an operation that are covered neither explicitly through a `DataFetcher`
|
||||
registration, nor implicitly by the default `PropertyDataFetcher`, which looks for a
|
||||
matching Java object property, will always be `null`.
|
||||
|
||||
GraphQL Java does not perform checks to ensure every schema field is covered, and that
|
||||
can result in gaps that might not be discovered depending on test coverage. At runtime
|
||||
you may get a "silent" `null`, or an error if the field is not nullable. As a lower level
|
||||
library, GraphQL Java simply does not know enough about `DataFetcher` implementations and
|
||||
their return types, and therefore can't compare schema type structure against Java object
|
||||
structure.
|
||||
|
||||
Spring for GraphQL defines the `SelfDescribingDataFetcher` interface to allow a
|
||||
`DataFetcher` to expose return type information. All Spring `DataFetcher` implementations
|
||||
implement this interface. That includes those for <<controllers>>, and those for
|
||||
<<data.querydsl>> and <<data.querybyexample>> 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 <<execution.graphqlsource, GraphQlSource section>> 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.webflux, WebFlux Context>>.
|
||||
|
||||
|
||||
|
||||
[[execution.context]]
|
||||
== Context Propagation
|
||||
|
||||
Spring for GraphQL provides support to transparently propagate context from the
|
||||
<<server.transports.http>>, 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
|
||||
<<server.interception, `WebGraphQlInterceptor`>> or `DataFetcher` switches to a
|
||||
different thread.
|
||||
|
||||
Spring for GraphQL supports propagating `ThreadLocal` values from the Servlet container
|
||||
thread to the thread a `DataFetcher` and other components invoked by GraphQL Java to
|
||||
execute on. To do this, an application needs to implement
|
||||
`io.micrometer.context.ThreadLocalAccessor` for a `ThreadLocal` values of interest:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {
|
||||
|
||||
@Override
|
||||
public Object key() {
|
||||
return RequestAttributesAccessor.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestAttributes getValue() {
|
||||
return RequestContextHolder.getRequestAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(RequestAttributes attributes) {
|
||||
RequestContextHolder.setRequestAttributes(attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
You can register a `ThreadLocalAccessor` manually on startup with the global
|
||||
`ContextRegistry` instance, which is accessible via
|
||||
`io.micrometer.context.ContextRegistry#getInstance()`. You can also register it
|
||||
automatically through the `java.util.ServiceLoader` mechanism.
|
||||
|
||||
|
||||
[[execution.context.webflux]]
|
||||
=== WebFlux
|
||||
|
||||
A <<execution.reactive-datafetcher>> can rely on access to Reactor context that
|
||||
originates from the WebFlux request handling chain. This includes Reactor context
|
||||
added by <<server.interception, WebGraphQlInterceptor>> components.
|
||||
|
||||
|
||||
|
||||
[[execution.exceptions]]
|
||||
== Exceptions
|
||||
|
||||
In GraphQL Java, `DataFetcherExceptionHandler` decides how to represent exceptions from
|
||||
data fetching in the "errors" section of the response. An application can register a
|
||||
single handler only.
|
||||
|
||||
Spring for GraphQL registers a `DataFetcherExceptionHandler` that provides default
|
||||
handling and enables the `DataFetcherExceptionResolver` contract. An application can
|
||||
register any number of resolvers via <<execution.graphqlsource>> builder and those are in
|
||||
order until one them resolves the `Exception` to a `List<graphql.GraphQLError>`.
|
||||
The Spring Boot starter detects beans of this type.
|
||||
|
||||
`DataFetcherExceptionResolverAdapter` is a convenient base class with protected methods
|
||||
`resolveToSingleError` and `resolveToMultipleErrors`.
|
||||
|
||||
The <<controllers>> programming model enables handling data fetching exceptions with
|
||||
annotated exception handler methods with a flexible method signature, see
|
||||
<<controllers.exception-handler>> 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 <<server.interception.web>>.
|
||||
|
||||
|
||||
[[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 <<boot-starter>> registers `ConnectionTypeDefinitionConfigurer` by default.
|
||||
|
||||
|
||||
[[execution.pagination.adapters]]
|
||||
=== `ConnectionAdapter`
|
||||
|
||||
Once <<execution.pagination.types>> are available in the schema, you also need
|
||||
equivalent Java types. GraphQL Java provides those, including generic `Connection` and
|
||||
`Edge`, as well as a `PageInfo`.
|
||||
|
||||
One option is to populate a `Connection` and return it from your controller method or
|
||||
`DataFetcher`. However, this requires boilerplate code to create the `Connection`,
|
||||
creating cursors, wrapping each item as an `Edge`, and creating the `PageInfo`.
|
||||
Moreover, you may already have an underlying pagination mechanism such as when using
|
||||
Spring Data repositories.
|
||||
|
||||
Spring for GraphQL defines the `ConnectionAdapter` contract to adapt a container of items
|
||||
to `Connection`. Adapters are applied through a `DataFetcher` decorator that is in turn
|
||||
installed through a `ConnectionFieldTypeVisitor`. You can configure it as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ConnectionAdapter adapter = ... ;
|
||||
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) // <1>
|
||||
|
||||
GraphQlSource.schemaResourceBuilder()
|
||||
.schemaResources(..)
|
||||
.typeDefinitionConfigurer(..)
|
||||
.typeVisitors(List.of(visitor)) // <2>
|
||||
----
|
||||
|
||||
<1> Create type visitor with one or more ``ConnectionAdapter``s.
|
||||
<2> Resister the type visitor.
|
||||
|
||||
There are <<data.pagination.scroll,built-in>> ``ConnectionAdapter``s for Spring Data's
|
||||
`Window` and `Slice`. You can also create your own custom adapter. `ConnectionAdapter`
|
||||
implementations rely on a <<execution.pagination.cursor.strategy>> to
|
||||
create cursors for returned items. The same strategy is also used to support the
|
||||
<<controllers.schema-mapping.subrange>> controller method argument that contains
|
||||
pagination input.
|
||||
|
||||
|
||||
[[execution.pagination.cursor.strategy]]
|
||||
=== `CursorStrategy`
|
||||
|
||||
`CursorStrategy` is a contract to encode and decode a String cursor that refers to the
|
||||
position of an item within a large result set. The cursor can be based on an index or
|
||||
on a keyset.
|
||||
|
||||
A <<execution.pagination.adapters>> uses this to encode cursors for returned items.
|
||||
<<controllers>> methods, <<data.querydsl>> repositories, and <<data.querybyexample>>
|
||||
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 <<data.pagination.scroll,built-in>> `CursorStrategy` for the Spring Data
|
||||
`ScrollPosition`. The <<boot-starter>> registers a `CursorStrategy<ScrollPosition>` with
|
||||
`Base64Encoder` when Spring Data is present.
|
||||
|
||||
|
||||
[[execution.pagination.sort.strategy]]
|
||||
=== Sort
|
||||
|
||||
There is no standard way to provide sort information in a GraphQL request. However,
|
||||
pagination depends on a stable sort order. You can use a default order, or otherwise
|
||||
expose input types and extract sort details from GraphQL arguments.
|
||||
|
||||
There is <<data.pagination.sort,built-in>> support for Spring Data's `Sort` as a controller
|
||||
method argument. For this to work, you need to have a `SortStrategy` bean.
|
||||
|
||||
|
||||
[[execution.batching]]
|
||||
== Batch Loading
|
||||
|
||||
Given a `Book` and its `Author`, we can create one `DataFetcher` for a book and another
|
||||
for its author. This allows selecting books with or without authors, but it means books
|
||||
and authors aren't loaded together, which is especially inefficient when querying multiple
|
||||
books as the author for each book is loaded individually. This is known as the N+1 select
|
||||
problem.
|
||||
|
||||
|
||||
[[execution.batching.dataloader]]
|
||||
=== `DataLoader`
|
||||
|
||||
GraphQL Java provides a `DataLoader` mechanism for batch loading of related entities.
|
||||
You can find the full details in the
|
||||
{graphql-java-docs}/batching/[GraphQL Java docs]. Below is a
|
||||
summary of how it works:
|
||||
|
||||
1. Register ``DataLoader``'s in the `DataLoaderRegistry` that can load entities, given unique keys.
|
||||
2. ``DataFetcher``'s can access ``DataLoader``'s and use them to load entities by id.
|
||||
3. A `DataLoader` defers loading by returning a future so it can be done in a batch.
|
||||
4. ``DataLoader``'s maintain a per request cache of loaded entities that can further
|
||||
improve efficiency.
|
||||
|
||||
|
||||
[[execution.batching.batch-loader-registry]]
|
||||
=== `BatchLoaderRegistry`
|
||||
|
||||
The complete batching loading mechanism in GraphQL Java requires implementing one of
|
||||
several `BatchLoader` interface, then wrapping and registering those as ``DataLoader``s
|
||||
with a name in the `DataLoaderRegistry`.
|
||||
|
||||
The API in Spring GraphQL is slightly different. For registration, there is only one,
|
||||
central `BatchLoaderRegistry` exposing factory methods and a builder to create and
|
||||
register any number of batch loading functions:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
public class MyConfig {
|
||||
|
||||
public MyConfig(BatchLoaderRegistry registry) {
|
||||
|
||||
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
|
||||
// return Mono<Map<Long, Author>
|
||||
});
|
||||
|
||||
// more registrations ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
The <<boot-starter>> declares a `BatchLoaderRegistry` bean that you can inject into
|
||||
your configuration, as shown above, or into any component such as a controller in order
|
||||
register batch loading functions. In turn the `BatchLoaderRegistry` is injected into
|
||||
`DefaultExecutionGraphQlService` where it ensures `DataLoader` registrations per request.
|
||||
|
||||
By default, the `DataLoader` name is based on the class name of the target entity.
|
||||
This allows an `@SchemaMapping` method to declare a
|
||||
<<controllers.schema-mapping.data-loader,DataLoader argument>> with a generic type, and
|
||||
without the need for specifying a name. The name, however, can be customized through the
|
||||
`BatchLoaderRegistry` builder, if necessary, along with other `DataLoaderOptions`.
|
||||
|
||||
To configure default `DataLoaderOptions` globally, to use as a starting point for any
|
||||
registration, you can override Boot's `BatchLoaderRegistry` bean and use the constructor
|
||||
for `DefaultBatchLoaderRegistry` that accepts `Supplier<DataLoaderOptions>`.
|
||||
|
||||
For many cases, when loading related entities, you can use
|
||||
<<controllers.batch-mapping,@BatchMapping>> controller methods, which are a shortcut
|
||||
for and replace the need to use `BatchLoaderRegistry` and `DataLoader` directly.
|
||||
|
||||
`BatchLoaderRegistry` provides other important benefits too. It supports access to
|
||||
the same `GraphQLContext` from batch loading functions and from `@BatchMapping` methods,
|
||||
as well as ensures <<execution.context>> to them. This is why applications are expected
|
||||
to use it. It is possible to perform your own `DataLoader` registrations directly but
|
||||
such registrations would forgo the above benefits.
|
||||
|
||||
|
||||
[[execution.batching.testing]]
|
||||
=== Testing Batch Loading
|
||||
|
||||
Start by having `BatchLoaderRegistry` perform registrations on a `DataLoaderRegistry`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
|
||||
// perform registrations...
|
||||
|
||||
DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
|
||||
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);
|
||||
----
|
||||
|
||||
Now you can access and test individual ``DataLoader``'s as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
|
||||
loader.load(1L);
|
||||
loader.loadMany(Arrays.asList(2L, 3L));
|
||||
List<Book> books = loader.dispatchAndJoin(); // actual loading
|
||||
|
||||
assertThat(books).hasSize(3);
|
||||
assertThat(books.get(0).getName()).isEqualTo("...");
|
||||
// ...
|
||||
----
|
||||
147
spring-graphql-docs/src/docs/asciidoc/includes/transports.adoc
Normal file
147
spring-graphql-docs/src/docs/asciidoc/includes/transports.adoc
Normal file
@@ -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
|
||||
<<server.interception>> 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
|
||||
<<boot-starter>> 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 <<server.interception>> 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 <<boot-starter>> 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`
|
||||
|
||||
<<server.transports.http>> and <<server.transports.websocket>> 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 <<boot-starter>>, see
|
||||
{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints].
|
||||
|
||||
|
||||
[[server.interception.rsocket]]
|
||||
=== `RSocketQlInterceptor`
|
||||
|
||||
Similar to <<server.interception.web>>, 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`.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user