489 lines
19 KiB
Plaintext
489 lines
19 KiB
Plaintext
[[data]]
|
|
= 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`.
|
|
|
|
|
|
|
|
[[data.querydsl]]
|
|
== Querydsl
|
|
|
|
Spring for GraphQL supports use of http://www.querydsl.com/[Querydsl] to fetch data through
|
|
the Spring Data
|
|
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions[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`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
public interface AccountRepository extends Repository<Account, Long>,
|
|
QuerydslPredicateExecutor<Account> {
|
|
}
|
|
----
|
|
|
|
Then use it to create a `DataFetcher`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
// 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
|
|
xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[`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.
|
|
|
|
NOTE: 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.
|
|
|
|
|
|
[[data.querydsl.build]]
|
|
=== Build Setup
|
|
|
|
To configure Querydsl in your build, follow the
|
|
https://querydsl.com/static/querydsl/latest/reference/html/ch02.html[official reference documentation]:
|
|
|
|
For example:
|
|
|
|
[tabs]
|
|
======
|
|
Gradle::
|
|
+
|
|
[source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"]
|
|
----
|
|
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
|
|
}
|
|
----
|
|
|
|
Maven::
|
|
+
|
|
[source,xml,indent=0,subs="verbatim,quotes,attributes",role="secondary"]
|
|
----
|
|
<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 {github-10x-branch}/samples/webmvc-http[webmvc-http] sample uses Querydsl for
|
|
`artifactRepositories`.
|
|
|
|
|
|
[[data.querydsl.customizations]]
|
|
=== 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 xref:data.adoc#data.querydsl.registration[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.
|
|
|
|
TIP: To learn what projections are, please refer to the
|
|
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data docs].
|
|
To understand how to use projections in GraphQL, please see xref:data.adoc#data.projections[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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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();
|
|
----
|
|
|
|
|
|
|
|
[[data.querydsl.registration]]
|
|
=== 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 xref:request-execution.adoc#execution.pagination[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
|
|
`**Books**Connection`). 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 xref:boot-starter.adoc[Boot Starter] automatically
|
|
detects `@GraphQlRepository` beans and uses them to initialize the
|
|
`RuntimeWiringConfigurer` with.
|
|
|
|
Auto-registration applies xref:data.adoc#data.querybyexample.customizations[customizations]
|
|
by calling `customize(Builder)` on the repository instance if your repository
|
|
implements `QuerydslBuilderCustomizer` or `ReactiveQuerydslBuilderCustomizer`
|
|
respectively.
|
|
|
|
|
|
[[data.querybyexample]]
|
|
== Query by Example
|
|
|
|
Spring Data supports the use of
|
|
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example[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`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
public interface AccountRepository extends Repository<Account, Long>,
|
|
QueryByExampleExecutor<Account> {
|
|
}
|
|
----
|
|
|
|
Use `QueryByExampleDataFetcher` to turn the repository into a `DataFetcher`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
// 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
|
|
xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[`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.
|
|
|
|
NOTE: 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.
|
|
|
|
|
|
[[data.querybyexample.build]]
|
|
=== 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.
|
|
|
|
|
|
[[data.querybyexample.customizations]]
|
|
=== Customizations
|
|
|
|
`QueryByExampleDataFetcher` supports interface and DTO projections to transform query
|
|
results before returning these for further GraphQL processing.
|
|
|
|
TIP: To learn what projections are, please refer to the
|
|
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data documentation].
|
|
To understand the role of projections in GraphQL, please see xref:data.adoc#data.projections[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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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();
|
|
----
|
|
|
|
|
|
[[data.querybyexample.registration]]
|
|
=== 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 xref:request-execution.adoc#execution.pagination[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
|
|
`**Books**Connection`). 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 xref:boot-starter.adoc[Boot Starter] automatically
|
|
detects `@GraphQlRepository` beans and uses them to initialize the
|
|
`RuntimeWiringConfigurer` with.
|
|
|
|
Auto-registration applies xref:data.adoc#data.querybyexample.customizations[customizations]
|
|
by calling `customize(Builder)` on the repository instance if your repository
|
|
implements `QueryByExampleBuilderCustomizer` or
|
|
`ReactiveQueryByExampleBuilderCustomizer` respectively.
|
|
|
|
|
|
|
|
[[data.projections]]
|
|
== Selection Set vs Projections
|
|
|
|
A common question that arises is, how GraphQL selection sets compare to
|
|
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[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
|
|
xref:data.adoc#data.querydsl[Querydsl] and the xref:data.adoc#data.querybyexample[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:
|
|
|
|
- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.closed[Closed interface projections]
|
|
are helpful if you cannot partially materialize the aggregate object, but you still
|
|
want to expose a subset of properties.
|
|
- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.open[Open interface projections]
|
|
leverage Spring's `@Value` annotation and
|
|
{spring-framework-ref-docs}/core/expressions.html[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.
|
|
|
|
|
|
|
|
[[data.pagination.scroll]]
|
|
== Scroll
|
|
|
|
As explained in xref:request-execution.adoc#execution.pagination[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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
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
|
|
xref:controllers.adoc#controllers.schema-mapping.subrange[ScrollSubrange] method argument to paginate forward
|
|
or backward. For this to work, you must declare a xref:request-execution.adoc#execution.pagination.cursor.strategy[`CursorStrategy`]
|
|
supports `ScrollPosition` as a bean.
|
|
|
|
The xref:boot-starter.adoc[Boot Starter] declares a `CursorStrategy<ScrollPosition>` bean, and registers the
|
|
`ConnectionFieldTypeVisitor` as shown above if Spring Data is on the classpath.
|
|
|
|
|
|
[[data.pagination.scroll.keyset]]
|
|
== 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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
|
|
.allowIfBaseType(Map.class)
|
|
.allowIfSubType(ZonedDateTime.class)
|
|
.build();
|
|
|
|
ObjectMapper mapper = new ObjectMapper();
|
|
mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL);
|
|
----
|
|
|
|
You can then create `JsonKeysetCursorStrategy`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
ObjectMapper mapper = ... ;
|
|
|
|
CodecConfigurer configurer = ServerCodecConfigurer.create();
|
|
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
|
|
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(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`, and any type from `java.time`.
|
|
|
|
|
|
|
|
[[data.pagination.sort]]
|
|
== 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.
|