In the last article Getting started with Spring Boot GraphQL service, we discussed features of GraphQL and its implementation in Spring for GraphQL using low-level APIs of GraphQL Java.
While low-level GraphQL Java implementation is fine for understanding the concept, you’ll mostly use higher-level abstraction provided by Spring.
In this article, we will discuss the @Controller
, @QueryMapping
, and @SchemaMapping
of Spring for GraphQL.
Here, we’ll use the same book catalog GraphQL API, described in an earlier article (also shown below).
In this article
type Query {
books: [Book]
bookById(id : ID) : Book
}
type Book {
id : ID
name : String
author: String
price: Float
ratings: [Rating]
}
type Rating {
id: ID
rating: Int
comment: String
user: String
}
Let’s get started!
Controller
Like REST API, Spring Boot offers @Controller
annotation to implement GraphQL APIs. All you need to do is to annotate your controller class with @Controller
annotation and define either @QueryMapping
or @SchemaMapping
annotation on the handler method.
Spring detects @Controller
beans and registers their annotated handler methods ( @QueryMapping
or @SchemaMapping
) as DataFetcher
s (also known as a resolver).
A resolver/DataFetcher is a function that’s responsible for populating the data for a single field in your GraphQL schema.
SchemaMapping Annotation
The @SchemaMapping
annotation maps a handler method to a field in the GraphQL schema as DataFetcher
. All you need to do is to define the typeName
and, optionally, the field
with @SchemaMapping
.
@SchemaMapping(typeName = "Query", field = "books")
public Collection<Book> books() {
return bookCatalogService.getBooks();
}
Additionally, if you keep the method name as same as the field
name then you can omit the field
from @SchemaMapping
.
@SchemaMapping(typeName = "Query")
public Collection<Book> books() {
return bookCatalogService.getBooks();
}
What happens if you don’t specify the
typeName
with@SchemaMapping
(as shown below) ?
@SchemaMapping()
public Collection<Book> books() {
return bookCatalogService.getBooks();
}
In this case, Spring can’t figure out which parent-type field books
belong to; therefore, it’ll throw BeanCreationException
, at the application startup, with a helpful error message.
No parentType specified, and a source/parent method argument was also not found: BooksCatalogController#books[0 args]
Similarly, for the field bookById
, you can define a data fetcher as:
@SchemaMapping(typeName = "Query")
public Book bookById(@Argument("id") Integer id) {
return bookCatalogService.bookById(id);
}
Here, we have used @Argument
annotation to map argument id as @Argument("id") Integer id
.
Finally, you can test this by heading over to http://localhost:8080/graphiql?path=/graphql in your browser.
Data Fetcher for the child ‘Type’ field
If you query GraphQL service with the ratings
field of Book
the response includes "ratings": null
?
Why?
If you don’t specify a Data Fetcher for a field the GraphQL Java assigns a default PropertyDataFetcher
. Subsequently, at the runtime this PropertyDataFetcher
looks for public getRatings()
field in the Book
POJO (as field ratings
belongs to the Book
).
To solve this, you can define a Data Fetcher for ratings
. As the field ratings
belongs to parent type Book
, you need to pass Book
in the argument.
@SchemaMapping(typeName = "Book", field = "ratings")
public List<Rating> ratings(Book book) {
return bookCatalogService.ratings(book);
}
While GraphQL Java is executing a query, it calls the appropriate
DataFetcher
for each field it encounters in the query. If you don’t define anyDataFetcher
it assignsPropertyDataFetcher
as default.
And, you can write without specifying the field
as:
@SchemaMapping(typeName = "Book")
public List<Rating> ratings(Book book) {
return bookCatalogService.ratings(book);
}
Also as:
@SchemaMapping
public List<Rating> ratings(Book book) {
return bookCatalogService.ratings(book);
}
In @SchemaMapping
you can also leave out typeName
and field
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.
QueryMapping annotation
Instead of using @SchemaMapping
, you can use @QueryMapping
. Effectively, this is a shortcut annotation to bind annotated methods for fields under the Query
type.
@QueryMapping(name = "books")
public Collection<Book> books() {
return bookCatalogService.getBooks();
}
Moreover, you can skip the name
from annotation. In that case, Spring derives the query field from the method name.
@QueryMapping
public Collection<Book> books() {
return bookCatalogService.getBooks();
}
Similarly, for the Query field bookById
:
@QueryMapping
public Book bookById(@Argument Integer id) {
return bookCatalogService.bookById(id);
}
You can’t use @QueryMapping
for the field ratings
as it doesn’t belong to the type Query
.
Configuration
By default, the Boot starter checks in src/main/resources/graphql
for GraphQL schema files with extensions “.graphqls
” or “.gqls
“. To override this behavior, you can change the following property.
spring.graphql.schema.locations=classpath:graphql/
spring.graphql.schema.fileExtensions=.graphqls, .gqls
Code example
The working code example of this article is listed on GitHub . To run the example, clone the repository, and import graphql-spring-annotation as a project in your favorite IDE as a Gradle
project.
Summary
Spring greatly simplifies the development of GraphQL APIs by the way of @Controller
annotation. You just need to annotate your Controller
classes with @Controller
annotation and define either @QueryMapping
or @SchemaMapping
annotation on the handler method.
Spring detects @Controller
beans and registers their annotated handler methods as DataFetcher
s (also known as a resolver).
Discussion about this post