請求執行

ExecutionGraphQlService 是 Spring 呼叫 GraphQL Java 執行請求的主要抽象。底層傳輸,例如 HTTP,將請求處理委託給 ExecutionGraphQlService

主要實現 DefaultExecutionGraphQlService 配置了一個 GraphQlSource,用於訪問要呼叫的 graphql.GraphQL 例項。

GraphQLSource

GraphQlSource 是一種公開要使用的 graphql.GraphQL 例項的契約,它還包括一個用於構建該例項的構建器 API。預設構建器可透過 GraphQlSource.schemaResourceBuilder() 獲取。

Boot Starter 建立此構建器的一個例項,並進一步初始化它以從可配置位置 載入模式檔案,以 公開屬性 以應用於 GraphQlSource.Builder,以檢測 RuntimeWiringConfigurer bean,用於 GraphQL 指標Instrumentation bean,以及用於 異常解析DataFetcherExceptionResolverSubscriptionExceptionResolver bean。如需進一步自定義,您還可以宣告一個 GraphQlSourceBuilderCustomizer bean,例如:

import org.springframework.boot.graphql.autoconfigure.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()));
	}

}

模式建立

預設情況下,GraphQlSource.Builder 使用 GraphQL Java 的 SchemaGenerator 來建立 graphql.schema.GraphQLSchema。這適用於典型用途,但如果您需要使用不同的生成器,您可以註冊一個 schemaFactory 回撥

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
		.configureRuntimeWiring(..)
		.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
			// create GraphQLSchema
		})

請參閱 GraphQlSource 部分,瞭解如何使用 Spring Boot 進行配置。

如果對聯合感興趣,請參閱 聯合 部分。

RuntimeWiringConfigurer

RuntimeWiringConfigurer 對於註冊以下內容很有用

  • 自定義標量型別。

  • 處理 指令 的程式碼。

  • 直接 DataFetcher 註冊。

  • 等等…​

Spring 應用程式通常不需要執行直接的 DataFetcher 註冊。相反,控制器方法透過 AnnotatedControllerConfigurer(它是一個 RuntimeWiringConfigurer)註冊為 DataFetcher
GraphQL Java 伺服器應用程式僅使用 Jackson 進行資料對映的序列化和反序列化。客戶端輸入被解析為對映。伺服器輸出根據欄位選擇集組裝成對映。這意味著您不能依賴 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,例如進行考慮模式定義的註冊,請實現接受 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 文件中的 模式指令

在 Spring GraphQL 中,您可以透過 RuntimeWiringConfigurer 註冊 SchemaDirectiveWiringBoot Starter 會檢測此類 bean,因此您可能會有類似以下內容:

@Configuration
public class GraphQlConfig {

	 @Bean
	 public RuntimeWiringConfigurer runtimeWiringConfigurer() {
		  return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
	 }

}
有關指令支援的示例,請檢視 Graphql Java 擴充套件驗證 庫。

ExecutionStrategy

GraphQL Java 中的 ExecutionStrategy 驅動所請求欄位的獲取。要建立 ExecutionStrategy,您需要提供一個 DataFetcherExceptionHandler。預設情況下,Spring for GraphQL 建立要使用的異常處理器(如 異常 中所述)並將其設定在 GraphQL.Builder 上。然後,GraphQL Java 使用它建立配置了異常處理器的 AsyncExecutionStrategy 例項。

如果您需要建立自定義的 ExecutionStrategy,您可以以相同的方式檢測 DataFetcherExceptionResolver 並建立異常處理器,並用它來建立自定義的 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));
}

模式轉換

如果您希望在模式建立後遍歷並轉換模式並進行更改,可以透過 builder.schemaResources(..).typeVisitorsToTransformSchema(..) 註冊 graphql.schema.GraphQLTypeVisitor。請記住,這比 模式遍歷 更昂貴,因此除非您需要進行模式更改,否則通常更傾向於遍歷而不是轉換。

模式遍歷

如果您希望在模式建立後遍歷模式,並可能對 GraphQLCodeRegistry 應用更改,可以透過 builder.schemaResources(..).typeVisitors(..) 註冊 graphql.schema.GraphQLTypeVisitor。但是請記住,這樣的訪問者不能更改模式。如果您需要對模式進行更改,請參閱 模式轉換

模式對映檢查

如果查詢、變異或訂閱操作沒有 DataFetcher,它將不會返回任何資料,也不會執行任何有用的操作。同樣,模式型別的欄位既沒有透過 DataFetcher 註冊明確覆蓋,也沒有透過查詢匹配 Class 屬性的預設 PropertyDataFetcher 隱式覆蓋,將始終為 null

