請求執行
ExecutionGraphQlService
是呼叫 GraphQL Java 執行請求的主要 Spring 抽象。底層傳輸,例如 HTTP,委託給 ExecutionGraphQlService
來處理請求。
主要實現 DefaultExecutionGraphQlService
配置了一個 GraphQlSource
,用於訪問要呼叫的 graphql.GraphQL
例項。
GraphQLSource
GraphQlSource
是一個契約,用於暴露要使用的 graphql.GraphQL
例項,它也包含一個用於構建該例項的構建器 API。預設構建器可透過 GraphQlSource.schemaResourceBuilder()
獲得。
Boot Starter 建立此構建器的一個例項,並進一步初始化它,以便從可配置位置載入 Schema 檔案,暴露可應用於 GraphQlSource.Builder
的屬性,檢測 RuntimeWiringConfigurer
bean,用於 GraphQL 指標 的 Instrumentation bean,以及用於 異常解析 的 DataFetcherExceptionResolver
和 SubscriptionExceptionResolver
bean。對於進一步的定製,你還可以宣告一個 GraphQlSourceBuilderCustomizer
bean,例如
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl((graphQlBuilder) ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
Schema 資源
GraphQlSource.Builder
可以配置一個或多個 Resource
例項進行解析和合並。這意味著 Schema 檔案幾乎可以從任何位置載入。
預設情況下,Boot starter 在 classpath:graphql/**
位置(通常是 src/main/resources/graphql
)下查詢副檔名為 ".graphqls" 或 ".gqls" 的 Schema 檔案。你也可以使用檔案系統位置,或 Spring Resource
層次結構支援的任何位置,包括一個從遠端位置、儲存或記憶體載入 Schema 檔案的自定義實現。
使用 classpath*:graphql/**/ 來查詢跨多個 classpath 位置(例如跨多個模組)的 Schema 檔案。 |
Schema 建立
預設情況下,GraphQlSource.Builder
使用 GraphQL Java 的 SchemaGenerator
來建立 graphql.schema.GraphQLSchema
。這適用於典型用途,但如果你需要使用不同的生成器,可以註冊一個 schemaFactory
回撥。
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
請參閱 GraphQlSource 部分,瞭解如何使用 Spring Boot 進行配置。
如果對 Federation 感興趣,請參閱聯合部分。
RuntimeWiringConfigurer
RuntimeWiringConfigurer
對於註冊以下內容非常有用:
-
自定義標量型別。
-
處理 指令 的程式碼。
-
直接的
DataFetcher
註冊。 -
以及更多…
Spring 應用通常不需要執行直接的 DataFetcher 註冊。相反,控制器方法會透過 AnnotatedControllerConfigurer (它是一個 RuntimeWiringConfigurer )註冊為 DataFetcher 。 |
GraphQL Java 伺服器應用僅使用 Jackson 進行資料 Map 之間的序列化和反序列化。客戶端輸入被解析為 Map。伺服器輸出根據欄位選擇集組裝成 Map。這意味著你不能依賴 Jackson 的序列化/反序列化註解。相反,你可以使用 自定義標量型別。 |
Boot Starter 會檢測型別為 RuntimeWiringConfigurer
的 bean,並將它們註冊到 GraphQlSource.Builder
中。這意味著在大多數情況下,你的配置會包含以下內容:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {
GraphQLScalarType scalarType = ... ;
SchemaDirectiveWiring directiveWiring = ... ;
return wiringBuilder -> wiringBuilder
.scalar(scalarType)
.directiveWiring(directiveWiring);
}
}
如果你需要新增一個 WiringFactory
(例如,進行考慮 Schema 定義的註冊),請實現接受 RuntimeWiring.Builder
和輸出 List<WiringFactory>
的替代 configure
方法。這允許你新增任意數量的工廠,然後按順序呼叫它們。
TypeResolver
GraphQlSource.Builder
註冊 ClassNameTypeResolver
作為預設的 TypeResolver
,用於那些尚未透過 RuntimeWiringConfigurer
進行此類註冊的 GraphQL 介面和聯合型別。TypeResolver
在 GraphQL Java 中的目的是確定 DataFetcher
為 GraphQL 介面或聯合欄位返回值的 GraphQL 物件型別。
ClassNameTypeResolver
嘗試將值的簡單類名與 GraphQL 物件型別進行匹配,如果失敗,它還會遍歷其超型別,包括基類和介面,查詢匹配項。ClassNameTypeResolver
提供了一個選項來配置名稱提取函式以及 Class
到 GraphQL 物件型別的名稱對映,這有助於覆蓋更多邊緣情況。
GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);
請參閱 GraphQlSource 部分,瞭解如何使用 Spring Boot 進行配置。
指令
GraphQL 語言支援指令,這些指令“描述 GraphQL 文件中的備用執行時執行和型別驗證行為”。指令類似於 Java 中的註解,但它們是在 GraphQL 文件中的型別、欄位、片段和操作上宣告的。
GraphQL Java 提供了 SchemaDirectiveWiring
契約,幫助應用檢測和處理指令。更多詳細資訊,請參閱 GraphQL Java 文件中的Schema 指令。
在 Spring GraphQL 中,你可以透過 RuntimeWiringConfigurer
註冊一個 SchemaDirectiveWiring
。Boot Starter 會檢測此類 bean,因此你的程式碼可能如下所示:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
有關指令支援的示例,請檢視 Extended Validation for Graphql Java 庫。 |
ExecutionStrategy
GraphQL Java 中的 ExecutionStrategy
負責驅動請求欄位的獲取。要建立一個 ExecutionStrategy
,你需要提供一個 DataFetcherExceptionHandler
。預設情況下,Spring for GraphQL 會建立用於異常處理的 handler(如 異常 中所述),並將其設定在 GraphQL.Builder
上。然後 GraphQL Java 使用該 handler 建立配置好的 AsyncExecutionStrategy
例項。
如果你需要建立一個自定義的 ExecutionStrategy
,可以檢測 DataFetcherExceptionResolver
並以相同方式建立一個異常 handler,然後使用它來建立自定義的 ExecutionStrategy
。例如,在 Spring Boot 應用中:
@Bean
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer(
ObjectProvider<DataFetcherExceptionResolver> resolvers) {
DataFetcherExceptionHandler exceptionHandler =
DataFetcherExceptionResolver.createExceptionHandler(resolvers.stream().toList());
AsyncExecutionStrategy strategy = new CustomAsyncExecutionStrategy(exceptionHandler);
return sourceBuilder -> sourceBuilder.configureGraphQl(builder ->
builder.queryExecutionStrategy(strategy).mutationExecutionStrategy(strategy));
}
Schema 轉換
如果你想在 Schema 建立後遍歷並轉換 Schema,並對 Schema 進行更改,可以透過 builder.schemaResources(..).typeVisitorsToTransformSchema(..)
註冊一個 graphql.schema.GraphQLTypeVisitor
。請記住,這比 Schema 遍歷 的成本更高,因此除非需要更改 Schema,否則通常更傾向於遍歷而非轉換。
Schema 遍歷
如果你想在 Schema 建立後遍歷 Schema,並可能對 GraphQLCodeRegistry
進行更改,可以透過 builder.schemaResources(..).typeVisitors(..)
註冊一個 graphql.schema.GraphQLTypeVisitor
。然而,請記住,這樣的訪問者不能更改 Schema。如果你需要更改 Schema,請參閱 Schema 轉換。
Schema 對映檢查
如果一個查詢、變異或訂閱操作沒有 DataFetcher
,它將不會返回任何資料,也不會執行任何有用的操作。同樣,那些既沒有透過 DataFetcher
註冊顯式覆蓋,也沒有被預設的查詢匹配 Class
屬性的 PropertyDataFetcher
隱式覆蓋的 Schema 型別欄位,將始終為 null
。
GraphQL Java 不會執行檢查以確保覆蓋了所有 Schema 欄位。作為一個較低層的庫,GraphQL Java 根本不知道 DataFetcher
能返回什麼或依賴於哪些引數,因此無法執行此類驗證。這可能導致一些缺陷,這些缺陷可能直到執行時客戶端遇到“靜默”的 null
值或非 null 欄位錯誤時才會被發現,具體取決於測試覆蓋率。
Spring for GraphQL 中的 SelfDescribingDataFetcher
介面允許 DataFetcher
暴露諸如返回型別和期望引數之類的資訊。所有內建的 Spring DataFetcher
實現,包括用於 控制器方法、用於 Querydsl 和用於 Query by Example 的實現,都實現了此介面。對於註解的控制器,返回型別和期望引數基於控制器方法的簽名。這使得在啟動時檢查 Schema 對映成為可能,以確保以下事項:
-
Schema 欄位要麼有一個
DataFetcher
註冊,要麼有一個對應的Class
屬性。 -
DataFetcher
註冊引用一個存在的 Schema 欄位。 -
DataFetcher
引數具有匹配的 Schema 欄位引數。
要啟用 Schema 檢查,請按如下所示定製 GraphQlSource.Builder
。在此示例中,報告僅被記錄日誌,但你可以選擇採取任何行動。
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(report -> {
logger.debug(report);
});
報告示例
GraphQL schema inspection: Unmapped fields: {Book=[title], Author[firstName, lastName]} (1) Unmapped registrations: {Book.reviews=BookController#reviews[1 args]} (2) Unmapped arguments: {BookController#bookSearch[1 args]=[myAuthor]} (3) Skipped types: [BookOrAuthor] (4)
1 | 未以任何方式覆蓋的 Schema 欄位 |
2 | 指向不存在欄位的 DataFetcher 註冊 |
3 | 不存在的 DataFetcher 期望引數 |
4 | 已跳過的 Schema 型別(後續解釋) |
在某些情況下,Schema 型別的 Class
型別未知。可能是 DataFetcher
沒有實現 SelfDescribingDataFetcher
,或者宣告的返回型別過於通用(例如 Object
)或未知(例如 List<?>
),或者完全缺少 DataFetcher
。在這種情況下,Schema 型別會被列為跳過,因為它無法被驗證。對於每個跳過的型別,都會有一條 DEBUG 訊息解釋跳過原因。
聯合型別和介面
對於聯合型別,檢查會遍歷成員型別並嘗試查詢相應的類。對於介面,檢查會遍歷實現型別並查詢相應的類。
預設情況下,可以在以下情況中即開即用地檢測到相應的 Java 類:
-
Class
的簡單名稱與 GraphQL 聯合成員或介面實現型別的名稱匹配,並且該Class
位於與對映到聯合或介面欄位的控制器方法或控制器類的返回型別相同的包中。 -
在 Schema 的其他部分中檢查了
Class
,其中對映的欄位是具體的聯合成員或介面實現型別。 -
你已註冊了一個具有顯式
Class
到 GraphQL 型別對映的 TypeResolver。
如果上述方法均無效,並且 Schema 檢查報告中顯示 GraphQL 型別被跳過,你可以進行以下定製:
-
顯式地將 GraphQL 型別名稱對映到一個或多個 Java 類。
-
配置一個函式,定製如何將 GraphQL 型別名稱適配為簡單的
Class
名稱。這有助於處理特定的 Java 類命名約定。 -
提供一個
ClassNameTypeResolver
將 GraphQL 型別對映到 Java 類。
例如:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(
initializer -> initializer.classMapping("Author", Author.class)
logger::debug);
操作快取
GraphQL Java 在執行操作之前必須進行 解析 和 驗證。這可能會顯著影響效能。為了避免重複解析和驗證,應用可以配置一個 PreparsedDocumentProvider
來快取和重用 Document 例項。GraphQL Java 文件 提供了關於透過 PreparsedDocumentProvider
進行查詢快取的更多詳細資訊。
在 Spring GraphQL 中,你可以透過 GraphQlSource.Builder#configureGraphQl
註冊一個 PreparsedDocumentProvider
:。
// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...
// Create provider
PreparsedDocumentProvider provider =
new ApolloPersistedQuerySupport(new InMemoryPersistedQueryCache(Collections.emptyMap()));
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
請參閱 GraphQlSource 部分,瞭解如何使用 Spring Boot 進行配置。
執行緒模型
大多數 GraphQL 請求受益於併發執行巢狀欄位的獲取。這就是為什麼當今大多數應用都依賴於 GraphQL Java 的 AsyncExecutionStrategy
,它允許資料 fetcher 返回 CompletionStage
並併發執行,而不是序列執行。
Java 21 和虛擬執行緒增加了高效使用更多執行緒的重要能力,但為了讓請求執行更快完成,仍然需要併發執行而不是序列執行。
Spring for GraphQL 支援:
-
響應式資料 Fetcher,它們會被適配為
CompletionStage
,符合AsyncExecutionStrategy
的預期。 -
將
CompletionStage
作為返回值。 -
Kotlin 協程的控制器方法。
-
@SchemaMapping 和 @BatchMapping 方法可以返回
Callable
,它會被提交到一個Executor
(例如 Spring Framework 的VirtualThreadTaskExecutor
)中執行。要啟用此功能,必須在AnnotatedControllerConfigurer
上配置一個Executor
。
Spring for GraphQL 可以執行在 Spring MVC 或 WebFlux 之上作為傳輸層。Spring MVC 使用非同步請求執行,除非 GraphQL Java 引擎返回後,生成的 CompletableFuture
立即完成——如果請求足夠簡單且不需要非同步資料獲取,就會發生這種情況。
響應式 DataFetcher
預設的 GraphQlSource
構建器支援 DataFetcher
返回 Mono
或 Flux
,這些會被適配為 CompletableFuture
,其中 Flux
的值會被聚合並轉換為 List,除非請求是 GraphQL 訂閱請求,在這種情況下,返回值仍然是用於流式傳輸 GraphQL 響應的 Reactive Streams Publisher
。
一個響應式 DataFetcher
可以依賴於從傳輸層傳播的 Reactor 上下文,例如來自 WebFlux 請求處理的上下文,詳見 WebFlux 上下文。
在訂閱請求的情況下,GraphQL Java 會在專案可用且所有請求欄位獲取完畢後立即產生專案。由於這涉及多層非同步資料獲取,專案可能會亂序傳送。如果你希望 GraphQL Java 緩衝專案並保持原始順序,可以透過在 GraphQLContext
中設定 SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED
配置標誌來實現。例如,可以使用自定義的 Instrumentation
來完成此操作。
import graphql.ExecutionResult;
import graphql.execution.SubscriptionExecutionStrategy;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentationContext;
import graphql.execution.instrumentation.SimplePerformantInstrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class GraphQlConfig {
@Bean
public SubscriptionOrderInstrumentation subscriptionOrderInstrumentation() {
return new SubscriptionOrderInstrumentation();
}
static class SubscriptionOrderInstrumentation extends SimplePerformantInstrumentation {
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
InstrumentationState state) {
// Enable option for keeping subscription results in upstream order
parameters.getGraphQLContext().put(SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED, true);
return SimpleInstrumentationContext.noOp();
}
}
}
上下文傳播
Spring for GraphQL 提供支援,可以透明地從 HTTP 傳輸層,透過 GraphQL Java,將上下文傳播到 DataFetcher
及其呼叫的其他元件。這包括來自 Spring MVC 請求處理執行緒的 ThreadLocal
上下文和來自 WebFlux 處理管道的 Reactor Context
。
WebMvc
由 GraphQL Java 呼叫的 DataFetcher
和其他元件可能並非總是與 Spring MVC handler 在同一執行緒上執行,例如如果非同步 WebGraphQlInterceptor
或 DataFetcher
切換到不同的執行緒。
Spring for GraphQL 支援將 ThreadLocal
值從 Servlet 容器執行緒傳播到由 GraphQL Java 呼叫的 DataFetcher
和其他元件執行所在的執行緒。為此,應用需要針對感興趣的 ThreadLocal
值實現 io.micrometer.context.ThreadLocalAccessor
。
public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {
@Override
public Object key() {
return RequestAttributesAccessor.class.getName();
}
@Override
public RequestAttributes getValue() {
return RequestContextHolder.getRequestAttributes();
}
@Override
public void setValue(RequestAttributes attributes) {
RequestContextHolder.setRequestAttributes(attributes);
}
@Override
public void reset() {
RequestContextHolder.resetRequestAttributes();
}
}
你可以在啟動時透過全域性 ContextRegistry
例項手動註冊一個 ThreadLocalAccessor
,該例項可透過 io.micrometer.context.ContextRegistry#getInstance()
訪問。你也可以透過 java.util.ServiceLoader
機制自動註冊它。
WebFlux
一個 響應式 DataFetcher
可以依賴於來自 WebFlux 請求處理鏈的 Reactor 上下文。這包括由 WebGraphQlInterceptor 元件新增的 Reactor 上下文。
異常
在 GraphQL Java 中,DataFetcherExceptionHandler
決定如何在響應的“errors”部分表示資料獲取中的異常。一個應用只能註冊一個 handler。
Spring for GraphQL 註冊了一個 DataFetcherExceptionHandler
,它提供預設處理並啟用 DataFetcherExceptionResolver
契約。應用可以透過 GraphQLSource
構建器註冊任意數量的 resolver,它們會按順序執行,直到其中一個將 Exception
解析為 List<graphql.GraphQLError>
。Spring Boot starter 會檢測此型別的 bean。
DataFetcherExceptionResolverAdapter
是一個方便的基類,包含受保護的方法 resolveToSingleError
和 resolveToMultipleErrors
。
註解控制器 程式設計模型支援使用帶有靈活方法簽名的註解異常處理方法來處理資料獲取異常,詳見 @GraphQlExceptionHandler
。
GraphQLError
可以根據 GraphQL Java 的 graphql.ErrorClassification
或 Spring GraphQL 的 ErrorType
分配一個類別,後者定義了以下內容:
-
BAD_REQUEST
-
UNAUTHORIZED
-
FORBIDDEN
-
NOT_FOUND
-
INTERNAL_ERROR
如果異常仍未解決,預設情況下,它會被歸類為 INTERNAL_ERROR
,並附帶一條通用訊息,其中包含類別名稱和來自 DataFetchingEnvironment
的 executionId
。此訊息特意設計為不透明,以避免洩露實現細節。應用可以使用 DataFetcherExceptionResolver
定製錯誤詳情。
未解決的異常會以 ERROR 級別記錄日誌,並附帶 executionId
以便與傳送給客戶端的錯誤關聯。已解決的異常會以 DEBUG 級別記錄日誌。
請求異常
GraphQL Java 引擎在解析請求時可能會遇到驗證或其他錯誤,這反過來會阻止請求執行。在這種情況下,響應包含一個帶有 null
的“data”鍵和一個或多個請求級別的“errors”,這些錯誤是全域性性的,即沒有欄位路徑。
DataFetcherExceptionResolver
無法處理此類全域性錯誤,因為它們在執行開始和任何 DataFetcher
被呼叫之前就被丟擲。應用可以使用傳輸層攔截器來檢查和轉換 ExecutionResult
中的錯誤。請參閱 WebGraphQlInterceptor
下的示例。
分頁
GraphQL 的 遊標連線規範 定義了一種透過一次返回專案子集來導航大型結果集的方式,其中每個專案都與一個遊標配對,客戶端可以使用該遊標請求引用專案之前或之後更多的專案。
該規範將這種模式稱為 “連線(Connections)”,名稱以 ~Connection
結尾的 Schema 型別是一種連線型別,表示分頁結果集。所有連線型別都包含一個名為“edges”的欄位,其中 ~Edge
型別包含實際的專案、一個遊標,以及一個名為“pageInfo”的欄位,用於指示在向前和向後方向上是否還有更多專案。
連線型別
連線型別需要樣板定義,如果未顯式宣告,Spring for GraphQL 的 ConnectionTypeDefinitionConfigurer
可以在啟動時透明地新增它們。這意味著你只需要以下內容,連線和邊緣型別就會為你新增:
Query {
books(first:Int, after:String, last:Int, before:String): BookConnection
}
type Book {
id: ID!
title: String!
}
規範中定義的用於向前分頁的 first
和 after
引數允許客戶端請求給定遊標“之後”的“前”N個專案。類似地,用於向後分頁的 last
和 before
引數允許請求給定遊標“之前”的“後”N個專案。
規範不鼓勵同時包含 first 和 last ,並指出分頁結果會變得不明確。在 Spring for GraphQL 中,如果存在 first 或 after ,則 last 和 before 會被忽略。 |
要生成連線型別,請按如下方式配置 ConnectionTypeDefinitionConfigurer
:
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
上述配置將新增以下型別定義:
type BookConnection {
edges: [BookEdge]!
pageInfo: PageInfo!
}
type BookEdge {
node: Book!
cursor: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
Boot Starter 預設註冊 ConnectionTypeDefinitionConfigurer
。
ConnectionAdapter
除了 Schema 中的 連線型別,你還需要等效的 Java 型別。GraphQL Java 提供了這些型別,包括泛型 Connection
和 Edge
型別,以及 PageInfo
。
你可以從控制器方法返回 Connection
,但這需要樣板程式碼來將底層資料分頁機制適配到 Connection
,建立遊標,新增 ~Edge
包裝器,並建立 PageInfo
。
Spring for GraphQL 定義了 ConnectionAdapter
契約,用於將專案容器適配到 Connection
。Adapter 由一個 DataFetcher
裝飾器呼叫,該裝飾器又由 ConnectionFieldTypeVisitor
新增。你可以按如下方式配置它:
ConnectionAdapter adapter = ... ;
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) (1)
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)) (2)
1 | 建立一個包含一個或多個 ConnectionAdapter 的型別訪問者。 |
2 | 註冊型別訪問者。 |
Spring Data 的 Window
和 Slice
有內建的 ConnectionAdapter
。你也可以建立自己的自定義介面卡。ConnectionAdapter
實現依賴於一個 CursorStrategy
來為返回的專案建立遊標。相同的策略也用於支援包含分頁輸入的 Subrange
控制器方法引數。
CursorStrategy
CursorStrategy
是一個契約,用於編碼和解碼引用大型結果集中專案位置的 String 遊標。遊標可以基於索引或鍵集。
一個 ConnectionAdapter
使用它來為返回的專案編碼遊標。註解控制器 方法、Querydsl 倉庫和 Query by Example 倉庫使用它來解碼分頁請求中的遊標,並建立一個 Subrange
。
CursorEncoder
是一個相關的契約,它進一步編碼和解碼 String 遊標,使其對客戶端不透明。EncodingCursorStrategy
結合了 CursorStrategy
和 CursorEncoder
。你可以使用 Base64CursorEncoder
、NoOpEncoder
或建立自己的實現。
Spring Data 的 ScrollPosition
有內建的 CursorStrategy
。當存在 Spring Data 時,Boot Starter 會註冊一個帶有 Base64Encoder
的 CursorStrategy<ScrollPosition>
。
排序
在 GraphQL 請求中提供排序資訊沒有標準方法。然而,分頁依賴於穩定的排序順序。你可以使用預設順序,或者暴露輸入型別並從 GraphQL 引數中提取排序詳情。
作為控制器方法引數,Spring Data 的 Sort
內建了支援。為了使其工作,你需要一個 SortStrategy
bean。
批次載入 (Batch Loading)
給定一個 Book
及其 Author
,我們可以為一個圖書建立一個 DataFetcher
,為它的作者建立另一個。這允許選擇帶或不帶作者的圖書,但這意味著圖書和作者不是一起載入的,當查詢多本圖書時,每本圖書的作者都是單獨載入的,效率尤其低下。這被稱為 N+1 查詢問題。
DataLoader
GraphQL Java 提供了一種 DataLoader
機制,用於批次載入相關實體。你可以在 GraphQL Java 文件中找到完整的詳細資訊。下面是它的工作原理摘要:
-
在
DataLoaderRegistry
中註冊DataLoader
,它們可以根據唯一的鍵載入實體。 -
DataFetcher
可以訪問DataLoader
並使用它們按 ID 載入實體。 -
DataLoader
透過返回 Future 來推遲載入,以便可以在一個批次中完成。 -
DataLoader
維護一個按請求快取的已載入實體,這可以進一步提高效率。
BatchLoaderRegistry
GraphQL Java 中完整的批次載入機制需要實現幾個 BatchLoader
介面之一,然後將它們包裝並註冊為帶有名稱的 DataLoader
到 DataLoaderRegistry
中。
Spring GraphQL 中的 API 略有不同。對於註冊,只有一箇中心的 BatchLoaderRegistry
,它暴露了工廠方法和構建器,用於建立和註冊任意數量的批次載入函式。
@Configuration
public class MyConfig {
public MyConfig(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Mono<Map<Long, Author>
});
// more registrations ...
}
}
Boot Starter 聲明瞭一個 BatchLoaderRegistry
bean,你可以將其注入到你的配置中(如上所示),或注入到任何元件(例如控制器)中,以便註冊批次載入函式。反過來,BatchLoaderRegistry
被注入到 DefaultExecutionGraphQlService
中,它確保了每個請求的 DataLoader
註冊。
預設情況下,DataLoader
的名稱基於目標實體的類名。這使得 @SchemaMapping
方法可以宣告一個帶有泛型型別的 DataLoader 引數,而無需指定名稱。但是,如果需要,可以透過 BatchLoaderRegistry
構建器自定義名稱,以及其他 DataLoaderOptions
。
要全域性配置預設的 DataLoaderOptions
,作為任何註冊的起點,你可以覆蓋 Boot 的 BatchLoaderRegistry
bean,並使用接受 Supplier<DataLoaderOptions>
的 DefaultBatchLoaderRegistry
建構函式。
在許多情況下,載入相關實體時,你可以使用 @BatchMapping 控制器方法,它們是使用 BatchLoaderRegistry
和 DataLoader
的快捷方式和替代方案。
BatchLoaderRegistry
還提供了其他重要的好處。它支援批次載入函式和 @BatchMapping
方法訪問相同的 GraphQLContext
,並確保 上下文傳播 (Context Propagation) 到它們。這就是為什麼應用程式應使用它的原因。直接執行自己的 DataLoader
註冊是可能的,但這樣的註冊會放棄上述好處。
測試批次載入 (Batch Loading)
首先讓 BatchLoaderRegistry
在 DataLoaderRegistry
上執行註冊。
BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...
DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);
現在你可以按如下方式訪問和測試單個 DataLoader
:
DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading
assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...