1. Overview
Spring for GraphQL provides support for Spring applications built on GraphQL Java. It is a joint collaboration between the GraphQL Java team and Spring engineering. Our shared philosophy is to provide as little opinion as we can while focusing on comprehensive support for a wide range of use cases.
Spring for GraphQL is the successor of the GraphQL Java Spring project from the GraphQL Java team. It aims to be the foundation for all Spring, GraphQL applications.
The project reached version 1.0 in May 2022. Please, use our issue tracker to report a problem, discuss a design issue, or to request a feature.
To get started, check the Spring GraphQL starter on start.spring.io and the Samples in this repository.
2. Requirements
Spring for GraphQL requires the following as a baseline:
-
JDK8
-
Spring Framework 5.3
-
GraphQL Java 18
-
Spring Data 2021.1.0 or later for QueryDSL or Query by Example
3. Server Transports
Spring for GraphQL supports server handling of GraphQL requests over HTTP, WebSocket, and RSocket.
3.1. HTTP
GraphQlHttpHandler
handles GraphQL over HTTP requests and delegates to the
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 GraphQL request details included as JSON in the
request body, as defined in the proposed
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+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
Web Endpoints section for
details, or check GraphQlWebMvcAutoConfiguration
or GraphQlWebFluxAutoConfiguration
it contains, for the actual config.
The Spring for GraphQL repository contains a Spring MVC HTTP sample application.
3.2. WebSocket
GraphQlWebSocketHandler
handles GraphQL over WebSocket requests based on the
protocol defined in the
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 Interception chain for further
request execution.
GraphQL Over WebSocket Protocols
There are two such protocols, one in the subscriptions-transport-ws library and another in the graphql-ws library. The former is not active and succeeded by the latter. Read this 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
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
Web Endpoints
section for details, or check the GraphQlWebMvcAutoConfiguration
or the
GraphQlWebFluxAutoConfiguration
for the actual Boot starter config.
The Spring for GraphQL repository contains a WebFlux WebSocket sample application.
3.3. 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:
@Controller
public class GraphQlRSocketController {
private final GraphQlRSocketHandler handler;
GraphQlRSocketController(GraphQlRSocketHandler handler) {
this.handler = handler;
}
@MessageMapping("graphql")
public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
return this.handler.handle(payload);
}
@MessageMapping("graphql")
public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
return this.handler.handleSubscription(payload);
}
}
3.4. Interception
Server transports allow intercepting requests before and after the GraphQL Java engine is called to process a request.
3.4.1. WebGraphQlInterceptor
HTTP and 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
:
class HeaderInterceptor implements WebGraphQlInterceptor { (1)
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
String value = request.getHeaders().getFirst("myHeader");
request.configureExecutionInput((executionInput, builder) ->
builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
return chain.next(request);
}
}
@Controller
class MyController { (2)
@QueryMapping
Person person(@ContextValue String myHeader) {
// ...
}
}
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:
@Controller
class MyController {
@QueryMapping
Person person(GraphQLContext context) { (1)
context.put("cookieName", "123");
}
}
// Subsequent access from a WebGraphQlInterceptor
class HeaderInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { (2)
return chain.next(request).doOnNext(response -> {
String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString());
});
}
}
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
:
static class RequestErrorInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
return chain.next(request).map(response -> {
if (response.isValid()) {
return response; (1)
}
List<GraphQLError> errors = response.getErrors().stream() (2)
.map(error -> {
GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
// ...
return builder.build();
})
.collect(Collectors.toList());
return response.transform(builder -> builder.errors(errors).build()); (3)
});
}
}
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
Web Endpoints.
3.4.2. RSocketQlInterceptor
Similar to WebGraphQlInterceptor
, 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
.
4. Request Execution
ExecutionGraphQlService
is the main Spring abstraction to call GraphQL Java to execute
requests. Underlying transports, such as the Server Transports, delegate to
ExecutionGraphQlService
to handle requests.
The main implementation, DefaultExecutionGraphQlService
, is configured with a
GraphQlSource
for access to the graphql.GraphQL
instance to invoke.
4.1. GraphQLSource
GraphQlSource
is a core Spring abstraction for access to the
graphql.GraphQL
instance to use for request execution. It provides a builder API to
initialize GraphQL Java and build a GraphQlSource
.
The default GraphQlSource
builder, accessible via
GraphQlSource.schemaResourceBuilder()
, enables support for
Reactive DataFetcher
, Context Propagation, and Exception Resolution.
The Spring Boot starter initializes a
GraphQlSource
instance through the default GraphQlSource.Builder
and also enables
the following:
-
Load schema files from a configurable location.
-
Expose properties that apply to
GraphQlSource.Builder
. -
Detect
RuntimeWiringConfigurer
beans. -
Detect Instrumentation beans for GraphQL metrics.
-
Detect
DataFetcherExceptionResolver
beans for exception resolution. -
Detect
SubscriptionExceptionResolver
beans for subscription exception resolution.
For further customizations, you can declare your own GraphQlSourceBuilderCustomizer
beans;
for example, for configuring your own ExecutionIdProvider
:
@Configuration(proxyBeanMethods = false)
class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl(graphQlBuilder ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
4.1.1. 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 Spring Boot starter
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.
Use classpath*:graphql/**/ to find schema files across multiple classpath
locations, e.g. across multiple modules.
|
4.1.2. Schema Creation
By default, GraphQlSource.Builder
uses the GraphQL Java GraphQLSchemaGenerator
to
create the graphql.schema.GraphQLSchema
. This works for most applications, but if
necessary, you can hook into the schema creation through the builder:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
The primary reason for this is to create the schema through a federation library.
The GraphQlSource section explains how to configure that with Spring Boot.
4.1.3. RuntimeWiringConfigurer
You can use RuntimeWiringConfigurer
to register:
-
Custom scalar types.
-
Directives handling code.
-
TypeResolver
, if you need to override the DefaultTypeResolver
for a type. -
DataFetcher
for a field, although most applications will simply configureAnnotatedControllerConfigurer
, which detects annotated,DataFetcher
handler methods. The Spring Boot starter adds theAnnotatedControllerConfigurer
by default.
Unlike web frameworks, GraphQL does not use Jackson annotations to drive JSON serialization/deserialization. Custom data types and their serialization must be described as Scalars. |
The Spring 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:
@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.
4.1.4. Default 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 RuntimeWiringConfigurer
. 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:
GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);
The GraphQlSource section explains how to configure that with Spring Boot.
4.1.5. 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 provide more details on
query caching through a PreparsedDocumentProvider
.
In Spring GraphQL you can register a PreparsedDocumentProvider
through
GraphQlSource.Builder#configureGraphQl
:
.
// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...
// Create provider
PreparsedDocumentProvider provider = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
The GraphQlSource section explains how to configure that with Spring Boot.
4.1.6. 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
Schema Directives in the
GraphQL Java documentation.
In Spring GraphQL you can register a SchemaDirectiveWiring
through a
RuntimeWiringConfigurer
. The Spring Boot starter detects
such beans, so you might have something like:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
For an example of directives support check out the Extended Validation for Graphql Java library. |
4.2. 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
WebFlux Context.
4.3. Context Propagation
Spring for GraphQL provides support to transparently propagate context from the
Server Transports, 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.
4.3.1. 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
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:
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.
4.3.2. WebFlux
A Reactive DataFetcher
can rely on access to Reactor context that
originates from the WebFlux request handling chain. This includes Reactor context
added by WebGraphQlInterceptor components.
4.4. Exception Resolution
A GraphQL Java application can register a DataFetcherExceptionHandler
to decide how to
represent exceptions from the data layer in the "errors" section of the GraphQL response.
Spring for GraphQL has a built-in DataFetcherExceptionHandler
that is configured for use
by the default GraphQLSource
builder. It allows applications to register
one or more Spring DataFetcherExceptionResolver
components that are invoked sequentially
until one resolves the Exception
to a (possibly empty) list of graphql.GraphQLError
objects.
DataFetcherExceptionResolver
is an asynchronous contract. For most implementations, it
would be sufficient to extend DataFetcherExceptionResolverAdapter
and override
one of its resolveToSingleError
or resolveToMultipleErrors
methods that
resolve exceptions synchronously.
A GraphQLError
can be assigned to a category via graphql.ErrorClassification
.
In Spring GraphQL, you can also assign via ErrorType
which has the following common
classifications that applications can use to categorize errors:
-
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.
4.4.1. 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 WebGraphQlInterceptor
.
4.4.2. 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.
4.5. 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.
4.5.1. DataLoader
GraphQL Java provides a DataLoader
mechanism for batch loading of related entities.
You can find the full details in the
GraphQL Java docs. Below is a
summary of how it works:
-
Register
DataLoader
's in theDataLoaderRegistry
that can load entities, given unique keys. -
DataFetcher
's can accessDataLoader
's and use them to load entities by id. -
A
DataLoader
defers loading by returning a future so it can be done in a batch. -
DataLoader
's maintain a per request cache of loaded entities that can further improve efficiency.
4.5.2. 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:
@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 Spring 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
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 DataLoader
options.
For many cases, when loading related entities, you can use
@BatchMapping controller methods, which are a shortcut
for and replace the need to use BatchLoaderRegistry
and DataLoader
directly.
s
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 Context Propagation 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.
4.5.3. Testing Batch Loading
Start by having BatchLoaderRegistry
perform registrations on a DataLoaderRegistry
:
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:
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("...");
// ...
5. 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
.
5.1. Querydsl
Spring for GraphQL supports use of Querydsl to fetch data through the Spring Data 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
:
public interface AccountRepository extends Repository<Account, Long>,
QuerydslPredicateExecutor<Account> {
}
Then use it to create a DataFetcher
:
// For single result queries
DataFetcher<Account> dataFetcher =
QuerydslDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QuerydslDataFetcher.builder(repository).many();
You can now register the above DataFetcher
through a
RuntimeWiringConfigurer
.
The DataFetcher
builds a Querydsl Predicate
from GraphQL request parameters, and
uses it to fetch data. Spring Data supports QuerydslPredicateExecutor
for JPA,
MongoDB, and LDAP.
If the repository is ReactiveQuerydslPredicateExecutor
, the builder returns
DataFetcher<Mono<Account>>
or DataFetcher<Flux<Account>>
. Spring Data supports this
variant for MongoDB.
5.1.1. Build Setup
To configure Querydsl in your build, follow the official reference documentation:
For example:
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
}
<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 webmvc-http sample uses Querydsl for
artifactRepositories
.
5.1.2. 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 Auto-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.
To learn what projections are, please refer to the Spring Data docs. To understand how to use projections in GraphQL, please see Selection Set vs 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:
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();
5.1.3. 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 both single value and multi-value
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.
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 does not support customizations.
If you need that, you’ll need to use QueryByExampleDataFetcher
to build and
register the DataFetcher
manually through a
RuntimeWiringConfigurer
.
5.2. Query by Example
Spring Data supports the use of 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
:
public interface AccountRepository extends Repository<Account, Long>,
QueryByExampleExecutor<Account> {
}
Use QueryByExampleDataFetcher
to turn the repository into a DataFecher
:
// For single result queries
DataFetcher<Account> dataFetcher =
QueryByExampleDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).many();
You can now register the above DataFetcher
through a
RuntimeWiringConfigurer
.
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.
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.
5.2.1. 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.
5.2.2. Customizations
QueryByExampleDataFetcher
supports interface and DTO projections to transform query
results before returning these for further GraphQL processing.
To learn what projections are, please refer to the Spring Data documentation. To understand the role of projections in GraphQL, please see Selection Set vs 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:
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();
5.2.3. 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 both single value and multi-value
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.
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 does not support customizations.
If you need that, you’ll need to use QueryByExampleDataFetcher
to build and
register the DataFetcher
manually through a
RuntimeWiringConfigurer
.
5.3. Selection Set vs Projections
A common question that arises is, how GraphQL selection sets compare to 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 Querydsl and the Query by Example 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:
-
Closed interface projections are helpful if you cannot partially materialize the aggregate object, but you still want to expose a subset of properties.
-
Open interface projections leverage Spring’s
@Value
annotation and 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.
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,
|
7. Security
The path to a Web GraphQL endpoint can be secured with HTTP URL security to ensure that only authenticated users can access it. This does not, however, differentiate among different GraphQL requests on such a shared endpoint on a single URL.
To apply more fine-grained security, add Spring Security annotations such as
@PreAuthorize
or @Secured
to service methods involved in fetching specific parts of
the GraphQL response. This should work due to Context Propagation that aims to make
Security, and other context, available at the data fetching level.
The Spring for GraphQL repository contains samples for Spring MVC and for WebFlux.
8. Client
Spring for GraphQL includes client support for executing GraphQL requests over HTTP, WebSocket, and RSocket.
8.1. GraphQlClient
GraphQlClient
is a contract that declares a common workflow for GraphQL requests that is
independent of the underlying transport. That means requests are executed with the same API
no matter what the underlying transport, and anything transport specific is configured at
build time.
To create a GraphQlClient
you need one of the following extensions:
Each defines a Builder
with options relevant to the transport. All builders extend
from a common, base GraphQlClient Builder
with options
relevant to all extensions.
Once you have a GraphQlClient
you can begin to make requests.
8.1.1. HTTP
HttpGraphQlClient
uses
WebClient to execute
GraphQL requests over HTTP.
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
Once HttpGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing HttpGraphQlClient
to create a new instance with customized settings:
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Perform requests with graphQlClient...
HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Perform requests with anotherGraphQlClient...
8.1.2. WebSocket
WebSocketGraphQlClient
executes GraphQL requests over a shared WebSocket connection.
It is built using the
WebSocketClient
from Spring WebFlux and you can create it as follows:
String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();
In contrast to HttpGraphQlClient
, the WebSocketGraphQlClient
is connection oriented,
which means it needs to establish a connection before making any requests. As you begin
to make requests, the connection is established transparently. Alternatively, use the
client’s start()
method to establish the connection explicitly before any requests.
In addition to being connection-oriented, WebSocketGraphQlClient
is also multiplexed.
It maintains a single, shared connection for all requests. If the connection is lost,
it is re-established on the next request or if start()
is called again. You can also
use the client’s stop()
method which cancels in-progress requests, closes the
connection, and rejects new requests.
Use a single WebSocketGraphQlClient instance for each server in order to have a
single, shared connection for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
|
Once WebSocketGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing WebSocketGraphQlClient
to create a new instance with customized settings:
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use graphQlClient...
WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherGraphQlClient...
Interceptor
The GraphQL over WebSocket
protocol defines a number of connection oriented messages in addition to executing
requests. For example, a client sends "connection_init"
and the server responds with
"connection_ack"
at the start of a connection.
For WebSocket transport specific interception, you can create a
WebSocketGraphQlClientInterceptor
:
static class MyInterceptor implements WebSocketGraphQlClientInterceptor {
@Override
public Mono<Object> connectionInitPayload() {
// ... the "connection_init" payload to send
}
@Override
public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
// ... the "connection_ack" payload received
}
}
Register the above interceptor as any other
GraphQlClientInterceptor
and use it also to intercept GraphQL requests, but note there
can be at most one interceptor of type WebSocketGraphQlClientInterceptor
.
8.1.3. RSocket
RSocketGraphQlClient
uses
RSocketRequester
to execute GraphQL requests over RSocket requests.
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.clientTransport(transport)
.build();
In contrast to HttpGraphQlClient
, the RSocketGraphQlClient
is connection oriented,
which means it needs to establish a session before making any requests. As you begin
to make requests, the session is established transparently. Alternatively, use the
client’s start()
method to establish the session explicitly before any requests.
RSocketGraphQlClient
is also multiplexed. It maintains a single, shared session for
all requests. If the session is lost, it is re-established on the next request or if
start()
is called again. You can also use the client’s stop()
method which cancels
in-progress requests, closes the session, and rejects new requests.
Use a single RSocketGraphQlClient instance for each server in order to have a
single, shared session for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
|
Once RSocketGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport.
8.1.4. Builder
GraphQlClient
defines a parent Builder
with common configuration options for the
builders of all extensions. Currently, it has lets you configure:
-
DocumentSource
strategy to load the document for a request from a file -
Interception of executed requests
8.2. Requests
Once you have a GraphQlClient
, you can begin to perform requests via
retrieve() or execute()
where the former is only a shortcut for the latter.
8.2.1. Retrieve
The below retrieves and decodes the data for a query:
String document = "{" +
" project(slug:\"spring-framework\") {" +
" name" +
" releases {" +
" version" +
" }"+
" }" +
"}";
Mono<Project> projectMono = graphQlClient.document(document) (1)
.retrieve("project") (2)
.toEntity(Project.class); (3)
1 | The operation to perform. |
2 | The path under the "data" key in the response map to decode from. |
3 | Decode the data at the path to the target type. |
The input document is a String
that could be a literal or produced through a code
generated request object. You can also define documents in files and use a
Document Source to resole them by file name.
The path is relative to the "data" key and uses a simple dot (".") separated notation
for nested fields with optional array indices for list elements, e.g. "project.name"
or "project.releases[0].version"
.
Decoding can result in FieldAccessException
if the given path is not present, or the
field value is null
and has an error. FieldAccessException
provides access to the
response and the field:
Mono<Project> projectMono = graphQlClient.document(document)
.retrieve("project")
.toEntity(Project.class)
.onErrorResume(FieldAccessException.class, ex -> {
ClientGraphQlResponse response = ex.getResponse();
// ...
ResponseField field = ex.getField();
// ...
});
8.2.2. Execute
Retrieve is only a shortcut to decode from a single path in the
response map. For more control, use the execute
method and handle the response:
For example:
Mono<Project> projectMono = graphQlClient.document(document)
.execute()
.map(response -> {
if (!response.isValid()) {
// Request failure... (1)
}
ResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure... (2)
}
else {
// Optional field set to null... (3)
}
}
return field.toEntity(Project.class); (4)
});
1 | The response does not have data, only errors |
2 | Field that is null and has an associated error |
3 | Field that was set to null by its DataFetcher |
4 | Decode the data at the given path |
8.2.3. Document Source
The document for a request is a String
that may be defined in a local variable or
constant, or it may be produced through a code generated request object.
You can also create document files with extensions .graphql
or .gql
under
"graphql-documents/"
on the classpath and refer to them by file name.
For example, given a file called projectReleases.graphql
in
src/main/resources/graphql-documents
, with content:
query projectReleases($slug: ID!) {
project(slug: $slug) {
name
releases {
version
}
}
}
You can then:
Mono<Project> projectMono = graphQlClient.documentName("projectReleases") (1)
.variable("slug", "spring-framework") (2)
.retrieve()
.toEntity(Project.class);
1 | Load the document from "projectReleases.graphql" |
2 | Provide variable values. |
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
You can use the GraphQlClient
Builder to customize the
DocumentSource
for loading documents by names.
8.3. Subscription Requests
GraphQlClient
can execute subscriptions over transports that support it. Currently, only
the WebSocket transport supports GraphQL streams, so you’ll need to create a
WebSocketGraphQlClient.
8.3.1. Retrieve
To start a subscription stream, use retrieveSubscription
which is similar to
retrieve for a single response but returning a stream of
responses, each decoded to some data:
Flux<String> greetingFlux = client.document("subscription { greetings }")
.retrieveSubscription("greeting")
.toEntity(String.class);
A subscription stream may end with:
-
SubscriptionErrorException
if the server ends the subscription with an explicit "error" message that contains one or more GraphQL errors. The exception provides access to the GraphQL errors decoded from that message. -
GraphQlTransportException
such asWebSocketDisconnectedException
if the underlying connection is closed or lost in which case you can use theretry
operator to reestablish the connection and start the subscription again.
8.3.2. Execute
Retrieve is only a shortcut to decode from a single path in each
response map. For more control, use the executeSubscription
method and handle each
response directly:
Flux<String> greetingFlux = client.document("subscription { greetings }")
.executeSubscription()
.map(response -> {
if (!response.isValid()) {
// Request failure...
}
ResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure...
}
else {
// Optional field set to null... (3)
}
}
return field.toEntity(String.class)
});
8.4. Interception
You create a GraphQlClientInterceptor
to intercept all requests through a client:
static class MyInterceptor implements GraphQlClientInterceptor {
@Override
public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
// ...
return chain.next(request);
}
@Override
public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
// ...
return chain.next(request);
}
}
Once the interceptor is created, register it through the client builder:
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.interceptor(new MyInterceptor())
.build();
9. Testing
Spring for GraphQL provides dedicated support for testing GraphQL requests over HTTP, WebSocket, and RSocket, as well as for testing directly against a server.
To make use of this, add spring-graphql-test
to your build:
dependencies {
// ...
testImplementation 'org.springframework.graphql:spring-graphql-test:1.1.0-RC1'
}
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<version>1.1.0-RC1</version>
<scope>test</scope>
</dependency>
</dependencies>
9.1. GraphQlTester
GraphQlTester
is a contract that declares a common workflow for testing GraphQL
requests that is independent of the underlying transport. That means requests are tested
with the same API no matter what the underlying transport, and anything transport
specific is configured at build time.
To create a GraphQlTester
that performs requests through a client, you need one of the
following extensions:
To create a GraphQlTester
that performs tests on the server side, without a client:
Each defines a Builder
with options relevant to the transport. All builders extend
from a common, base GraphQlTester Builder
with
options relevant to all extensions.
9.1.1. HTTP
HttpGraphQlTester
uses
WebTestClient to execute
GraphQL requests over HTTP, with or without a live server, depending on how
WebTestClient
is configured.
To test in Spring WebFlux, without a live server, point to your Spring configuration that declares the GraphQL HTTP endpoint:
ApplicationContext context = ... ;
WebTestClient client =
WebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
To test in Spring MVC, without a live server, do the same using MockMvcWebTestClient
:
ApplicationContext context = ... ;
WebTestClient client =
MockMvcWebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
Or to test against a live server running on a port:
WebTestClient client =
WebTestClient.bindToServer()
.baseUrl("http://localhost:8080/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
Once HttpGraphQlTester
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing HttpSocketGraphQlTester
to create a new instance with customized settings:
HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use tester...
HttpGraphQlTester anotherTester = tester.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherTester...
9.1.2. WebSocket
WebSocketGraphQlTester
executes GraphQL requests over a shared WebSocket connection.
It is built using the
WebSocketClient
from Spring WebFlux and you can create it as follows:
String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();
WebSocketGraphQlTester
is connection oriented and multiplexed. Each instance establishes
its own single, shared connection for all requests. Typically, you’ll want to use a single
instance only per server.
Once WebSocketGraphQlTester
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing WebSocketGraphQlTester
to create a new instance with customized settings:
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use tester...
WebSocketGraphQlTester anotherTester = tester.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherTester...
WebSocketGraphQlTester
provides a stop()
method that you can use to have the WebSocket
connection closed, e.g. after a test runs.
9.1.3. RSocket
RSocketGraphQlTester
uses RSocketRequester
from spring-messaging to execute GraphQL
requests over RSocket:
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlTester client = RSocketGraphQlTester.builder()
.clientTransport(transport)
.build();
RSocketGraphQlTester
is connection oriented and multiplexed. Each instance establishes
its own single, shared session for all requests. Typically, you’ll want to use a single
instance only per server. You can use the stop()
method on the tester to close the
session explicitly.
Once RSocketGraphQlTester
is created, you can begin to
execute requests using the same API, independent of the underlying
transport.
9.1.4. GraphQlService
Many times it’s enough to test GraphQL requests on the server side, without the use of a
client to send requests over a transport protocol. To test directly against a
ExecutionGraphQlService
, use the ExecutionGraphQlServiceTester
extension:
GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);
Once ExecutionGraphQlServiceTester
is created, you can begin to
execute requests using the same API, independent of the underlying
transport.
9.1.5. WebGraphQlHandler
The GraphQlService
extension lets you test on the server side, without
a client. However, in some cases it’s useful to involve server side transport
handling with given mock transport input.
The WebGraphQlTester
extension lets you processes request through the
WebGraphQlInterceptor
chain before handing off to ExecutionGraphQlService
for
request execution:
WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.create(handler);
The builder for this extension allows you to define HTTP request details:
WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.builder(handler)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
Once WebGraphQlServiceTester
is created, you can begin to
execute requests using the same API, independent of the underlying
transport.
9.1.6. Builder
GraphQlTester
defines a parent Builder
with common configuration options for the
builders of all extensions. It lets you configure the following:
-
errorFilter
- a predicate to suppress expected errors, so you can inspect the data of the response. -
documentSource
- a strategy for loading the document for a request from a file on the classpath or from anywhere else. -
responseTimeout
- how long to wait for request execution to complete before timing out.
9.2. Requests
Once you have a GraphQlTester
, you can begin to test requests. The below executes a
query for a project and uses JsonPath to extract
project release versions from the response:
String document = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }"+
" }" +
"}";
graphQlTester.document(document)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
The JsonPath is relative to the "data" section of the response.
You can also create document files with extensions .graphql
or .gql
under
"graphql-test/"
on the classpath and refer to them by file name.
For example, given a file called projectReleases.graphql
in
src/main/resources/graphql-test
, with content:
query projectReleases($slug: ID!) {
project(slug: $slug) {
releases {
version
}
}
}
You can then use:
graphQlTester.documentName("projectReleases") (1)
.variable("slug", "spring-framework") (2)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
1 | Refer to the document in the file named "project". |
2 | Set the slug variable. |
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion. |
If a request does not have any response data, e.g. mutation, use executeAndVerify
instead of execute
to verify there are no errors in the response:
graphQlTester.query(query).executeAndVerify();
See Errors for more details on error handling.
9.3. Subscriptions
To test subscriptions, call executeSubscription
instead of execute
to obtain a stream
of responses and then use StepVerifier
from Project Reactor to inspect the stream:
Flux<String> greetingFlux = tester.document("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class); // decode at JSONPath
StepVerifier.create(greetingFlux)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
Subscriptions are supported only with WebSocketGraphQlTester, or with the server side
GraphQlService
and WebGraphQlHandler
extensions.
9.4. Errors
When you use verify()
, any errors under the "errors" key in the response will cause
an assertion failure. To suppress a specific error, use the error filter before
verify()
:
graphQlTester.query(query)
.execute()
.errors()
.filter(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
You can register an error filter at the builder level, to apply to all tests:
WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
.errorFilter(error -> ...)
.build();
If you want to verify that an error does exist, and in contrast to filter
, throw an
assertion error if it doesn’t, then use exepect
instead:
graphQlTester.query(query)
.execute()
.errors()
.expect(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
You can also inspect all errors through a Consumer
, and doing so also marks them as
filtered, so you can then also inspect the data in the response:
graphQlTester.query(query)
.execute()
.errors()
.satisfy(errors -> {
// ...
});
10. Samples
This Spring for GraphQL repository contains sample applications for various scenarios.
You can run those by cloning this repository and running main application classes from your IDE or by typing the following on the command line:
$ ./gradlew :samples:{sample-directory-name}:bootRun