資料整合
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 型別的名稱必須與倉庫域型別的簡單名稱匹配。如果需要,你可以使用 @GraphQlRepository 的 typeName 屬性來指定目標 GraphQL 型別名稱。
對於分頁查詢,倉庫域型別的簡單名稱必須與不帶 Connection 字尾的 Connection 型別名稱匹配(例如,Book 匹配 BooksConnection)。對於自動註冊,分頁是基於偏移的,每頁 20 項。
自動註冊會檢測給定倉庫是否實現了 QuerydslBinderCustomizer,並透過 QuerydslDataFetcher 構建器方法透明地應用它。
自動註冊透過內建的 RuntimeWiringConfigurer 執行,該配置器可從 QuerydslDataFetcher 獲取。Boot Starter 會自動檢測 @GraphQlRepository bean,並使用它們來初始化 RuntimeWiringConfigurer。
如果你的倉庫分別實現了 QuerydslBuilderCustomizer 或 ReactiveQuerydslBuilderCustomizer,自動註冊會透過呼叫倉庫例項上的 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 的此變體。
自定義
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 型別的名稱必須與倉庫域型別的簡單名稱匹配。如果需要,你可以使用 @GraphQlRepository 的 typeName 屬性來指定目標 GraphQL 型別名稱。
對於分頁查詢,倉庫域型別的簡單名稱必須與不帶 Connection 字尾的 Connection 型別名稱匹配(例如,Book 匹配 BooksConnection)。對於自動註冊,分頁是基於偏移的,每頁 20 項。
自動註冊透過內建的 RuntimeWiringConfigurer 執行,該配置器可從 QueryByExampleDataFetcher 獲取。Boot Starter 會自動檢測 @GraphQlRepository bean,並使用它們來初始化 RuntimeWiringConfigurer。
如果你的倉庫分別實現了 QueryByExampleBuilderCustomizer 或 ReactiveQueryByExampleBuilderCustomizer,自動註冊會透過呼叫倉庫例項上的 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,具體取決於資料儲存查詢結果。有兩種介面投影,它們都決定從底層資料來源載入哪些屬性
DTO 投影提供更高水平的定製,因為你可以將轉換程式碼放在建構函式或 getter 方法中。
DTO 投影從查詢中具體化,其中各個屬性由投影本身確定。DTO 投影通常與全引數建構函式(例如 Java 記錄)一起使用,因此只有當所有必需欄位(或列)都屬於資料庫查詢結果時才能構造它們。
滾動
如分頁中所述,GraphQL Cursor Connection 規範定義了一種使用 Connection、Edge 和 PageInfo 模式型別進行分頁的機制,而 GraphQL Java 提供了等效的 Java 型別表示。
Spring for GraphQL 提供了內建的 ConnectionAdapter 實現,以透明地適配 Spring Data 分頁型別 Window 和 Slice。你可以按如下方式配置:
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 | 註冊型別訪問器。 |
在請求端,控制器方法可以宣告一個 ScrollSubrange 方法引數來向前或向後分頁。為此,你必須宣告一個支援 ScrollPosition 的 CursorStrategy 作為 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 庫在類路徑中,則上述自定義會應用於 Date、Calendar、UUID 以及 java.time 中的任何型別。
排序
Spring for GraphQL 定義了一個 SortStrategy,用於從 GraphQL 引數建立 Sort。AbstractSortStrategy 透過抽象方法實現契約,以提取排序方向和屬性。要啟用對 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 方法無關。獨立的事務會帶來所有可能的不屬於同一個事務的缺點,例如不可重複讀或在 addAccount 和 person 方法呼叫之間資料被修改時可能出現的不一致。 |
為了在單個事務中執行多個突變並保持簡單設定,我們建議設計一個接受所有必需輸入的突變方法。該方法可以呼叫多個服務方法,確保它們都參與同一個事務。
事務性檢測
應用事務性檢測是一種更高階的方法,用於將事務跨越 GraphQL 請求的整個執行過程。透過在第一次資料獲取器被呼叫之前宣告事務,你的應用程式可以確保所有資料獲取器都可以參與到同一個事務中。
在檢測伺服器時,你需要確保 ExecutionStrategy 序列執行 DataFetcher 呼叫,以便所有呼叫都在同一個 Thread 上執行。這是強制性的:同步事務管理使用 ThreadLocal 狀態以允許參與事務。考慮 AsyncSerialExecutionStrategy 作為起點是一個不錯的選擇,因為它序列執行資料獲取器。
你有兩種通用選項來實現事務性檢測
-
GraphQL Java 的
Instrumentation契約允許在各個階段介入執行生命週期。Instrumentation SPI 的設計考慮了可觀察性,但它作為與執行無關的擴充套件點,無論你使用同步、響應式還是任何其他非同步形式來呼叫資料獲取器,並且在這方面不那麼固執己見。 -
ExecutionStrategy提供對執行的完全控制,並開闢了多種可能性,可以將失敗的事務或事務清理期間的錯誤反饋給客戶端。它還可以作為很好的入口點,用於實現自定義指令,允許客戶端透過指令指定事務屬性,或在你的模式中使用指令來劃定某些查詢或突變的事務邊界。
手動管理事務時,請確保在完成工作單元后清理事務,即提交或回滾。ExceptionWhileDataFetching 是一個有用的 GraphQLError,用於獲取底層 Exception。當使用 SimpleDataFetcherExceptionHandler 時,會構造此錯誤。預設情況下,Spring GraphQL 會回退到不暴露原始異常的內部 GraphQLError。
應用事務性檢測創造了重新思考事務參與的機會:所有 @SchemaMapping 控制器方法都參與事務,無論它們是為根、巢狀欄位還是作為突變的一部分被呼叫。事務性控制器方法(或呼叫鏈中的服務方法)可以宣告事務屬性,例如傳播行為 REQUIRES_NEW,以便在需要時啟動新事務。