資料整合

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。