請求執行

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,以及用於 異常解析DataFetcherExceptionResolverSubscriptionExceptionResolver 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 返回 MonoFlux,這些會被適配為 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 在同一執行緒上執行,例如如果非同步 WebGraphQlInterceptorDataFetcher 切換到不同的執行緒。

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 是一個方便的基類,包含受保護的方法 resolveToSingleErrorresolveToMultipleErrors

註解控制器 程式設計模型支援使用帶有靈活方法簽名的註解異常處理方法來處理資料獲取異常,詳見 @GraphQlExceptionHandler

GraphQLError 可以根據 GraphQL Java 的 graphql.ErrorClassification 或 Spring GraphQL 的 ErrorType 分配一個類別,後者定義了以下內容:

  • BAD_REQUEST

  • UNAUTHORIZED

  • FORBIDDEN

  • NOT_FOUND

  • INTERNAL_ERROR

如果異常仍未解決,預設情況下,它會被歸類為 INTERNAL_ERROR,並附帶一條通用訊息,其中包含類別名稱和來自 DataFetchingEnvironmentexecutionId。此訊息特意設計為不透明,以避免洩露實現細節。應用可以使用 DataFetcherExceptionResolver 定製錯誤詳情。

未解決的異常會以 ERROR 級別記錄日誌,並附帶 executionId 以便與傳送給客戶端的錯誤關聯。已解決的異常會以 DEBUG 級別記錄日誌。

請求異常

GraphQL Java 引擎在解析請求時可能會遇到驗證或其他錯誤,這反過來會阻止請求執行。在這種情況下,響應包含一個帶有 null 的“data”鍵和一個或多個請求級別的“errors”,這些錯誤是全域性性的,即沒有欄位路徑。

DataFetcherExceptionResolver 無法處理此類全域性錯誤,因為它們在執行開始和任何 DataFetcher 被呼叫之前就被丟擲。應用可以使用傳輸層攔截器來檢查和轉換 ExecutionResult 中的錯誤。請參閱 WebGraphQlInterceptor 下的示例。

訂閱異常

訂閱請求的 Publisher 可能會伴隨錯誤訊號完成,在這種情況下,底層傳輸(例如 WebSocket)會發送一個最終的“error”型別訊息,其中包含 GraphQL 錯誤列表。

DataFetcherExceptionResolver 無法解析訂閱 Publisher 的錯誤,因為資料 DataFetcher 最初只建立 Publisher。之後,傳輸層訂閱 Publisher,後者可能會伴隨錯誤完成。

應用可以註冊一個 SubscriptionExceptionResolver,以便解析訂閱 Publisher 的異常,並將其解析為要傳送給客戶端的 GraphQL 錯誤。

分頁

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!
}

規範中定義的用於向前分頁的 firstafter 引數允許客戶端請求給定遊標“之後”的“前”N個專案。類似地,用於向後分頁的 lastbefore 引數允許請求給定遊標“之前”的“後”N個專案。

規範不鼓勵同時包含 firstlast,並指出分頁結果會變得不明確。在 Spring for GraphQL 中,如果存在 firstafter,則 lastbefore 會被忽略。

要生成連線型別,請按如下方式配置 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 提供了這些型別,包括泛型 ConnectionEdge 型別,以及 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 的 WindowSlice內建的 ConnectionAdapter。你也可以建立自己的自定義介面卡。ConnectionAdapter 實現依賴於一個 CursorStrategy 來為返回的專案建立遊標。相同的策略也用於支援包含分頁輸入的 Subrange 控制器方法引數。

CursorStrategy

CursorStrategy 是一個契約,用於編碼和解碼引用大型結果集中專案位置的 String 遊標。遊標可以基於索引或鍵集。

一個 ConnectionAdapter 使用它來為返回的專案編碼遊標。註解控制器 方法、Querydsl 倉庫和 Query by Example 倉庫使用它來解碼分頁請求中的遊標,並建立一個 Subrange

CursorEncoder 是一個相關的契約,它進一步編碼和解碼 String 遊標,使其對客戶端不透明。EncodingCursorStrategy 結合了 CursorStrategyCursorEncoder。你可以使用 Base64CursorEncoderNoOpEncoder 或建立自己的實現。

Spring Data 的 ScrollPosition內建的 CursorStrategy。當存在 Spring Data 時,Boot Starter 會註冊一個帶有 Base64EncoderCursorStrategy<ScrollPosition>

排序

在 GraphQL 請求中提供排序資訊沒有標準方法。然而,分頁依賴於穩定的排序順序。你可以使用預設順序,或者暴露輸入型別並從 GraphQL 引數中提取排序詳情。

作為控制器方法引數,Spring Data 的 Sort 內建了支援。為了使其工作,你需要一個 SortStrategy bean。

批次載入 (Batch Loading)

給定一個 Book 及其 Author,我們可以為一個圖書建立一個 DataFetcher,為它的作者建立另一個。這允許選擇帶或不帶作者的圖書,但這意味著圖書和作者不是一起載入的,當查詢多本圖書時,每本圖書的作者都是單獨載入的,效率尤其低下。這被稱為 N+1 查詢問題。

DataLoader

GraphQL Java 提供了一種 DataLoader 機制,用於批次載入相關實體。你可以在 GraphQL Java 文件中找到完整的詳細資訊。下面是它的工作原理摘要:

  1. DataLoaderRegistry 中註冊 DataLoader,它們可以根據唯一的鍵載入實體。

  2. DataFetcher 可以訪問 DataLoader 並使用它們按 ID 載入實體。

  3. DataLoader 透過返回 Future 來推遲載入,以便可以在一個批次中完成。

  4. DataLoader 維護一個按請求快取的已載入實體,這可以進一步提高效率。

BatchLoaderRegistry

GraphQL Java 中完整的批次載入機制需要實現幾個 BatchLoader 介面之一,然後將它們包裝並註冊為帶有名稱的 DataLoaderDataLoaderRegistry 中。

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 控制器方法,它們是使用 BatchLoaderRegistryDataLoader 的快捷方式和替代方案。

BatchLoaderRegistry 還提供了其他重要的好處。它支援批次載入函式和 @BatchMapping 方法訪問相同的 GraphQLContext,並確保 上下文傳播 (Context Propagation) 到它們。這就是為什麼應用程式應使用它的原因。直接執行自己的 DataLoader 註冊是可能的,但這樣的註冊會放棄上述好處。

測試批次載入 (Batch Loading)

首先讓 BatchLoaderRegistryDataLoaderRegistry 上執行註冊。

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("...");
// ...