数据集成
Spring for GraphQL 让你能够利用现有的 Spring 技术,遵循通用的编程模型,通过 GraphQL 暴露底层数据源。
本节讨论 Spring Data 的集成层,它提供了一种简单的方式来适配 Querydsl 或 Query by Example 存储库到 DataFetcher
,包括自动检测并注册标记有 @GraphQlRepository
注解的存储库中的 GraphQL 查询的选项。
Querydsl
Spring for GraphQL 支持使用 Querydsl 通过 Spring Data Querydsl 扩展 来获取数据。Querydsl 通过使用注解处理器生成元模型,提供了一种灵活且类型安全的查询谓词表达方式。
例如,将一个存储库声明为 QuerydslPredicateExecutor
public interface AccountRepository extends Repository<Account, Long>,
QuerydslPredicateExecutor<Account> {
}
然后使用它来创建一个 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();
现在你可以通过 RuntimeWiringConfigurer
注册上述 DataFetcher
。
该 DataFetcher
从 GraphQL 参数构建一个 Querydsl Predicate
,并使用它来获取数据。Spring Data 支持 JPA、MongoDB、Neo4j 和 LDAP 的 QuerydslPredicateExecutor
。
对于作为 GraphQL 输入类型的单个参数,QuerydslDataFetcher 会嵌套一层,并使用参数子映射中的值。 |
如果存储库是 ReactiveQuerydslPredicateExecutor
,构建器将返回 DataFetcher<Mono<Account>>
或 DataFetcher<Flux<Account>>
。Spring Data 支持 MongoDB 和 Neo4j 的这种变体。
构建配置
要在构建中配置 Querydsl,请遵循官方参考文档
例如
-
Gradle
-
Maven
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>
webmvc-http 示例使用 Querydsl 来处理 artifactRepositories
。
自定义
QuerydslDataFetcher
支持自定义 GraphQL 参数如何绑定到属性以创建 Querydsl Predicate
。默认情况下,每个可用属性的参数都绑定为“等于”。要自定义此行为,你可以使用 QuerydslDataFetcher
构建器方法来提供一个 QuerydslBinderCustomizer
。
存储库本身可以是 QuerydslBinderCustomizer
的实例。在自动注册期间,这将自动检测并透明地应用。但是,当手动构建 QuerydslDataFetcher
时,你需要使用构建器方法来应用它。
QuerydslDataFetcher
支持接口和 DTO 投影,以便在返回查询结果进行进一步的 GraphQL 处理之前对其进行转换。
要了解什么是投影,请参考 Spring Data 文档。要了解如何在 GraphQL 中使用投影,请参阅Selection Set vs Projections。 |
要将 Spring Data 投影与 Querydsl 存储库一起使用,可以创建一个投影接口或目标 DTO 类,并通过 projectAs
方法进行配置,以获得一个生成目标类型的 DataFetcher
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();
自动注册
如果存储库使用 @GraphQlRepository
进行注解,它将自动注册到尚未注册 DataFetcher
且其返回类型与存储库域类型匹配的查询。这包括单值查询、多值查询和分页查询。
默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,你可以使用 @GraphQlRepository
的 typeName
属性来指定目标 GraphQL 类型名称。
对于分页查询,存储库域类型的简单名称必须与 Connection
类型名称匹配,但不包含 Connection
结尾(例如,Book
匹配 BooksConnection
)。对于自动注册,分页是基于偏移量的,每页 20 个项目。
自动注册会检测给定的存储库是否实现了 QuerydslBinderCustomizer
,并透明地通过 QuerydslDataFetcher
构建器方法应用它。
自动注册是通过一个内置的 RuntimeWiringConfigurer
执行的,该配置器可以从 QuerydslDataFetcher
获取。Boot Starter 会自动检测 @GraphQlRepository
bean,并使用它们来初始化 RuntimeWiringConfigurer
。
如果你的存储库分别实现了 QuerydslBuilderCustomizer
或 ReactiveQuerydslBuilderCustomizer
,自动注册会通过调用存储库实例上的 customize(Builder)
来应用自定义。
Query by Example
Spring Data 支持使用 Query by Example 来获取数据。Query by Example (QBE) 是一种简单的查询技术,不需要你通过特定于存储的查询语言编写查询。
首先,声明一个实现 QueryByExampleExecutor
的存储库
public interface AccountRepository extends Repository<Account, Long>,
QueryByExampleExecutor<Account> {
}
使用 QueryByExampleDataFetcher
将存储库转换为 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();
现在你可以通过 RuntimeWiringConfigurer
注册上述 DataFetcher
。
该 DataFetcher
使用 GraphQL 参数映射创建存储库的域类型,并将其用作示例对象来获取数据。Spring Data 支持 JPA、MongoDB、Neo4j 和 Redis 的 QueryByExampleDataFetcher
。
对于作为 GraphQL 输入类型的单个参数,QueryByExampleDataFetcher 会嵌套一层,并绑定参数子映射中的值。 |
如果存储库是 ReactiveQueryByExampleExecutor
,构建器将返回 DataFetcher<Mono<Account>>
或 DataFetcher<Flux<Account>>
。Spring Data 支持 MongoDB、Neo4j、Redis 和 R2dbc 的这种变体。
自定义
QueryByExampleDataFetcher
支持接口和 DTO 投影,以便在返回查询结果进行进一步的 GraphQL 处理之前对其进行转换。
要了解什么是投影,请参考 Spring Data 文档。要理解投影在 GraphQL 中的作用,请参阅Selection Set vs Projections。 |
要将 Spring Data 投影与 Query by Example 存储库一起使用,可以创建一个投影接口或目标 DTO 类,并通过 projectAs
方法进行配置,以获得一个生成目标类型的 DataFetcher
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();
自动注册
如果存储库使用 @GraphQlRepository
进行注解,它将自动注册到尚未注册 DataFetcher
且其返回类型与存储库域类型匹配的查询。这包括单值查询、多值查询和分页查询。
默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,你可以使用 @GraphQlRepository
的 typeName
属性来指定目标 GraphQL 类型名称。
对于分页查询,存储库域类型的简单名称必须与 Connection
类型名称匹配,但不包含 Connection
结尾(例如,Book
匹配 BooksConnection
)。对于自动注册,分页是基于偏移量的,每页 20 个项目。
自动注册是通过一个内置的 RuntimeWiringConfigurer
执行的,该配置器可以从 QueryByExampleDataFetcher
获取。Boot Starter 会自动检测 @GraphQlRepository
bean,并使用它们来初始化 RuntimeWiringConfigurer
。
如果你的存储库分别实现了 QueryByExampleBuilderCustomizer
或 ReactiveQueryByExampleBuilderCustomizer
,自动注册会通过调用存储库实例上的 customize(Builder)
来应用自定义。
Selection Set 与 Projections
一个常见的疑问是,GraphQL selection sets 如何与 Spring Data projections 进行比较,以及它们各自扮演什么角色?
简而言之,Spring for GraphQL 不是一个数据网关,它不会将 GraphQL 查询直接翻译成 SQL 或 JSON 查询。相反,它让你能够利用现有的 Spring 技术,并且不假定 GraphQL schema 与底层数据模型之间存在一对一的映射。这就是为什么客户端驱动的选择和服务器端对数据模型的转换可以扮演互补的角色。
为了更好地理解,考虑 Spring Data 推崇领域驱动设计 (DDD) 作为管理数据层复杂性的推荐方法。在 DDD 中,遵守聚合的约束非常重要。根据定义,一个聚合只有在完全加载时才有效,因为部分加载的聚合可能会限制聚合的功能。
在 Spring Data 中,你可以选择是按原样暴露聚合,还是在将数据模型作为 GraphQL 结果返回之前对其应用转换。有时候只需要前者,默认情况下,Querydsl 和 Query by Example 集成将 GraphQL selection set 转换为属性路径提示,底层 Spring Data 模块使用这些提示来限制选择。
在其他情况下,为了适应 GraphQL schema,减少甚至转换底层数据模型会很有用。Spring Data 通过接口和 DTO 投影支持这一点。
接口投影定义了要暴露的固定属性集,属性可能为 null
也可能不为 null
,具体取决于数据存储查询结果。有两种类型的接口投影,它们都决定从底层数据源加载哪些属性
DTO 投影提供了更高层次的自定义,因为你可以将转换代码放在构造函数或 getter 方法中。
DTO 投影从查询中实例化,其各个属性由投影本身决定。DTO 投影通常与全参数构造函数(例如 Java 记录)一起使用,因此只有当所有必需的字段(或列)都是数据库查询结果的一部分时才能构建它们。
滚动 (Scroll)
如分页中所解释,GraphQL Cursor Connection spec 定义了一个使用 Connection
、Edge
和 PageInfo
schema 类型的分页机制,而 GraphQL Java 提供了等效的 Java 类型表示。
Spring for GraphQL 提供了内置的 ConnectionAdapter
实现,以透明地适配 Spring Data 的分页类型 Window
和 Slice
。你可以如下配置
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 | 创建将 ScrollPosition 转换为 Base64 编码光标的策略。 |
2 | 创建类型访问器以适配从 DataFetcher 返回的 Window 和 Slice 。 |
3 | 注册类型访问器。 |
在请求端,控制器方法可以声明一个 ScrollSubrange 方法参数来向前或向后分页。为此,你必须声明一个支持 ScrollPosition
的 CursorStrategy
bean。
Boot Starter 声明了一个 CursorStrategy<ScrollPosition>
bean,并且如果 classpath 中存在 Spring Data,则会如上所示注册 ConnectionFieldTypeVisitor
。
Keyset 位置
对于 KeysetScrollPosition
,光标需要从 keyset 创建,keyset 本质上是一个键值对的 Map
。要决定如何从 keyset 创建光标,可以使用 CursorStrategy<Map<String, Object>>
配置 ScrollPositionCursorStrategy
。默认情况下,JsonKeysetCursorStrategy
将 keyset Map
写入 JSON。这适用于简单的类型,如 String、Boolean、Integer 和 Double,但其他类型如果不知道目标类型,则无法恢复为相同类型。Jackson 库有一个默认类型功能,可以在 JSON 中包含类型信息。为了安全地使用它,你必须指定允许类型的列表。例如
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.allowIfSubType(ZonedDateTime.class)
.build();
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL);
然后你可以创建 JsonKeysetCursorStrategy
ObjectMapper mapper = ... ;
CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer);
默认情况下,如果创建 JsonKeysetCursorStrategy
时没有 CodecConfigurer
且 Jackson 库在 classpath 中,则会对 Date
、Calendar
和任何 java.time
中的类型应用上述自定义。