6. 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:
@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".
6.1. 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 Spring 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
GraphQL RuntimeWiring section
in the Boot starter documentation.
6.2. @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:
@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":
@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.
@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:
@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..
6.2.1. Method Signature
Schema mapping handler methods can have any of the following method arguments:
Method Argument | Description |
---|---|
|
For access to a named field argument bound to a higher-level, typed Object.
See |
|
For access to all field arguments bound to a higher-level, typed Object.
See |
|
For access to the raw map of arguments, where |
|
For access to the raw map of arguments. |
|
For access to field arguments through a project interface.
See |
"Source" |
For access to the source (i.e. parent/container) instance of the field. See Source. |
|
For access to a |
|
For access to an attribute from the main |
|
For access to an attribute from the local |
|
For access to the context from the |
|
Obtained from the Spring Security context, if available. |
|
For access to |
|
For access to the selection set for the query through the |
|
For access to the |
|
For direct access to the underlying |
Schema mapping handler methods can return:
-
A resolved value of any type.
-
Mono
andFlux
for asynchronous value(s). Supported for controller methods and for anyDataFetcher
as described in ReactiveDataFetcher
. -
java.util.concurrent.Callable
to have the value(s) produced asynchronously. For this to work,AnnotatedControllerConfigurer
must be configured with anExecutor
.
6.2.2. @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:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
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")
.
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 map of
all argument values. The name attribute on @Argument
must not be set.
6.2.3. @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.
6.2.4. @ProjectedPayload
Interface
As an alternative to using complete Objects with @Argument
,
you can also use a projection interface to access GraphQL request arguments through a
well-defined, minimal interface. Argument projections are provided by
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:
@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();
}
6.2.5. 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.
@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.
A |
6.2.6. DataLoader
When you register a batch loading function for an entity, as explained in
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:
@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
@BatchMapping method as described in the next section.
6.2.7. Validation
When a javax.validation.Validator
bean is found, AnnotatedControllerConfigurer
enables support for
Bean Validation
on annotated controller methods. Typically, the bean is of type LocalValidatorFactoryBean
.
Bean validation lets you declare constraints on types:
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:
@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 Exception Resolution chain to decide how to present that to clients
by turning it into an error to include in the GraphQL response.
In addition to @Valid , you can also use Spring’s @Validated that allows
specifying validation groups.
|
Bean validation is useful for @Argument
,
@Arguments
, and
@ProjectedPayload
method parameters, but applies more generally to any method parameter.
Validation and Kotlin Coroutines
Hibernate Validator is not compatible with Kotlin Coroutine methods and fails when introspecting their method parameters. Please see spring-projects/spring-graphql#344 (comment) for links to relevant issues and a suggested workaround. |
6.3. @BatchMapping
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:
@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:
@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.
To be used as a unique key, |
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
.
6.3.1. Method Signature
Batch mapping methods support the following arguments:
Method Argument | Description |
---|---|
|
The source/parent objects. |
|
Obtained from Spring Security context, if available. |
|
For access to a value from the |
|
For access to the context from the |
|
The environment that is available in GraphQL Java to a
|
Batch mapping methods can return:
Return Type | Description |
---|---|
|
A map with parent objects as keys, and batch loaded objects as values. |
|
A sequence of batch loaded objects that must be in the same order as the source/parent objects passed into the method. |
|
Imperative variants, e.g. without remote calls to make. |
|
Imperative variants to be invoked asynchronously. For this to work,
|