821 lines
26 KiB
Plaintext
821 lines
26 KiB
Plaintext
[[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 xref:boot-starter.adoc[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 xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
|
|
|
|
| `@Argument Map<String, Object>`
|
|
| For access to the raw argument value.
|
|
|
|
See xref:controllers.adoc#controllers.schema-mapping.argument[`@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 xref:controllers.adoc#controllers.schema-mapping.argument-value[`ArgumentValue`].
|
|
|
|
| `@Arguments`
|
|
| For access to all field arguments bound to a higher-level, typed Object.
|
|
|
|
See xref:controllers.adoc#controllers.schema-mapping.arguments[`@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 xref:controllers.adoc#controllers.schema-mapping.projectedpayload.argument[`@ProjectedPayload` Interface].
|
|
|
|
| "Source"
|
|
| For access to the source (i.e. parent/container) instance of the field.
|
|
|
|
See xref:controllers.adoc#controllers.schema-mapping.source[Source].
|
|
|
|
| `Subrange` and `ScrollSubrange`
|
|
| For access to pagination arguments.
|
|
|
|
See xref:request-execution.adoc#execution.pagination[Pagination], xref:data.adoc#data.pagination.scroll[Scroll], xref:controllers.adoc#controllers.schema-mapping.subrange[`Subrange`].
|
|
|
|
| `Sort`
|
|
| For access to sort details.
|
|
|
|
See xref:request-execution.adoc#execution.pagination[Pagination], xref:controllers.adoc#controllers.schema-mapping.sort[`Sort`].
|
|
|
|
| `DataLoader`
|
|
| For access to a `DataLoader` in the `DataLoaderRegistry`.
|
|
|
|
See xref:controllers.adoc#controllers.schema-mapping.data-loader[`DataLoader`].
|
|
|
|
| `@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 xref:request-execution.adoc#execution.reactive-datafetcher[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 xref:controllers.adoc#controllers.schema-mapping.arguments[`@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 xref:controllers.adoc#controllers.schema-mapping.argument[`@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 xref:controllers.adoc#controllers.schema-mapping.argument[`@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 xref:controllers.adoc#controllers.batch-mapping[`@BatchMapping`] 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 xref:request-execution.adoc#execution.pagination.cursor.strategy[`CursorStrategy`] 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(ScrollPosition.offset());
|
|
int count = subrange.count().orElse(20);
|
|
// ...
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
See xref:request-execution.adoc#execution.pagination[Pagination] for an overview of pagination and of built-in mechanisms.
|
|
|
|
|
|
[[controllers.schema-mapping.sort]]
|
|
=== `Sort`
|
|
|
|
When there is a xref:data.adoc#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
|
|
xref:request-execution.adoc#execution.batching[Batch Loading], 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
|
|
xref:controllers.adoc#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/validation/beanvalidation.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 xref:request-execution.adoc#execution.exceptions[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 xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`],
|
|
xref:controllers.adoc#controllers.schema-mapping.arguments[`@Arguments`], and
|
|
xref:controllers.adoc#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`
|
|
|
|
xref:request-execution.adoc#execution.batching[Batch Loading] 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 xref:controllers.adoc#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 xref:request-execution.adoc#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
|
|
xref:controllers.adoc#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.
|
|
|
|
|===
|
|
|
|
|
|
|
|
[[controllers.namespacing]]
|
|
== Namespacing
|
|
|
|
At the schema level, query and mutation operations are defined directly under the `Query` and `Mutation` types.
|
|
Rich GraphQL APIs can define dozens of operation sunder those types, making it harder to explore the API and separate concerns.
|
|
You can choose to https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/[define Namespaces in your GraphQL schema].
|
|
While there are some caveats with this approach, you can implement this pattern with Spring for GraphQL annotated controllers.
|
|
|
|
With namespacing, your GraphQL schema can, for example, nest query operations under top-level types, instead of listing them directly under `Query`.
|
|
Here, we will define `MusicQueries` and `UserQueries` types and make them available under `Query`:
|
|
|
|
[source,json,subs="verbatim,quotes"]
|
|
----
|
|
include::ROOT:{include-resources}/controllers/namespaces.graphqls[]
|
|
----
|
|
|
|
A GraphQL client would use the `album` query like this:
|
|
|
|
[source,graphql,subs="verbatim,quotes"]
|
|
----
|
|
{
|
|
music {
|
|
album(id: 42) {
|
|
id
|
|
title
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
And get the following response:
|
|
|
|
[source,json,subs="verbatim,quotes"]
|
|
----
|
|
{
|
|
"data": {
|
|
"music": {
|
|
"album": {
|
|
"id": "42",
|
|
"title": "Spring for GraphQL"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
This can be implemented in a `@Controller` with the following pattern:
|
|
|
|
include-code::MusicController[]
|
|
<1> Annotate the controller with `@SchemaMapping` and a `typeName` attribute, to avoid repeating it on methods
|
|
<2> Define a `@QueryMapping` for the "music" namespace
|
|
<3> The "music" query returns an "empty" record, but could also return an empty map
|
|
<4> Queries are now declared as fields under the "MusicQueries" type
|
|
|
|
Instead of declaring wrapping types ("MusicQueries", "UserQueries") explicitly in controllers,
|
|
you can choose to configure them with the runtime wiring using a `GraphQlSourceBuilderCustomizer` with Spring Boot:
|
|
|
|
include-code::NamespaceConfiguration[]
|
|
<1> List all the wrapper types for the "Query" type
|
|
<2> Manually declare data fetchers for each of them, returning an empty Map
|