数据集成

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 中使用投影,请参阅选择集与投影

要将 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 类型的名称必须与仓库域类型的简单名称匹配。如果需要,你可以使用 @GraphQlRepositorytypeName 属性来指定目标 GraphQL 类型名称。

对于分页查询,仓库域类型的简单名称必须与不带 Connection 后缀的 Connection 类型名称匹配(例如,Book 匹配 BooksConnection)。对于自动注册,分页是基于偏移的,每页 20 项。

自动注册会检测给定仓库是否实现了 QuerydslBinderCustomizer,并通过 QuerydslDataFetcher 构建器方法透明地应用它。

自动注册通过内置的 RuntimeWiringConfigurer 执行,该配置器可从 QuerydslDataFetcher 获取。Boot Starter 会自动检测 @GraphQlRepository bean,并使用它们来初始化 RuntimeWiringConfigurer

如果你的仓库分别实现了 QuerydslBuilderCustomizerReactiveQuerydslBuilderCustomizer,自动注册会通过调用仓库实例上的 customize(Builder) 来应用自定义

按示例查询 (Query by Example)

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

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 投影与 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 类型的名称必须与仓库域类型的简单名称匹配。如果需要,你可以使用 @GraphQlRepositorytypeName 属性来指定目标 GraphQL 类型名称。

对于分页查询,仓库域类型的简单名称必须与不带 Connection 后缀的 Connection 类型名称匹配(例如,Book 匹配 BooksConnection)。对于自动注册,分页是基于偏移的,每页 20 项。

自动注册通过内置的 RuntimeWiringConfigurer 执行,该配置器可从 QueryByExampleDataFetcher 获取。Boot Starter 会自动检测 @GraphQlRepository bean,并使用它们来初始化 RuntimeWiringConfigurer

如果你的仓库分别实现了 QueryByExampleBuilderCustomizerReactiveQueryByExampleBuilderCustomizer,自动注册会通过调用仓库实例上的 customize(Builder) 来应用自定义

选择集与投影

一个常见的问题是,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 也可能不为 null,具体取决于数据存储查询结果。有两种接口投影,它们都决定从底层数据源加载哪些属性

  • 封闭接口投影在无法部分实现聚合对象但仍想暴露属性子集时很有用。

  • 开放接口投影利用 Spring 的 @Value 注解和 SpEL 表达式来应用轻量级数据转换,例如拼接、计算或将静态函数应用于属性。

DTO 投影提供更高水平的定制,因为你可以将转换代码放在构造函数或 getter 方法中。

DTO 投影从查询中具体化,其中各个属性由投影本身确定。DTO 投影通常与全参数构造函数(例如 Java 记录)一起使用,因此只有当所有必需字段(或列)都属于数据库查询结果时才能构造它们。

滚动

分页中所述,GraphQL Cursor Connection 规范定义了一种使用 ConnectionEdgePageInfo 模式类型进行分页的机制,而 GraphQL Java 提供了等效的 Java 类型表示。

Spring for GraphQL 提供了内置的 ConnectionAdapter 实现,以透明地适配 Spring Data 分页类型 WindowSlice。你可以按如下方式配置:

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 返回的 WindowSlice
3 注册类型访问器。

在请求端,控制器方法可以声明一个 ScrollSubrange 方法参数来向前或向后分页。为此,你必须声明一个支持 ScrollPositionCursorStrategy 作为 bean。

如果 Spring Data 位于类路径中,Boot Starter 会声明一个 CursorStrategy<ScrollPosition> bean,并注册如上所示的 ConnectionFieldTypeVisitor

Keyset 位置

对于 KeysetScrollPosition,游标需要从键集创建,键集本质上是键值对的 Map。为了决定如何从键集创建游标,你可以使用 CursorStrategy<Map<String, Object>> 配置 ScrollPositionCursorStrategy。默认情况下,JsonKeysetCursorStrategy 将键集 Map 写入 JSON。这适用于简单的类型,如 String、Boolean、Integer 和 Double,但其他类型无法在没有目标类型信息的情况下恢复为相同类型。Jackson 库具有默认类型功能,可以在 JSON 中包含类型信息。要安全地使用它,你必须指定允许的类型列表。例如

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();

然后你可以创建 JsonKeysetCursorStrategy

ObjectMapper mapper = ... ;

CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper));
configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper));

JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer);

默认情况下,如果创建 JsonKeysetCursorStrategy 时没有 CodecConfigurer 且 Jackson 库在类路径中,则上述自定义会应用于 DateCalendarUUID 以及 java.time 中的任何类型。

排序

