请求执行

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.