Data Integration

Spring for GraphQL 让你能够利用现有的 Spring 技术,按照常见的编程模型通过 GraphQL 公开底层数据源。 本节讨论 Spring Data 的集成层,它提供了一种简单的方法来改编 Querydsl 或 Query by Example 存储库为 DataFetcher,包括自动检测和 GraphQL 查询注册带有 @GraphQlRepository 的存储库。

Querydsl

Spring for GraphQL 支持使用 Querydsl通过 Spring Data Querydsl extension 提取数据。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();

你现在可以通过一个 xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[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 的此变体。

Build Setup

要在你的构建中配置 Querydsl,请按照 official reference documentation操作:

例如:

  • 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 示例对`artifactRepositories`使用 Querydsl。

Customizations

QuerydslDataFetcher 支持自定义如何将 GraphQL 参数绑定到属性以创建 Querydsl Predicate。默认情况下,参数绑定为 “等于” 对每个可用的属性。要自定义它,你可以使用 QuerydslDataFetcher 生成器方法来提供一个 QuerydslBinderCustomizer

一个资源库本身可以是 QuerydslBinderCustomizer 的一个实例。这是在 Auto-Registration 期间自动检测和透明应用的。但是,在手动构建 QuerydslDataFetcher 时,你将需要使用构建器方法来应用它。

`QuerydslDataFetcher`支持接口和DTO投影,以便在为进一步的GraphQL处理返回查询结果之前对其进行转换。

要了解投影是什么,请参考 Spring Data docs。要了解如何在 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();

Auto-Registration

如果一个资源库使用 @GraphQlRepository 注释,则它将自动注册为查询,这些查询还没有注册 DataFetcher,并且其返回类型与资源库领域类型匹配。这包括单值查询、多值查询和 paginated 查询。

默认情况下,查询返回的GraphQL类型的名称必须与存储库域类型名称的简单名称匹配。如果需要,可以使用`@GraphQlRepository`的`typeName`属性来指定目标GraphQL类型名称。

对于分页查询,存储库域类型的简单名称必须与没有`Connection`结尾的`Connection`类型名称匹配(例如`Book匹配*Books*Connection`)。对于自动注册,分页是基于偏移量的,一页有20个项目。

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

自动注册通过内置 RuntimeWiringConfigurer 执行,它可以从 QuerydslDataFetcher 中获取。Boot Starter 自动检测 @GraphQlRepository bean,并使用它们对`RuntimeWiringConfigurer` 进行初始化。

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

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

你现在可以通过一个 xref:request-execution.adoc#execution.graphqlsource.runtimewiring-configurer[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的此变体。

Build Setup

按实例查询已包含在Spring Data模块中,适用于支持该查询的数据存储,因此无需额外的设置即可启用它。

Customizations

`QueryByExampleDataFetcher`支持接口和DTO投影,以便在为进一步的GraphQL处理返回查询结果之前对其进行转换。

要了解投影是什么,请参考 Spring Data documentation。要了解投影在 GraphQL 中的作用,请参阅 Selection Set vs Projections

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

Auto-Registration

如果一个资源库使用 @GraphQlRepository 注释,则它将自动注册为查询,这些查询还没有注册 DataFetcher,并且其返回类型与资源库领域类型匹配。这包括单值查询、多值查询和 paginated 查询。

默认情况下,查询返回的GraphQL类型的名称必须与存储库域类型名称的简单名称匹配。如果需要,可以使用`@GraphQlRepository`的`typeName`属性来指定目标GraphQL类型名称。

对于分页查询,存储库域类型的简单名称必须与没有`Connection`结尾的`Connection`类型名称匹配(例如`Book匹配*Books*Connection`)。对于自动注册,分页是基于偏移量的,一页有20个项目。

自动注册通过内置 RuntimeWiringConfigurer 执行,它可以从 QueryByExampleDataFetcher 中获取。Boot Starter 自动检测 @GraphQlRepository bean,并使用它们对`RuntimeWiringConfigurer` 进行初始化。

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

Selection Set vs Projections

一个常见问题是,GraphQL 选择集与 Spring Data projections有何比较,以及各自扮演什么角色?

简短的回答是 Spring for GraphQL 并不是一个直接将 GraphQL 查询转换为 SQL 或 JSON 查询的数据网关。相反,它让你能够利用现有的 Spring 技术,并且它并不想当然地认为 GraphQL 架构和底层数据模型之间是一对一的映射。这就是为什么客户端驱动的选择和服务器端数据模型的转换能够起到互补作用。

为了更好地理解这一点,考虑一下 Spring Data 推崇基于领域(DDD)的设计作为在数据层管理复杂性的推荐方法。在 DDD 中,遵守聚合的约束非常重要。在定义上,聚合只有在加载全体时才有效,因为部分加载的聚合可能会限制聚合的功能。

在 Spring Data 中,你可以选择是否希望按原样公开聚合,或者在将聚合作为 GraphQL 结果返回之前,是否将转换应用于数据模型。有时执行前者就足够了,默认情况下,QuerydslQuery by Example 集成将 GraphQL 选择集转换为属性路径提示,底层的 Spring Data 模块使用这些提示来限制选择。

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

接口投影定义了一组固定的属性来公开,这些属性可能为 null 或不为 null,具体取决于数据存储查询结果。有两种接口投影,两者都决定了从底层数据源加载哪些属性:

  • Closed interface projections在你无法部分实现聚合对象,但仍希望公开部分属性时,会非常有用。

  • Open interface projections利用 Spring 的 `@Value`注解和https://docs.spring.io/spring-framework/reference/core/expressions.html[SpEL] 表达式对属性应用轻量级数据转换,例如串联、计算或应用静态函数。

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

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

Scroll

正如 Pagination 中所解释的,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 返回的 Window`和 `Slice
3 Register the type visitor.

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

如果 Spring Data 在类路径中,则 Boot Starter 声明一个 CursorStrategy<ScrollPosition> bean,并注册上文所示的`ConnectionFieldTypeVisitor`。

Keyset Position

对于 KeysetScrollPosition,游标需要从键集创建,键集本质上是一个键值对的 Map。为了决定如何从键集创建游标,你可以使用 CursorStrategy<Map<String, Object>> 配置 ScrollPositionCursorStrategy。默认情况下,JsonKeysetCursorStrategy 将键集 Map 写入 JSON。对于字符串、布尔值、整数和双精度这样的简单值,该方法有效,但其他值不能在没有目标类型信息的情况下恢复为相同类型。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);

默认情况下,如果在没有 CodecConfigurer 的情况下创建了 JsonKeysetCursorStrategy,并且 Jackson 库在类路径上,将会为 DateCalendarjava.time 中的任何类型应用类似于上面的自定义。

Sort

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