数据集成

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 类型的名称必须与存储库域类型的简单名称匹配。如果需要,你可以使用 @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 来获取数据。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 中的作用,请参阅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 类型的名称必须与存储库域类型的简单名称匹配。如果需要,你可以使用 @GraphQlRepositorytypeName 属性来指定目标 GraphQL 类型名称。

对于分页查询,存储库域类型的简单名称必须与 Connection 类型名称匹配,但不包含 Connection 结尾(例如,Book 匹配 BooksConnection)。对于自动注册,分页是基于偏移量的,每页 20 个项目。

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

如果你的存储库分别实现了 QueryByExampleBuilderCustomizerReactiveQueryByExampleBuilderCustomizer,自动注册会通过调用存储库实例上的 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 结果返回之前对其应用转换。有时候只需要前者,默认情况下,QuerydslQuery by Example 集成将 GraphQL selection set 转换为属性路径提示,底层 Spring Data 模块使用这些提示来限制选择。

在其他情况下,为了适应 GraphQL schema,减少甚至转换底层数据模型会很有用。Spring Data 通过接口和 DTO 投影支持这一点。

接口投影定义了要暴露的固定属性集,属性可能为 null 也可能不为 null,具体取决于数据存储查询结果。有两种类型的接口投影,它们都决定从底层数据源加载哪些属性

  • 封闭接口投影 在你无法部分实例化聚合对象,但仍想暴露一部分属性时很有用。

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

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

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

滚动 (Scroll)

分页中所解释,GraphQL Cursor Connection spec 定义了一个使用 ConnectionEdgePageInfo schema 类型的分页机制,而 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。

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 中,则会对 DateCalendar 和任何 java.time 中的类型应用上述自定义。

排序 (Sort)

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