資料整合
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 型別的名稱必須與儲存庫域型別的簡單名稱匹配。如果需要,你可以使用 @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 來獲取資料。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 中的作用,請參閱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 型別的名稱必須與儲存庫域型別的簡單名稱匹配。如果需要,你可以使用 @GraphQlRepository
的 typeName
屬性來指定目標 GraphQL 型別名稱。
對於分頁查詢,儲存庫域型別的簡單名稱必須與 Connection
型別名稱匹配,但不包含 Connection
結尾(例如,Book
匹配 BooksConnection
)。對於自動註冊,分頁是基於偏移量的,每頁 20 個專案。
自動註冊是透過一個內建的 RuntimeWiringConfigurer
執行的,該配置器可以從 QueryByExampleDataFetcher
獲取。Boot Starter 會自動檢測 @GraphQlRepository
bean,並使用它們來初始化 RuntimeWiringConfigurer
。
如果你的儲存庫分別實現了 QueryByExampleBuilderCustomizer
或 ReactiveQueryByExampleBuilderCustomizer
,自動註冊會透過呼叫儲存庫例項上的 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 結果返回之前對其應用轉換。有時候只需要前者,預設情況下,Querydsl 和 Query by Example 整合將 GraphQL selection set 轉換為屬性路徑提示,底層 Spring Data 模組使用這些提示來限制選擇。
在其他情況下,為了適應 GraphQL schema,減少甚至轉換底層資料模型會很有用。Spring Data 透過介面和 DTO 投影支援這一點。
介面投影定義了要暴露的固定屬性集,屬性可能為 null
也可能不為 null
,具體取決於資料儲存查詢結果。有兩種型別的介面投影,它們都決定從底層資料來源載入哪些屬性
DTO 投影提供了更高層次的自定義,因為你可以將轉換程式碼放在建構函式或 getter 方法中。
DTO 投影從查詢中例項化,其各個屬性由投影本身決定。DTO 投影通常與全引數建構函式(例如 Java 記錄)一起使用,因此只有當所有必需的欄位(或列)都是資料庫查詢結果的一部分時才能構建它們。
滾動 (Scroll)
如分頁中所解釋,GraphQL Cursor Connection spec 定義了一個使用 Connection
、Edge
和 PageInfo
schema 型別的分頁機制,而 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。
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 中,則會對 Date
、Calendar
和任何 java.time
中的型別應用上述自定義。