|
此版本仍在开发中,尚未被认为是稳定的。请使用最新稳定版本 Spring GraphQL 2.0.2! |
数据集成
Spring for GraphQL 让您能够利用现有的 Spring 技术,遵循常见的编程模式,通过 GraphQL 暴露底层数据源。
本节讨论了Spring Data的一个整合层,它提供了一种简单的方法来将Querydsl或示例查询仓库适配到DataFetcher中,包括为标记有@GraphQlRepository的仓库自动检测和注册GraphQL查询选项。
Querydsl
Spring for GraphQL 支持使用 Querydsl 通过 Spring Data Querydsl 扩展 获取数据。 Querydsl 提供了一种灵活且类型安全的方式来表达查询谓词,通过注解处理器生成元模型。
例如,声明一个仓库(repository)为 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。
The DataFetcher 从 GraphQL 参数构建一个 Querydsl Predicate,并使用它来获取数据。Spring Data 支持 QuerydslPredicateExecutor 对于 JPA、MongoDB、Neo4j 和 LDAP。
对于单一参数且该参数为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 中使用投影,请参见 选择集与投影。 |
要使用 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();
Auto-Registration
如果一个仓库注解了@GraphQlRepository,它将自动注册用于查询那些尚未有注册的DataFetcher并且其返回类型与仓库领域类型匹配的查询。这包括单值查询、多值查询以及分页查询。
默认情况下,查询返回的GraphQL类型名称必须与仓库领域类型的简单名称匹配。如果需要,可以使用typeName属性来指定目标GraphQL类型名称。
对于分页查询,仓库领域类型的简单名称必须与没有 Connection 结尾的 Connection 类型名称匹配(例如,Book 匹配 BooksConnection)。对于自动注册,分页基于偏移量,每页包含 20 项。
自动注册会检测给定的仓库是否实现了QuerydslBinderCustomizer,并通过QuerydslDataFetcher的构建器方法透明地应用这一实现。
自动注册是通过内置的RuntimeWiringConfigurer完成的,该RuntimeWiringConfigurer可以从QuerydslDataFetcher中获得。Boot Starter会自动检测@GraphQlRepository bean并使用它们初始化RuntimeWiringConfigurer。
Auto-registration 应用自定义设置 通过在实现 customize(Builder) 或 QuerydslBuilderCustomizer 接口的仓库实例上调用 ReactiveQuerydslBuilderCustomizer。
按示例查询
Spring Data 支持使用 查询示例(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。
The DataFetcher 使用 GraphQL 参数映射来创建仓库的域类型,并使用该类型作为查询数据的示例对象。Spring Data 支持QueryByExampleDataFetcher用于 JPA、MongoDB、Neo4j 和 Redis。
对于一个作为GraphQL输入类型单个参数,QueryByExampleDataFetcher 会在一层嵌套下,并与子映射中的值进行绑定。 |
如果仓库是ReactiveQueryByExampleExecutor,构建器返回DataFetcher<Mono<Account>>或DataFetcher<Flux<Account>>。Spring Data 支持这种变体用于 MongoDB、Neo4j、Redis 和 R2dbc。
自定义配置
QueryByExampleDataFetcher 支持接口和DTO投影,在返回这些结果进行进一步的GraphQL处理之前,将查询结果进行转换。
| 要了解什么是投影,请参阅 Spring Data 文档。 要理解投影在 GraphQL 中的作用,请参见 选择集 vs 投影。 |
要使用 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();
Auto-Registration
如果一个仓库注解了@GraphQlRepository,它将自动注册用于查询那些尚未有注册的DataFetcher并且其返回类型与仓库领域类型匹配的查询。这包括单值查询、多值查询以及分页查询。
默认情况下,查询返回的GraphQL类型名称必须与仓库领域类型的简单名称匹配。如果需要,可以使用typeName属性来指定目标GraphQL类型名称。
对于分页查询,仓库领域类型的简单名称必须与没有 Connection 结尾的 Connection 类型名称匹配(例如,Book 匹配 BooksConnection)。对于自动注册,分页基于偏移量,每页包含 20 项。
自动注册是通过内置的RuntimeWiringConfigurer完成的,该RuntimeWiringConfigurer可以从QueryByExampleDataFetcher中获得。Boot Starter会自动检测@GraphQlRepository bean并使用它们初始化RuntimeWiringConfigurer。
自动注册会应用自定义设置,通过在仓库实例上调用customize(Builder)来实现,如果你的仓库实现了QueryByExampleBuilderCustomizer或ReactiveQueryByExampleBuilderCustomizer。
其中:0对应原文中的
选择集与投影
一个常见问题是,GraphQL 选择集如何与 Spring Data 投影 比较,并且每种方式各自扮演什么角色?
短答案是,Spring for GraphQL 并不是一个数据网关,它不会直接将GraphQL 查询转换为SQL或JSON查询。相反,它允许你利用现有的Spring技术,并且不假设GraphQL模式与底层数据模型之间有一对一的映射关系。这就是为什么客户端驱动的数据选择和服务器端的数据模型转换可以相互补充。
要更好地理解,请考虑Spring Data推崇领域驱动设计(DDD)作为管理数据层复杂性的推荐方法。在DDD中,遵循聚合的约束非常重要。根据定义,只有当聚合完全加载时才是有效的,因为部分加载的聚合可能会限制聚合的功能。
在Spring Data中,您可以选择是直接暴露聚合结果,还是在将其作为GraphQL结果返回之前对数据模型应用转换。有时只需要前者即可,默认情况下,Querydsl 和 查询示例 整合将GraphQL的选择集转换为Spring Data模块使用的属性路径提示,用于限制选择。
在其他情况下,为了适应GraphQL模式,减少或甚至转换底层数据模型是有用的。Spring Data 通过接口和DTO投影支持这一点。
接口投影定义了一组固定的属性,用于暴露数据,其中的属性可能会或可能不会为null,这取决于数据存储查询结果。有两种类型的接口投影,它们都决定了从底层数据源加载哪些属性。
DTO投影提供了一种更高的自定义级别,因为您可以将转换代码放在构造函数中或getter方法中。
DTO投影是从查询中产生的,其中个别属性由投影本身确定。DTO投影通常与全参数构造函数一起使用(例如Java记录),因此只有当数据库查询结果包含所有必需字段(或列)时,才能构建这些对象。
滚动
如分页说明所述,GraphQL Cursor Connection 规范定义了使用 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)
| 1 | 创建策略将 ScrollPosition 转换为 Base64 编码的游标。 |
| 2 | 创建类型访问者以适应从DataFetcher返回的Window和Slice。 |
| 3 | 注册类型访问器。 |
在请求端,控制器方法可以声明一个
ScrollSubrange 方法参数,以实现向前或向后分页。为实现此功能,您必须声明一个 CursorStrategy
支持 ScrollPosition 作为 Bean。
The Boot Starter 声明了一个 CursorStrategy<ScrollPosition> bean,并且如果类路径中包含 Spring Data,则会注册 ConnectionFieldTypeVisitor。
键集位置
对于KeysetScrollPosition,游标需要从一个键集创建,这实际上是一个包含Map个键值对的集合。决定如何从键集中创建游标时,您可以使用ScrollPositionCursorStrategy配置CursorStrategy<Map<String, Object>>。
默认情况下,JsonKeysetCursorStrategy会将键集Map写入JSON。这适用于简单的类型如String、Boolean、Integer和Double,但对于其他类型,则需要目标类型信息才能恢复回相同类型。Jackson库具有默认的类型化功能,可以在JSON中包含类型信息。为了安全使用它,您必须指定允许的类型列表。
默认情况下,如果在类路径上存在Jackson库,并且没有创建CodecConfigurer而直接创建了JsonKeysetCursorStrategy,JSON键集将支持Date、Calendar、UUID、Java枚举、Number以及来自java.time的任何类型。
应用程序可以进一步通过实例化自己的JsonKeysetCursorStrategy来自定义密钥集的JSON序列化,使用自定义的Jackson编码器/解码器对。
在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 颗粒。
事务管理
在处理数据时,某个时刻你可能会开始关注操作的原子性和隔离性。这些都是事务的属性。GraphQL本身并没有定义任何事务语义,因此如何处理事务是由服务器和你的应用程序决定的。
GraphQL 和特别是 GraphQL Java 是设计为对数据如何获取不持偏见的。GraphQL 的一个核心特性是客户端驱动请求;字段可以在不依赖其原始来源的情况下独立解决,以允许组合。 减少的字段集可以要求获取更少的数据,从而提高性能。
应用分布式字段解析的概念在事务中并不是一个好的解决方案:
-
事务保持了一个工作单元,通常在一个事务中完成整个对象图的获取(就像典型的对象关系映射器的行为一样)。这与GraphQL的核心设计背道而驰,后者允许客户端驱动查询。
-
保持多个数据获取器中的事务跨越多个数据检索过程,每个数据获取器只负责其扁平对象的检索,可以缓解性能问题,并与解耦字段解析相一致,但这也可能导致长时间运行的事务占用资源时间过长。
通常来说,事务最好应用于更改状态的 mutation 而不是仅仅读取数据的查询。然而,在某些情况下确实需要事务性的读取。
GraphQL 设计用于在单个请求中支持多个变更操作。根据具体用例,您可能希望:<br>
-
在每个突变内部运行其自己的事务。
-
确保在一个事务中保留一些变更以保证状态的一致性。
-
跨越所有涉及的修改操作将单个事务持续下去。
每个方法都需要稍微不同的事务管理策略。
当使用Spring框架(例如JDBC)或Spring Data时,模板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
}
}
| 1 | 该addAccount方法调用在其自己的事务中运行。 |
| 2 | person 方法的调用会创建其自己的、独立的事务,该事务与 addAccount 方法没有关联,以防两个方法作为同一个 GraphQL 请求的一部分被调用。一个独立的事务伴随着不属于同一事务的所有可能缺点,例如不可重复读取或在 addAcount 和 person 方法调用之间数据被修改时导致的不一致性。 |
to 运行多个事务操作并在单一事务中保持简单设置,我们建议设计一个接受所有必要输入的 mutation 方法。该方法可以调用多个服务方法,确保它们都参与同一个事务。
事务化插桩
应用事务跟踪是一种更高级的方法,用于在整个执行过程中跨越GraphQL请求。通过在第一个数据获取器被调用之前声明一个事务,您的应用程序可以确保所有数据获取器能够参与同一个事务。
当对服务器进行监控时,您需要确保一个ExecutionStrategy运行DataFetcher调用按顺序执行,以便所有调用都在同一个Thread上执行。这是强制性的:同步事务管理使用ThreadLocal状态来允许参与事务。以AsyncSerialExecutionStrategy作为起点是一个好选择,因为它会串行执行数据获取器。
您有两种一般方法来实现事务性监控:
-
GraphQL Java的
Instrumentation合约允许在执行生命周期的不同阶段进行钩子操作。Instrumentation SPI 设计时考虑了可观测性,但它作为执行无关的扩展点,无论您是使用同步响应式还是任何其他异步形式调用数据获取器,都更不具倾向性。 -
一个
ExecutionStrategy提供了对执行的完全控制,并且可以提供多种可能性,以便在事务清理过程中将失败交易或错误信息反馈给客户端。它还可以作为实现自定义指令的良好入口点,这些指令允许客户端通过指令指定事务属性,或者使用指令在您的模式中分隔某些查询或突变的事务边界。
手动管理事务时,请确保在完成工作单元后清理事务,即提交或回滚事务。
ExceptionWhileDataFetching 可以作为 GraphQLError 用于获取底层的 Exception。当使用 SimpleDataFetcherExceptionHandler 时会构造此错误。默认情况下,Spring GraphQL 将回退到一个内部的 GraphQLError,而不暴露原始异常。
应用事务跟踪创建了重新思考事务参与的机会:所有@SchemaMapping控制器方法都会参与到事务中,无论它们是为根对象、嵌套字段还是作为某种变更的一部分被调用。
事务性的控制器方法(或调用链中的服务方法)可以声明事务属性,如传播行为REQUIRES_NEW,如果需要的话可以开始一个新的事务。