数据集成
Spring for GraphQL 允许您利用现有的 Spring 技术,遵循通用编程模型,通过 GraphQL 暴露底层数据源。
本节讨论 Spring Data 的集成层,该层提供了一种简便方法,可将 Querydsl 或 Query by Example 存储库适配为 DataFetcher,包括为标记有 @GraphQlRepository 的存储库提供自动检测和 GraphQL 查询注册的选项。
Querydsl
Spring for GraphQL 支持通过 Spring Data 的 Querydsl 扩展 使用 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: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>
自定义
QuerydslDataFetcher 支持自定义 GraphQL 参数如何绑定到属性以创建 Querydsl Predicate。默认情况下,对于每个可用属性,参数都绑定为“等于”。要自定义此行为,您可以使用 QuerydslDataFetcher 构建器方法提供 QuerydslBinderCustomizer。
一个存储库本身可以是 QuerydslBinderCustomizer 的实例。在自动注册过程中,系统会自动检测并透明地应用此功能。然而,当手动构建 QuerydslDataFetcher 时,您需要使用构建器方法来应用它。
QuerydslDataFetcher 支持接口和 DTO 投影,以便在返回查询结果进行后续 GraphQL 处理之前对其进行转换。
要了解什么是投影,请参阅 Spring Data 文档。要理解如何在 GraphQL 中使用投影,请参阅 选择集与投影。
要在 Querydsl 仓库中使用 Spring Data 投影,可以创建一个投影接口或目标 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** 匹配 **Books**Connection)。在自动注册的情况下,分页采用基于偏移量的方式,每页包含 20 个项目。
自动注册功能会检测给定的存储库是否实现了 QuerydslBinderCustomizer 接口,并通过 QuerydslDataFetcher 构建器方法透明地应用该实现。
自动注册是通过一个内置的 RuntimeWiringConfigurer 执行的,该配置器可以从 QuerydslDataFetcher 获取。Boot Starter 会自动检测 @GraphQlRepository 注解的 Bean,并使用它们来初始化 RuntimeWiringConfigurer。
自动注册通过调用存储库实例上的 customize(Builder) 来应用自定义配置,前提是你的存储库分别实现了 QuerydslBuilderCustomizer 或 ReactiveQuerydslBuilderCustomizer。
按示例查询
Spring Data 支持使用按示例查询来获取数据。按示例查询(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 支持此变体。
构建设置
Query by Example 功能已包含在支持该功能的 Spring Data 模块中,因此无需额外设置即可启用。
自定义
QueryByExampleDataFetcher 支持接口和 DTO 投影,以便在返回查询结果进行进一步 GraphQL 处理之前对其进行转换。
要了解投影是什么,请参阅 Spring Data 文档。要理解投影在 GraphQL 中的作用,请查看 选择集与投影。
要在使用按示例查询的存储库时应用Spring Data投影,可以创建一个投影接口或目标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** 匹配 **Books**Connection)。在自动注册的情况下,分页采用基于偏移量的方式,每页包含 20 个项目。
自动注册是通过一个内置的 RuntimeWiringConfigurer 来执行的,该配置器可以从 QueryByExampleDataFetcher 获取。Boot Starter 会自动检测 @GraphQlRepository 注解的 Bean,并使用它们来初始化 RuntimeWiringConfigurer。
自动注册通过调用存储库实例上的 customize(Builder) 来应用自定义功能,前提是你的存储库分别实现了 QueryByExampleBuilderCustomizer 或 ReactiveQueryByExampleBuilderCustomizer 接口。
选择集与投影
一个常见的问题是,GraphQL 选择集与 Spring Data 投影 有何异同,它们各自扮演什么角色?
简单来说,Spring for GraphQL 并非一个直接将 GraphQL 查询转换为 SQL 或 JSON 查询的数据网关。相反,它允许你利用现有的 Spring 技术,并且不假定 GraphQL 模式与底层数据模型之间存在一一对应的映射关系。这正是为什么客户端驱动的数据选择与服务器端的数据模型转换可以发挥互补作用。
为了更好地理解,请考虑 Spring Data 提倡领域驱动设计(DDD)作为管理数据层复杂性的推荐方法。在 DDD 中,遵守聚合的约束非常重要。根据定义,聚合只有在完全加载时才有效,因为部分加载的聚合可能会限制聚合的功能。
在其他情况下,为了适配GraphQL模式,减少甚至转换底层数据模型是有用的。Spring Data通过接口和DTO投影来支持这一点。
接口投影定义了一组固定的属性来公开,这些属性可能为null也可能不为null,具体取决于数据存储查询结果。接口投影有两种类型,它们都决定了从底层数据源加载哪些属性:
DTO投影提供了更高程度的自定义能力,因为您可以将转换代码放置在构造函数或getter方法中。
DTO 投影通过查询具体化,其中各个属性由投影本身决定。DTO 投影通常与全参数构造函数(例如 Java 记录)一起使用,因此只有在所有必需字段(或列)都是数据库查询结果的一部分时,才能构建它们。
滚动
如分页所述,GraphQL 游标连接规范定义了使用 Connection、Edge 和 PageInfo 模式类型进行分页的机制,而 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
创建将
ScrollPosition转换为 Base64 编码游标的策略。创建类型访问器以适配从
DataFetcher返回的Window和Slice。注册该类型访问器。
在请求端,控制器方法可以声明一个 ScrollSubrange 方法参数来实现向前或向后分页。为此,你必须声明一个支持 ScrollPosition 的 CursorStrategy 作为 Bean。
Boot Starter 声明了一个 CursorStrategy<ScrollPosition> Bean,并在类路径中存在 Spring Data 时如上所示注册了 ConnectionFieldTypeVisitor。
键集定位
对于 KeysetScrollPosition,游标需要从键集创建,键集本质上是一个键值对的 Map。要决定如何从键集创建游标,你可以使用 CursorStrategy<Map<String, Object>> 来配置 ScrollPositionCursorStrategy。默认情况下,JsonKeysetCursorStrategy 会将键集 Map 写入 JSON。这对于简单类型如 String、Boolean、Integer 和 Double 有效,但其他类型在没有目标类型信息的情况下无法恢复为相同类型。Jackson 库有一个默认类型标注功能,可以在 JSON 中包含类型信息。要安全地使用此功能,你必须指定允许类型的列表。
默认情况下,如果创建 JsonKeysetCursorStrategy 时未指定 CodecConfigurer 且类路径中包含 Jackson 库,JSON 游标键将支持 Date、Calendar、UUID、Java 枚举、Number 以及 java.time 包中的任何类型。
应用程序可以通过实例化自己的 JsonKeysetCursorStrategy 并搭配自定义的 Jackson 编码器/解码器对,进一步优化 JSON 格式的键集序列化。在 Spring Boot 中,只需提供如下所示的 EncodingCursorStrategy 即可:
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.graphql.data.pagination.CursorEncoder;
import org.springframework.graphql.data.pagination.CursorStrategy;
import org.springframework.graphql.data.pagination.EncodingCursorStrategy;
import org.springframework.graphql.data.query.JsonKeysetCursorStrategy;
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
@Configuration
public class KeysetCursorConfiguration {
@Bean
// override the EncodingCursorStrategy bean in Spring Boot
public EncodingCursorStrategy<ScrollPosition> cursorStrategy() {
JsonKeysetCursorStrategy keysetCursorStrategy = keysetCursorStrategy();
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy(keysetCursorStrategy);
return CursorStrategy.withEncoder(cursorStrategy, CursorEncoder.base64());
}
// create a cursor strategy with a custom CodecConfigurer
private JsonKeysetCursorStrategy keysetCursorStrategy() {
JsonMapper mapper = keysetJsonMapper();
CodecConfigurer codecConfigurer = keysetCodecConfigurer(mapper);
return new JsonKeysetCursorStrategy(codecConfigurer);
}
// use a custom JsonMapper for encoding/decoding JSON
private CodecConfigurer keysetCodecConfigurer(JsonMapper jsonMapper) {
CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(jsonMapper));
configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(jsonMapper));
return configurer;
}
// create a custom JsonMapper
private JsonMapper keysetJsonMapper() {
// Configure which types should be allowed for serialization
// those should include all fields included in the keyset
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.allowIfSubType(Calendar.class)
.allowIfSubType(Date.class)
.allowIfSubType(UUID.class)
.allowIfSubType(Number.class)
.allowIfSubType(Enum.class)
.allowIfSubType("java.time.")
.build();
return JsonMapper.builder()
.activateDefaultTyping(validator, DefaultTyping.NON_FINAL)
// as of Jackson 3.0, dates are not written as timestamps by default
.enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
}
}
排序
Spring for GraphQL 定义了一个 SortStrategy 接口,用于根据 GraphQL 参数创建 Sort 对象。AbstractSortStrategy 实现了该契约,并通过抽象方法提取排序方向和属性。要启用 Sort 作为控制器方法参数的支持,您需要声明一个 SortStrategy bean。
事务管理
在处理数据时,操作的原子性和隔离性在某些情况下变得至关重要。这两者都是事务的特性。GraphQL 本身并未定义任何事务语义,因此如何处理事务取决于服务器和您的应用程序。
GraphQL,特别是 GraphQL Java,在设计上对数据获取方式持中立态度。GraphQL 的一个核心特性是客户端驱动请求;字段的解析可以独立于其原始数据源,从而实现组合查询。精简的字段集可以减少所需获取的数据量,从而提升性能。
在事务中应用分布式字段解析的概念并不合适:
-
事务将工作单元保持在一起,通常导致在单个事务中获取整个对象图(就像典型的对象关系映射器那样)。这与 GraphQL 让客户端驱动查询的核心设计相悖。
-
在多个数据获取器之间保持事务开放,每个获取器仅获取其扁平对象,这缓解了性能方面的问题,并与解耦的字段解析保持一致,但可能导致长时间运行的事务,使资源占用时间超出必要。
一般来说,事务最适合用于改变状态的变更操作,而不一定适用于仅读取数据的查询。然而,在某些用例中,确实需要事务性读取。
GraphQL 设计支持在单个请求中执行多个变更操作。根据具体使用场景,您可能需要:
-
每个变更操作在各自的事务中执行。
-
将部分变更操作置于同一事务中,以确保状态一致性。
-
将所有相关变更操作置于单个事务中执行。
每种方法都需要略微不同的事务管理策略。
在使用 Spring Framework(例如 JDBC)或 Spring Data 时,Template API 和存储库默认(无需任何额外配置)会为单个操作使用隐式事务,这会导致每个存储库方法调用都会启动并提交一个事务。这是大多数数据库的正常操作模式。
以下部分概述了在 GraphQL 服务器中管理事务的两种不同策略:
事务性控制器方法
管理事务的最简单方法是结合使用 Spring 的事务管理与 @MutationMapping 控制器方法(或任何其他 @SchemaMapping 方法),例如:
- 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 -> {
// ...
});
}
}
一个事务从进入 addAccount 方法开始,直到其返回结束。在此期间,对事务性资源的所有调用都属于同一个事务,从而确保了变更的原子性和隔离性。
这是推荐的方法。它让您能够完全控制事务边界,并提供一个明确定义的入口点,而无需对 GraphQL 服务器基础设施进行插桩。
在方法调用后清理事务会导致后续数据获取(例如,对于嵌套字段)不属于事务性方法 addAccount 的一部分,如下所述:
@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
}
}
addAccount方法的调用在其自身的事务中运行。person方法的调用会创建其自身独立的事务,该事务与addAccount方法无关,即使这两个方法作为同一个 GraphQL 请求的一部分被调用。独立的事务会带来所有可能的不在同一事务中的缺点,例如不可重复读,或者在addAccount和person方法调用之间数据被修改而导致的不一致。
为了在保持简单设置的同时,在单个事务中运行多个变更操作,我们建议设计一个接受所有必需输入的变更方法。该方法随后可以调用多个服务方法,确保它们都参与同一个事务。
事务性仪表化
应用事务性插装是一种更高级的方法,用于在GraphQL请求的整个执行过程中跨越一个事务。通过在调用第一个数据获取器之前声明一个事务,您的应用程序可以确保所有数据获取器都能参与同一个事务。
在服务器端进行插桩时,你需要确保 ExecutionStrategy 以串行方式运行 DataFetcher 调用,以便所有调用都在同一个 Thread 上执行。这是强制性的:同步事务管理使用 ThreadLocal 状态来允许参与事务。将 AsyncSerialExecutionStrategy 作为起点是一个不错的选择,因为它会串行执行数据获取器。
实现事务性检测有两种通用方案:
-
GraphQL Java 的
Instrumentation契约允许在执行的各个阶段介入生命周期。该 Instrumentation SPI 在设计时考虑了可观测性,但它作为与执行方式无关的扩展点,无论您使用同步响应式还是任何其他异步形式来调用数据获取器,在这方面都保持较低的倾向性。 -
ExecutionStrategy提供了对执行的完全控制,并为多种可能性打开了大门,例如如何将失败的事务或事务清理期间的错误传达回客户端。它也可以作为实现自定义指令的良好切入点,允许客户端通过指令指定事务属性,或在架构中使用指令为特定查询或变更划定事务边界。
在手动管理事务时,请确保在完成工作单元后清理事务,即提交或回滚。ExceptionWhileDataFetching 是一个有用的 GraphQLError,可用于获取底层 Exception。此错误在使用 SimpleDataFetcherExceptionHandler 时构造。默认情况下,Spring GraphQL 会回退到一个不暴露原始异常的内部 GraphQLError。
应用事务性工具为重新思考事务参与创造了机会:所有 @SchemaMapping 控制器方法都会参与事务,无论它们是为根字段、嵌套字段调用,还是作为变更操作的一部分。事务性控制器方法(或调用链中的服务方法)可以声明事务属性,例如传播行为 REQUIRES_NEW,以便在需要时启动新事务。