Spring for GraphQL 定义了一个 SortStrategy,用于从 GraphQL 参数创建 SortAbstractSortStrategy 通过抽象方法实现契约,以提取排序方向和属性。要启用对 Sort 作为控制器方法参数的支持,你需要声明一个 SortStrategy bean。

事务管理

在某个时刻,处理数据操作的原子性和隔离性变得很重要。这两个都是事务的属性。GraphQL 本身不定义任何事务语义,因此由服务器和你的应用程序来决定如何处理事务。

GraphQL,特别是 GraphQL Java,旨在对数据获取方式不持任何观点。GraphQL 的一个核心属性是客户端驱动请求;字段可以独立于其原始源进行解析,以允许组合。减少的字段集可以减少所需获取的数据量,从而带来更好的性能。

在事务中应用分布式字段解析的概念并不合适

  • 事务将一个工作单元保持在一起,通常会导致在单个事务中获取整个对象图(就像典型的对象关系映射器那样)。这与 GraphQL 的核心设计——让客户端驱动查询——相悖。

  • 在多个数据获取器之间保持事务打开,每个数据获取器只获取其平面对象,这减轻了性能方面的影响,并与解耦的字段解析相符,但这可能导致事务长时间运行,占用资源的时间超过必要。

一般来说,事务最适合应用于改变状态的突变(mutations),而不一定适用于只读取数据的查询(queries)。但是,有些用例确实需要事务性读取。

GraphQL 旨在支持单个请求中的多个突变。根据用例,你可能需要

  • 在各自的事务中运行每个突变。

  • 将某些突变保持在单个事务中以确保一致状态。

  • 将单个事务跨越所有涉及的突变。

每种方法都需要略微不同的事务管理策略。

当使用 Spring Framework(例如 JDBC)或 Spring Data 时,模板 API 和仓库默认(无需任何进一步的工具)对单个操作使用隐式事务,导致每次仓库方法调用都会启动和提交事务。这是大多数数据库的正常操作模式。

以下部分概述了在 GraphQL 服务器中管理事务的两种不同策略:

事务性控制器方法

管理事务最简单的方法是结合使用 Spring 的事务管理和 @MutationMapping 控制器方法(或任何其他 @SchemaMapping 方法),例如

  • 声明式

  • 编程式

@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 方法调用作为同一个 GraphQL 请求的一部分被调用,它会创建自己的独立事务,该事务与 addAccount 方法无关。独立的事务会带来所有可能的不属于同一个事务的缺点,例如不可重复读或在 addAccountperson 方法调用之间数据被修改时可能出现的不一致。

为了在单个事务中运行多个突变并保持简单设置,我们建议设计一个接受所有必需输入的突变方法。该方法可以调用多个服务方法,确保它们都参与同一个事务。

事务性检测

应用事务性检测是一种更高级的方法,用于将事务跨越 GraphQL 请求的整个执行过程。通过在第一次数据获取器被调用之前声明事务,你的应用程序可以确保所有数据获取器都可以参与到同一个事务中。

在检测服务器时,你需要确保 ExecutionStrategy 串行运行 DataFetcher 调用,以便所有调用都在同一个 Thread 上执行。这是强制性的:同步事务管理使用 ThreadLocal 状态以允许参与事务。考虑 AsyncSerialExecutionStrategy 作为起点是一个不错的选择,因为它串行执行数据获取器。

你有两种通用选项来实现事务性检测

  1. GraphQL Java 的 Instrumentation 契约允许在各个阶段介入执行生命周期。Instrumentation SPI 的设计考虑了可观察性,但它作为与执行无关的扩展点,无论你使用同步、响应式还是任何其他异步形式来调用数据获取器,并且在这方面不那么固执己见。

  2. ExecutionStrategy 提供对执行的完全控制,并开辟了多种可能性,可以将失败的事务或事务清理期间的错误反馈给客户端。它还可以作为很好的入口点,用于实现自定义指令,允许客户端通过指令指定事务属性,或在你的模式中使用指令来划定某些查询或突变的事务边界。

手动管理事务时,请确保在完成工作单元后清理事务,即提交或回滚。ExceptionWhileDataFetching 是一个有用的 GraphQLError,用于获取底层 Exception。当使用 SimpleDataFetcherExceptionHandler 时,会构造此错误。默认情况下,Spring GraphQL 会回退到不暴露原始异常的内部 GraphQLError

应用事务性检测创造了重新思考事务参与的机会:所有 @SchemaMapping 控制器方法都参与事务,无论它们是为根、嵌套字段还是作为突变的一部分被调用。事务性控制器方法(或调用链中的服务方法)可以声明事务属性,例如传播行为 REQUIRES_NEW,以便在需要时启动新事务。

© . This site is unofficial and not affiliated with VMware.