This version is still in development and is not considered stable yet. For the latest stable version, please use Spring GraphQL 1.4.2! |
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
.
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();
// For paginated queries
DataFetcher<Iterable<Account>> dataFetcher =
QuerydslDataFetcher.builder(repository).scrollable();
You can now register the above DataFetcher
through a
RuntimeWiringConfigurer
.
The DataFetcher
builds a Querydsl Predicate
from GraphQL arguments, and uses it to
fetch data. Spring Data supports QuerydslPredicateExecutor
for JPA, MongoDB, Neo4j, and LDAP.
For a single argument that is a GraphQL input type, QuerydslDataFetcher nests one
level down, and uses the values from the argument sub-map.
|
If the repository is ReactiveQuerydslPredicateExecutor
, the builder returns
DataFetcher<Mono<Account>>
or DataFetcher<Flux<Account>>
. Spring Data supports this
variant for MongoDB and Neo4j.
Build Setup
To configure Querydsl in your build, follow the official reference documentation:
For example:
-
Gradle
-
Maven
dependencies {
//...
annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jakarta",
'jakarta.persistence:jakarta.persistence-api'
}
compileJava {
options.annotationProcessorPath = configurations.annotationProcessor
}
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<!-- Explicit opt-in required via annotationProcessors or
annotationProcessorPaths on Java 22+, see https://bugs.openjdk.org/browse/JDK-8306819 -->
<annotationProcessorPath>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jakarta</classifier>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</annotationProcessorPath>
</annotationProcessorPaths>
<!-- Recommended: Some IDE's might require this configuration to include generated sources for IDE usage -->
<generatedTestSourcesDirectory>target/generated-test-sources</generatedTestSourcesDirectory>
<generatedSourcesDirectory>target/generated-sources</generatedSourcesDirectory>
</configuration>
</plugin>
</plugins>
</build>
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();
Auto-Registration
If a repository is annotated with @GraphQlRepository
, it is automatically registered
for queries that do not already have a registered DataFetcher
and whose return type
matches that of the repository domain type. This includes single value queries, multi-value
queries, and paginated queries.
By default, the name of the GraphQL type returned by the query must match the simple name
of the repository domain type. If needed, you can use the typeName
attribute of
@GraphQlRepository
to specify the target GraphQL type name.
For paginated queries, the simple name of the repository domain type must match the
Connection
type name without the Connection
ending (e.g. Book
matches
BooksConnection
). For auto-registration, pagination is offset-based with 20 items
per page.
Auto-registration detects if a given repository implements QuerydslBinderCustomizer
and
transparently applies that through QuerydslDataFetcher
builder methods.
Auto-registration is performed through a built-in RuntimeWiringConfigurer
that can be
obtained from QuerydslDataFetcher
. The Boot Starter automatically
detects @GraphQlRepository
beans and uses them to initialize the
RuntimeWiringConfigurer
with.
Auto-registration applies customizations
by calling customize(Builder)
on the repository instance if your repository
implements QuerydslBuilderCustomizer
or ReactiveQuerydslBuilderCustomizer
respectively.
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 DataFetcher
:
// For single result queries
DataFetcher<Account> dataFetcher =
QueryByExampleDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).many();
// For paginated queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).scrollable();
You can now register the above DataFetcher
through a
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.
For a single argument that is a GraphQL input type, QueryByExampleDataFetcher
nests one level down, and binds with the values from the argument sub-map.
|
If the repository is ReactiveQueryByExampleExecutor
, the builder returns
DataFetcher<Mono<Account>>
or DataFetcher<Flux<Account>>
. Spring Data supports this
variant for MongoDB, Neo4j, Redis, and R2dbc.
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.
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();
Auto-Registration
If a repository is annotated with @GraphQlRepository
, it is automatically registered
for queries that do not already have a registered DataFetcher
and whose return type
matches that of the repository domain type. This includes single value queries, multi-value
queries, and paginated queries.
By default, the name of the GraphQL type returned by the query must match the simple name
of the repository domain type. If needed, you can use the typeName
attribute of
@GraphQlRepository
to specify the target GraphQL type name.
For paginated queries, the simple name of the repository domain type must match the
Connection
type name without the Connection
ending (e.g. Book
matches
BooksConnection
). For auto-registration, pagination is offset-based with 20 items
per page.
Auto-registration is performed through a built-in RuntimeWiringConfigurer
that can be
obtained from QueryByExampleDataFetcher
. The Boot Starter automatically
detects @GraphQlRepository
beans and uses them to initialize the
RuntimeWiringConfigurer
with.
Auto-registration applies customizations
by calling customize(Builder)
on the repository instance if your repository
implements QueryByExampleBuilderCustomizer
or
ReactiveQueryByExampleBuilderCustomizer
respectively.
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.
Scroll
As explained in Pagination, the GraphQL Cursor Connection spec defines a
mechanism for pagination with Connection
, Edge
, and PageInfo
schema types, while
GraphQL Java provides the equivalent Java type representations.
Spring for GraphQL provides built-in ConnectionAdapter
implementations to adapt the
Spring Data pagination types Window
and Slice
transparently. You can configure that
as follows:
CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder(
new ScrollPositionCursorStrategy(),
CursorEncoder.base64()); (1)
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
new WindowConnectionAdapter(strategy),
new SliceConnectionAdapter(strategy))); (2)
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)); (3)
1 | Create strategy to convert ScrollPosition to a Base64 encoded cursor. |
2 | Create type visitor to adapt Window and Slice returned from DataFetcher s. |
3 | Register the type visitor. |
On the request side, a controller method can declare a
ScrollSubrange method argument to paginate forward
or backward. For this to work, you must declare a CursorStrategy
supports ScrollPosition
as a bean.
The Boot Starter declares a CursorStrategy<ScrollPosition>
bean, and registers the
ConnectionFieldTypeVisitor
as shown above if Spring Data is on the classpath.
Keyset Position
For KeysetScrollPosition
, the cursor needs to be created from a keyset, which is
essentially a Map
of key-value pairs. To decide how to create a cursor from a keyset,
you can configure ScrollPositionCursorStrategy
with CursorStrategy<Map<String, Object>>
.
By default, JsonKeysetCursorStrategy
writes the keyset Map
to JSON. That works for
simple like String, Boolean, Integer, and Double, but others cannot be restored back to the
same type without target type information. The Jackson library has a default typing feature
that can include type information in the JSON. To use it safely you must specify a list of
allowed types. For example:
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.allowIfSubType(ZonedDateTime.class)
.build();
JsonMapper mapper = JsonMapper.builder()
.activateDefaultTyping(validator, DefaultTyping.NON_FINAL)
.enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
You can then create JsonKeysetCursorStrategy
:
ObjectMapper mapper = ... ;
CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper));
configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper));
JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer);
By default, if JsonKeysetCursorStrategy
is created without a CodecConfigurer
and the
Jackson library is on the classpath, customizations like the above are applied for
Date
, Calendar
, UUID
and any type from java.time
.
Sort
Spring for GraphQL defines a SortStrategy
to create Sort
from GraphQL arguments.
AbstractSortStrategy
implements the contract with abstract methods to extract the sort
direction and properties. To enable support for Sort
as a controller method argument,
you need to declare a SortStrategy
bean.
Transaction Management
At some point when working with data atomicity and isolation of operations start to matter. These are both properties of transactions. GraphQL itself does not define any transaction semantics, so it is up to the server and your application to decide how to handle transactions.
GraphQL and specifically GraphQL Java are designed to be non-opinionated about how data is fetched. A core property of GraphQL is that clients drive the request; Fields can be resolved independently of their original source to allow for composition. A reduced fieldset can require less data to be fetched resulting in better performance.
Applying the concept of distributed field resolution within transactions is not a good fit:
-
Transactions keep a unit of work together resulting typically in fetching the entire object graph (like a typical object-relational mapper would behave) within a single transaction. This is at odds with GraphQL’s core design to let the client drive queries.
-
Keeping a transaction open across multiple data fetchers of which each one would fetch only its flat object mitigates the performance aspect and aligns with decoupled field resolution, but it can lead to long-running transactions that hold on to resources for longer than necessary.
Generally speaking, transactions are best applied to mutations that change state and not necessarily to queries that just read data. However, there are use cases where transactional reads are required.
GraphQL is designed to support multiple mutations within a single request. Depending on the use case, you might want to:
-
Run each mutation within its own transaction.
-
Keep some mutations within a single transaction to ensure a consistent state.
-
Span a single transaction over all involved mutations.
Each approach requires a slightly different transaction management strategy.
When using Spring Framework (e.g. JDBC) or Spring Data, the Template API and repositories default (without any further instrumentation) to use implicit transactions for individual operations resulting in starting and commiting a transaction for each repository method call. This is the normal mode of operation for most databases.
The following sections are outlining two different strategies to manage transactions in a GraphQL server:
Transactional Controller Methods
The simplest approach to manage transactions is to use Spring’s Transaction Management
together with @MutationMapping
controller methods (or any other @SchemaMapping
method)
for example:
-
Declarative
-
Programmatic
@Controller
public class AccountController {
@MutationMapping
@Transactional
public Account addAccount(@Argument AccountInput input) {
// ...
}
}
@Controller
public class AccountController {
private final TransactionOperations transactionOperations;
@MutationMapping
public Account addAccount(@Argument AccountInput input) {
return transactionOperations.execute(status -> {
// ...
});
}
}
A transaction spans from entering the addAccount
method until its return.
All invocations to transactional resources are part of the same transaction resulting in
atomicity and isolation of the mutation.
This is the recommended approach. It gives you full control over transaction boundaries with a clearly defined entrypoint without the need to instrument GraphQL server infrastructure.
Cleaning up a transaction after the method call results that subsequent data fetching
(e.g. for nested fields) is not part of the transactional method addAccount
as
outlined below:
@Controller
public class AccountController {
@MutationMapping
@Transactional
public Account addAccount(@Argument AccountInput input) { (1)
// ...
}
@SchemaMapping
@Transactional
public Person person(Account account) { (2)
... // fetching the person within a separate transaction
}
}
1 | The addAccount method invocation runs within its own transaction. |
2 | The person method invocation creates its own, separate transaction that is not
tied to the addAccount method in case both methods were invoked as part of the same
GraphQL request. A separate transaction comes with all possible drawbacks of not
being part of the same transaction, such as non-repeatable reads or inconsistencies
in case the data has been modified between the addAcount and person method invocations. |
To run multiple mutations in a single transaction maintaining a simple setup we recommend designing a mutation method that accepts all required inputs. This method can then call multiple service methods, ensuring they all participate in the same transaction.
Transactional Instrumentation
Applying a Transactional Instrumentation is a more advanced approach to span a transaction over the entire execution of a GraphQL request. By stating a transaction before the first data fetcher is invoked your application can ensure that all data fetchers can participate in the same transaction.
When instrumenting the server, you need to ensure an ExecutionStrategy
runs
DataFetcher
invocations serially so that all invocations are executed on the same
Thread
. This is mandatory: Synchronous transaction management uses ThreadLocal
state
to allow participation in transactions. Considering AsyncSerialExecutionStrategy
as
starting point is a good choice as it executes data fetchers serially.
You have two general options to implement transactional instrumentation:
-
GraphQL Java’s
Instrumentation
contract allows to hook into the execution lifecycle at various stages. The Instrumentation SPI was designed with observability in mind, yet it serves as execution-agnostic extension points regardless of whether you’re using synchronous reactive, or any other asynchronous form to invoke data fetchers and is less opinionated in that regard. -
An
ExecutionStrategy
provides full control over the execution and opens a variety of possibilities how to communicate failed transactions or errors during transaction cleanup back to the client. It can also serve as good entry point to implement custom directives that allow clients specifying transactional attributes through directives or using directives in your schema to demarcate transactional boundaries for certain queries or mutations.
When manually managing transactions, ensure to clean up the transaction, that is either
commiting or rolling back, after completing the unit of work.
ExceptionWhileDataFetching
can be a useful GraphQLError
to obtain an underlying
Exception
. This error is constructed when using SimpleDataFetcherExceptionHandler
.
By default, Spring GraphQL falls back to an internal GraphQLError
that doesn’t expose
the original exception.
Applying transactional instrumentation creates opportunities to rethink transaction
participation: All @SchemaMapping
controller methods participate in the transaction
regardless whether they are invoked for the root, nested fields, or as part of a mutation.
Transactional controller methods (or service methods within the invocation chain) can
declare transactional attributes such as propagation behavior REQUIRES_NEW
to start
a new transaction if required.