GraphQL Java 不會執行檢查以確保每個模式欄位都已覆蓋,作為較低級別的庫,GraphQL Java 根本不知道 DataFetcher 可以返回什麼或它依賴於什麼引數,因此無法執行此類驗證。這可能會導致漏洞,根據測試覆蓋率,這些漏洞可能直到執行時才被發現,屆時客戶端可能會遇到“靜默” null 值或非空欄位錯誤。

Spring for GraphQL 中的 SelfDescribingDataFetcher 介面允許 DataFetcher 暴露返回型別和預期引數等資訊。所有內建的 Spring DataFetcher 實現,用於 控制器方法,用於 Querydsl 和用於 Query by Example,都是此介面的實現。對於帶註解的控制器,返回型別和預期引數基於控制器方法簽名。這使得在啟動時檢查模式對映成為可能,以確保以下幾點:

  • 模式欄位具有 DataFetcher 註冊或相應的 Class 屬性。

  • DataFetcher 註冊引用存在的模式欄位。

  • DataFetcher 引數具有匹配的模式欄位引數。

如果應用程式使用 Kotlin 編寫,或者使用 Null-safety 註解,則可以執行進一步的檢查。GraphQL 模式可以宣告可空型別(Book)和非可空型別(Book!)。因此,我們可以確保應用程式不會違反模式的非空要求。

當模式欄位為非空時,我們確保相關的 Class 屬性和 DataFetcher 返回型別也為非空。相反的情況不被視為錯誤:當模式具有可空欄位 author: Author 且應用程式宣告 @NonNull Author getAuthor(); 時,檢查器不會將其報告為錯誤。應用程式不一定應該在模式中將欄位設定為非空,因為資料獲取操作期間的任何錯誤都會強制 GraphQL 引擎將層次結構中的欄位設定為 null,直到允許 null 為止。部分響應是 GraphQL 的一個關鍵功能,因此在設計模式時應考慮 nullness。

當欄位引數可空時,我們確保 DataFetcher 引數也是可空的。在這種情況下,如果使用者輸入違反了可空性契約,則不應將其提供給應用程式,因為這會導致執行時失敗。

