資料整合

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 中使用投影,請參閱 選取集與投影的比較

若要將 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) 來套用自訂

範例查詢

Spring Data 支援使用範例查詢來擷取資料。範例查詢 (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 的此變體。

建置設定

範例查詢已包含在 Spring Data 模組中,適用於支援它的資料儲存區,因此不需要額外的設定即可啟用它。

自訂

QueryByExampleDataFetcher 支援介面和 DTO 投影,以便在傳回查詢結果以進行進一步的 GraphQL 處理之前轉換它們。

若要了解什麼是投影,請參閱 Spring Data 文件。若要了解投影在 GraphQL 中的作用,請參閱 選取集與投影的比較

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

自動註冊

如果儲存庫使用 @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,具體取決於資料儲存區查詢結果。有兩種介面投影,兩者都決定要從底層資料來源載入哪些屬性

  • 封閉介面投影 如果您無法部分具體化聚合物件,但仍然想要公開屬性的子集,則封閉介面投影會很有用。

  • 開放介面投影 利用 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 方法引數以向前或向後分頁。為了使其運作,您必須宣告一個支援 ScrollPosition 作為 bean 的 CursorStrategy

Boot Starter 會宣告一個 CursorStrategy<ScrollPosition> bean,並註冊如上所示的 ConnectionFieldTypeVisitor,如果 Spring Data 在類別路徑上的話。

Keyset 位置

對於 KeysetScrollPosition,游標需要從鍵集建立,鍵集本質上是鍵值對的 Map。若要決定如何從鍵集建立游標,您可以使用 CursorStrategy<Map<String, Object>> 設定 ScrollPositionCursorStrategy。預設情況下,JsonKeysetCursorStrategy 會將鍵集 Map 寫入 JSON。這適用於 StringBooleanIntegerDouble 等簡單型別,但其他型別在沒有目標型別資訊的情況下無法還原回相同的型別。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 程式庫在類別路徑上,則上述自訂會套用於 DateCalendarjava.time 中的任何型別。

排序

Spring for GraphQL 定義了一個 SortStrategy,以從 GraphQL 引數建立 SortAbstractSortStrategy 實作了合約,其中包含用於擷取排序方向和屬性的抽象方法。若要啟用對 Sort 作為控制器方法引數的支援,您需要宣告一個 SortStrategy bean。