要啟用模式檢查,請按如下所示自定義 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)
    Field nullness errors: {Book=[title is NON_NULL -> 'Book#title' is NULLABLE]} (4)
    Argument nullness errors: {BookController#bookById[1 args]=[java.lang.String id should be NULLABLE]} (5)
    Skipped types: [BookOrAuthor] (6)
1 未以任何方式覆蓋的模式欄位
2 對不存在欄位的 DataFetcher 註冊
3 不存在的 DataFetcher 預期引數
4 “title”模式欄位為非空,但 Book.getTitle()@Nullable
5 bookById(id: ID) 有一個可空的“id”引數,但 Book bookById(@NonNull String id) 為非空。
6 已跳過的模式型別(接下來解釋)

在某些情況下,模式型別的 Class 型別是未知的。可能是 DataFetcher 沒有實現 SelfDescribingDataFetcher,或者宣告的返回型別過於通用(例如 Object)或未知(例如 List<?>),或者 DataFetcher 可能完全缺失。在這種情況下,模式型別被列為已跳過,因為它無法驗證。對於每個已跳過的型別,都會有一條 DEBUG 訊息解釋其跳過原因。

聯合型別和介面

對於聯合型別,檢查會遍歷成員型別並嘗試查詢相應的類。對於介面,檢查會遍歷實現型別並查詢相應的類。

預設情況下,在以下情況下可以開箱即用地檢測到相應的 Java 類:

  • Class 的簡單名稱與 GraphQL 聯合成員或介面實現型別名稱匹配,並且 Class 位於與對映到聯合或介面欄位的控制器方法的返回型別或控制器類相同的包中。

  • 在模式的其他部分中檢查 Class,其中對映欄位是具體的聯合成員或介面實現型別。

  • 您已註冊了一個具有顯式 Class 到 GraphQL 型別對映的 TypeResolver

如果以上方法均無效,並且 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,它允許資料獲取器返回 CompletionStage 並併發執行而不是序列執行。

Java 21 和虛擬執行緒增加了有效使用更多執行緒的重要能力,但為了讓請求執行更快完成,仍然需要併發執行而不是序列執行。

Spring for GraphQL 支援

  • 響應式資料獲取器,它們被轉換為 AsyncExecutionStrategy 預期的 CompletionStage

  • CompletionStage 作為返回值。

  • Kotlin 協程方法作為控制器方法。

  • @SchemaMapping@BatchMapping 方法可以返回 Callable,該 Callable 將提交給 Executor,例如 Spring Framework 的 VirtualThreadTaskExecutor。要啟用此功能,您必須在 AnnotatedControllerConfigurer 上配置一個 Executor

Spring for GraphQL 在 Spring MVC 或 WebFlux 上作為傳輸執行。Spring MVC 使用非同步請求執行,除非在 GraphQL Java 引擎返回後 CompletableFuture 立即完成,如果請求足夠簡單且不需要非同步資料獲取,則會發生這種情況。

GraphQL 請求超時

GraphQL 客戶端可以傳送在伺服器端消耗大量資源的請求。有許多方法可以防止這種情況,其中之一是配置請求超時。這可以確保如果響應需要太長時間才能實現,則伺服器端會關閉請求。

Spring for GraphQL 為 Web 傳輸提供了 TimeoutWebGraphQlInterceptor。應用程式可以使用超時持續時間配置此攔截器;如果請求超時,伺服器將返回帶有特定 HTTP 狀態的錯誤。在這種情況下,攔截器將向上遊傳送“取消”訊號,響應式資料獲取器將自動取消任何正在進行的工作。

此攔截器可以在 WebGraphQlHandler 上配置

TimeoutWebGraphQlInterceptor timeoutInterceptor = new TimeoutWebGraphQlInterceptor(Duration.ofSeconds(5));
WebGraphQlHandler webGraphQlHandler = WebGraphQlHandler
		.builder(executionGraphQlService)
		.interceptor(timeoutInterceptor)
		.build();
GraphQlHttpHandler httpHandler = new GraphQlHttpHandler(webGraphQlHandler);

在 Spring Boot 應用程式中,將攔截器作為 bean 貢獻就足夠了

import java.time.Duration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.server.TimeoutWebGraphQlInterceptor;

@Configuration(proxyBeanMethods = false)
public class HttpTimeoutConfiguration {

	@Bean
	public TimeoutWebGraphQlInterceptor timeoutWebGraphQlInterceptor() {
		return new TimeoutWebGraphQlInterceptor(Duration.ofSeconds(5));
	}

}

對於更多傳輸特定的超時,處理程式實現(如 GraphQlWebSocketHandlerGraphQlSseHandler)上有一些專門的屬性。

響應式 DataFetcher

預設的 GraphQlSource 構建器支援 DataFetcher 返回 MonoFlux,這些會被轉換為 CompletableFuture,其中 Flux 值會被聚合並轉換為 List,除非請求是 GraphQL 訂閱請求,在這種情況下,返回值仍然是用於流式傳輸 GraphQL 響應的 Reactive Streams Publisher

響應式 DataFetcher 可以依賴於從傳輸層(例如 WebFlux 請求處理)傳播的 Reactor 上下文,請參見 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

DataFetcher 和 GraphQL Java 呼叫的其他元件可能並不總是在與 Spring MVC 處理器相同的執行緒上執行,例如,如果非同步 WebGraphQlInterceptorDataFetcher 切換到不同的執行緒。

Spring for GraphQL 支援將 ThreadLocal 值從 Servlet 容器執行緒傳播到 DataFetcher 和 GraphQL Java 呼叫的其他元件執行的執行緒。為此,應用程式需要為感興趣的 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 例項(可透過 io.micrometer.context.ContextRegistry#getInstance() 訪問)手動註冊 ThreadLocalAccessor。您還可以透過 java.util.ServiceLoader 機制自動註冊它。

WebFlux

響應式 DataFetcher 可以依賴於從 WebFlux 請求處理鏈派生的 Reactor 上下文。這包括由 WebGraphQlInterceptor 元件新增的 Reactor 上下文。

異常

在 GraphQL Java 中,DataFetcherExceptionHandler 決定如何將資料獲取中的異常表示為響應的“errors”部分。應用程式只能註冊一個處理器。

Spring for GraphQL 註冊一個 DataFetcherExceptionHandler,它提供預設處理並啟用 DataFetcherExceptionResolver 契約。應用程式可以透過 GraphQLSource 構建器註冊任意數量的解析器,這些解析器將按順序執行,直到其中一個將 Exception 解析為 List<graphql.GraphQLError>。Spring Boot 啟動器會檢測此型別的 bean。

DataFetcherExceptionResolverAdapter 是一個方便的基類,包含受保護的方法 resolveToSingleErrorresolveToMultipleErrors

帶註解的控制器 程式設計模型支援使用帶註解的異常處理方法處理資料獲取異常,這些方法具有靈活的方法簽名,詳情請參見 @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 引擎在解析請求時可能會遇到驗證或其他錯誤,進而阻止請求執行。在這種情況下,響應包含一個“data”鍵,其值為 null,以及一個或多個全域性的請求級別“errors”,即沒有欄位路徑的錯誤。

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

訂閱異常

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

DataFetcherExceptionResolver 無法解決訂閱 Publisher 中的錯誤,因為資料 DataFetcher 僅在最初建立 Publisher。之後,傳輸訂閱 Publisher,然後 Publisher 可能會因錯誤而完成。

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

分頁

GraphQL 游標連線規範 定義了一種透過一次返回專案子集來導航大型結果集的方法,其中每個專案都與一個游標配對,客戶端可以使用該游標請求引用專案之前或之後的更多專案。

該規範將此模式稱為“Connections”,並且名稱以 ~Connection 結尾的模式型別是表示分頁結果集的連線型別。所有連線型別都包含一個名為“edges”的欄位,其中 ~Edge 型別包含實際的專案、游標以及一個名為“pageInfo”的欄位,該欄位指示是否存在更多向前和向後專案。

連線型別

連線型別需要樣板定義,如果未明確宣告,Spring for GraphQL 的 ConnectionTypeDefinitionConfigurer 可以在啟動時透明地新增它們。這意味著您只需要以下內容,連線和邊緣型別就會為您新增

type 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

除了模式中的 連線型別 之外,您還需要等效的 Java 型別。GraphQL Java 提供了這些,包括泛型 ConnectionEdge 型別以及 PageInfo

您可以從控制器方法返回 Connection,但這需要樣板程式碼來將您的底層資料分頁機制適應 Connection,建立游標,新增 ~Edge 包裝器,並建立 PageInfo

Spring for GraphQL 定義了 ConnectionAdapter 契約,用於將專案容器適配到 Connection。介面卡由 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 是一種契約,用於編碼和解碼引用大型結果集中專案位置的字串游標。游標可以基於索引或鍵集。

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

CursorEncoder 是一個相關的契約,它進一步編碼和解碼字元串游標,使其對客戶端不透明。EncodingCursorStrategyCursorStrategyCursorEncoder 結合起來。您可以使用 Base64CursorEncoderNoOpEncoder 或建立自己的。

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

排序

在 GraphQL 請求中提供排序資訊沒有標準方法。但是,分頁依賴於穩定的排序順序。您可以使用預設順序,或者公開輸入型別並從 GraphQL 引數中提取排序詳細資訊。

作為控制器方法引數,對 Spring Data 的 Sort內建支援。為此,您需要一個 SortStrategy bean。

批次載入

給定一本 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,並確保 上下文傳播 給它們。這就是為什麼應用程式預計會使用它。可以直接執行您自己的 DataLoader 註冊,但此類註冊將放棄上述好處。

批次載入技巧

對於直接的情況,@BatchMapping 註解通常是最佳選擇,樣板程式碼最少。對於更高階的用例,BatchLoaderRegistry 提供了更大的靈活性。

如上所述DataLoader 將對 load() 呼叫進行排隊,並可能一次性分派所有呼叫,或分批分派。這意味著一次分派可以為不同的 @SchemaMapping 呼叫和不同的 GraphQL 上下文載入實體。由於載入的實體將由 GraphQL Java 根據其鍵在請求的整個生命週期中進行快取,因此開發人員應考慮不同的策略來最佳化記憶體消耗與 I/O 呼叫次數。

在下一節中,我們將考慮以下模式來載入有關朋友的資訊。請注意,我們可以篩選朋友,只加載具有特定最喜歡飲料的朋友。

type Query {
    me: Person
    people: [Person]
}

input FriendsFilter {
    favoriteBeverage: String
}

type Person {
    id: ID!
    name: String
    favoriteBeverage: String
    friends(filter: FriendsFilter): [Person]
}

我們可以透過首先在 DataLoader 中載入給定人物的所有朋友,然後在 @SchemaMapping 級別過濾掉不必要的朋友來解決此問題。這將在 DataLoader 快取中載入更多 Person 例項並使用更多記憶體,但可能會執行更少的 I/O 呼叫。

	public FriendsControllerFiltering(BatchLoaderRegistry registry) {
		registry.forTypePair(Integer.class, Person.class).registerMappedBatchLoader((personIds, env) -> {
			Map<Integer, Person> friends = new HashMap<>();
			personIds.forEach((personId) -> friends.put(personId, this.people.get(personId))); (1)
			return Mono.just(friends);
		});
	}

	@QueryMapping
	public Person me() {
		return ...
	}

	@QueryMapping
	public Collection<Person> people() {
		return ...
	}

	@SchemaMapping
	public CompletableFuture<List<Person>> friends(Person person, @Argument FriendsFilter filter, DataLoader<Integer, Person> dataLoader) {
		return dataLoader
				.loadMany(person.friendsId())
				.thenApply(filter::apply); (2)
	}

	public record FriendsFilter(String favoriteBeverage) {

		List<Person> apply(List<Person> friends) {
			return friends.stream()
					.filter((person) -> person.favoriteBeverage.equals(this.favoriteBeverage))
					.toList();
		}
	}
1 獲取所有朋友,不應用過濾器,按 ID 快取 Person
2 載入所有朋友,然後應用給定的過濾器

這非常適合少量連線緊密的朋友和受歡迎的飲料。如果相反,我們處理的是大量朋友但共同朋友很少,或者更小眾的飲料,我們可能會面臨為客戶端實際傳送的少數條目在記憶體中載入大量資料的風險。

在這裡,我們可以使用不同的策略,透過批次載入帶有複合鍵的實體:人員和選擇的過濾器。這種方法只會在記憶體中載入足夠的實體,代價是快取中可能存在重複的 Person 和更多的 I/O 操作。

	public FriendsControllerComposedKey(BatchLoaderRegistry registry) {
		registry.forTypePair(FriendFilterKey.class, Person[].class).registerMappedBatchLoader((keys, env) -> {
			return dataStore.load(keys);
			Map<FriendFilterKey, Person[]> result = new HashMap<>();
			keys.forEach((key) -> { (2)
				Person[] friends = key.person().friendsId().stream()
						.map(this.people::get)
						.filter((friend) -> key.friendsFilter().matches(friend))
						.toArray(Person[]::new);
				result.put(key, friends);
			});
			return Mono.just(result);
		});
	}

	@QueryMapping
	public Person me() {
		return ...
	}

	@QueryMapping
	public Collection<Person> people() {
		return ...
	}

	@SchemaMapping
	public CompletableFuture<Person[]> friends(Person person, @Argument FriendsFilter filter, DataLoader<FriendFilterKey, Person[]> dataLoader) {
		return dataLoader.load(new FriendFilterKey(person, filter));
	}

	public record FriendsFilter(String favoriteBeverage) {
		boolean matches(Person friend) {
			return friend.favoriteBeverage.equals(this.favoriteBeverage);
		}
	}

	public record FriendFilterKey(Person person, FriendsFilter friendsFilter) { (1)
	}
1 因為這個鍵包含人物和過濾器,所以我們需要多次獲取同一個朋友

在這兩種情況下,查詢

query {
    me {
        name
        friends(filter: {favoriteBeverage: "tea"}) {
            name
            favoriteBeverage
        }
    }
    people {
        name
        friends(filter: {favoriteBeverage: "coffee"}) {
            name
            favoriteBeverage
        }
    }
}

將產生以下結果

{
  "data": {
    "me": {
      "name": "Brian",
      "friends": [
        {
          "name": "Donna",
          "favoriteBeverage": "tea"
        }
      ]
    },
    "people": [
      {
        "name": "Andi",
        "friends": [
          {
            "name": "Rossen",
            "favoriteBeverage": "coffee"
          },
          {
            "name": "Brad",
            "favoriteBeverage": "coffee"
          }
        ]
      },
      {
        "name": "Brad",
        "friends": [
          {
            "name": "Rossen",
            "favoriteBeverage": "coffee"
          },
          {
            "name": "Andi",
            "favoriteBeverage": "coffee"
          }
        ]
      },
      {
        "name": "Donna",
        "friends": [
          {
            "name": "Rossen",
            "favoriteBeverage": "coffee"
          },
          {
            "name": "Brad",
            "favoriteBeverage": "coffee"
          }
        ]
      },
      {
        "name": "Brian",
        "friends": [
          {
            "name": "Rossen",
            "favoriteBeverage": "coffee"
          }
        ]
      },
      {
        "name": "Rossen",
        "friends": []
      }
    ]
  }
}

測試批次載入

首先讓 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("...");
// ...
© . This site is unofficial and not affiliated with VMware.