|
此版本仍在开发中,尚未被认为是稳定的。请使用最新稳定版本 Spring GraphQL 2.0.2! |
请求执行
ExecutionGraphQlService 是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,例如 HTTP,将请求委托给 ExecutionGraphQlService 处理。
The main implementation, DefaultExecutionGraphQlService, is configured with a
GraphQlSource for access to the graphql.GraphQL instance to invoke.
GraphQLSource
GraphQlSource 是一个契约,用于暴露 graphql.GraphQL 实例并使用该实例,还包含了一个构建该实例的 builder API。默认的 builder 可以通过 GraphQlSource.schemaResourceBuilder() 获取。
The Boot Starter 会创建此构建器的实例,并进一步将其初始化为从可配置位置加载模式文件,
以公开属性
应用于GraphQlSource.Builder,检测
RuntimeWiringConfigurer Bean,
为GraphQL 指标提供
Instrumentation Bean,
以及用于异常解析的DataFetcherExceptionResolver和SubscriptionExceptionResolver Bean。
如需进一步自定义,您还可以声明一个GraphQlSourceBuilderCustomizer Bean,例如:
@Configuration(proxyBeanMethods = false)
public class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl((graphQlBuilder) ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
架构资源
GraphQlSource.Builder 可以配置一个或多个 Resource 实例进行解析和合并。这意味着模式文件可以从几乎任何位置加载。
默认情况下,BootStarters会在classpath:graphql/**位置寻找扩展名为".graphqls"或".gqls"的模式文件,通常该位置是src/main/resources/graphql。你也可以使用文件系统位置,或者任何被Spring Resource层次结构支持的位置,包括从远程位置、存储中或内存中加载模式文件。
使用classpath*:graphql/**/来跨多个类路径位置查找模式文件,例如跨多个模块。 |
模式创建
默认情况下,GraphQlSource.Builder 使用 GraphQL Java SchemaGenerator 创建 graphql.schema.GraphQLSchema。这适用于典型用法,但如果您需要使用不同的生成器,可以注册一个 schemaFactory 回调:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
请参阅GraphQL源部分,了解如何使用Spring Boot进行配置。
如果对Federation化感兴趣,请参阅Federation化部分。
RuntimeWiringConfigurer
一个 RuntimeWiringConfigurer 用于注册以下项:
-
自定义标量类型。
-
处理
指令的代码。 -
直接
DataFetcher注册。 -
和更多...
Spring 应用程序通常不需要执行直接的 DataFetcher 注册。
相反,控制器方法是通过 AnnotatedControllerConfigurer 注册为 DataFetchers 的,AnnotatedControllerConfigurer 是一个 RuntimeWiringConfigurer。 |
| GraphQL Java,服务器应用仅使用Jackson进行数据到地图的数据序列化和反序列化。 客户端输入会被解析成一个映射。服务器输出会根据字段选择集组装成一个映射。 这意味着你不能依赖于Jackson的序列化/反序列化注解。 相反,你可以使用自定义标量类型。 |
The 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,例如为了考虑模式定义进行注册,请实现替代的configure方法,该方法接受RuntimeWiring.Builder和输出List<WiringFactory>。这允许你添加任意数量的工厂,并依次调用这些工厂。
TypeResolver
GraphQlSource.Builder 将 ClassNameTypeResolver 注册为默认的 TypeResolver,用于那些尚未通过 RuntimeWiringConfigurer 进行此类注册的 GraphQL 接口和联合类型。在 GraphQL Java 中,TypeResolver 的作用是确定从 GraphQL 接口或联合字段的 DataFetcher 返回的值所对应的 GraphQL 对象类型。
ClassNameTypeResolver 尝试将值的简单类名与 GraphQL 对象类型匹配,如果失败,则会导航到其超类型(包括基类和接口)以寻找匹配。ClassNameTypeResolver 提供了一个配置名称提取函数的选择,并且可以与Class 一起使用GraphQL对象类型的名称映射来帮助覆盖更多边缘情况。
GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);
请参阅GraphQL源部分,了解如何使用Spring Boot进行配置。
指令
GraphQL语言支持描述GraphQL文档中“替代运行时执行和类型验证行为”的指令。这些指令类似于Java中的注解,但在GraphQL文档中声明在类型、字段、片段和操作上。
GraphQL Java 提供了 SchemaDirectiveWiring 合约来帮助应用程序检测和处理指令。更多细节,请参见
Schema Directives 在
GraphQL Java 文档中。
在 Spring GraphQL 中,您可以通过 SchemaDirectiveWiring 注册一个 RuntimeWiringConfigurer。Boot Starter 会检测此类 Bean,因此您可能需要如下配置:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
| 对于指令支持的示例,请参阅 GraphQL Java 扩展验证库。 |
ExecutionStrategy
一个 ExecutionStrategy 在GraphQL Java中驱动请求字段的获取。
要创建一个 ExecutionStrategy,您需要提供一个 DataFetcherExceptionHandler。
默认情况下,Spring for GraphQL会创建异常处理器,并按照
异常处理 中所述使用该处理器设置在
GraphQL.Builder 上。GraphQL Java 然后使用这个配置的异常处理器来创建 AsyncExecutionStrategy 实例。
如果您需要创建一个自定义的ExecutionStrategy,您可以通过检测DataFetcherExceptionResolvers 并以相同的方式创建异常处理器来实现,并使用它来创建自定义的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,以便在创建后遍历和转换模式,并对模式进行更改。请注意,这比模式遍历更昂贵,因此通常除非需要对模式进行更改,否则请优先选择遍历来避免转换。
架构遍历
您可以通过builder.schemaResources(..).typeVisitors(..)注册一个graphql.schema.GraphQLTypeVisitor,以便在模式创建之后遍历该模式,并可能应用更改到GraphQLCodeRegistry。请注意,这样的访问者不能改变模式。如果您需要对模式进行更改,请参阅模式转换。
架构映射检查
如果查询、突变或订阅操作没有DataFetcher,它将不会返回任何数据,并且不会做任何有用的事情。同样地,那些既没有通过DataFetcher注册明确覆盖,也没有通过默认的PropertyDataFetcher隐式匹配找到对应的Class属性的模式类型字段,总是会是null。
GraphQL Java 不会执行检查以确保每个模式字段都被覆盖,作为一个较低级别的库,GraphQL Java 并不知道一个 `0` 可能返回什么内容或它依赖于哪些参数,因此无法进行这样的验证。这可能会导致漏洞,在测试覆盖率不足的情况下,这些漏洞可能直到运行时才会被发现,此时客户端可能会遇到“静默”的 `1` 值错误或非空字段错误。
Spring for GraphQL 中的 SelfDescribingDataFetcher 接口允许 DataFetcher 公开返回类型和预期参数等信息。所有内置的 Spring DataFetcher 实现,包括用于 控制器方法、Querydsl 以及 按示例查询(Query by Example) 的实现,都是该接口的实现。对于注解控制器,返回类型和预期参数基于控制器方法的签名。这使得在启动时检查模式映射成为可能,以确保以下内容:
-
Schema字段要么具有注册值
DataFetcher,要么具有相应的Class属性。 -
DataFetcher注册信息指的是一个存在的模式字段。 -
DataFetcher个参数与模式字段参数匹配。
如果应用程序是用 Kotlin 编写的,或者正在使用 空安全注解,则可以进一步执行检查。GraphQL 模式可以声明可为空类型 (Book) 和不可为空类型 (Book!)。
因此,我们可以确保应用不会违反模式的空性要求。
当模式字段非空时,我们确保相关Class属性和DataFetcher返回类型也非空。相反的情况不被视为错误:当模式包含可选字段author: Author且应用程序声明了@NonNull Author getAuthor();时,检查器不会将此情况视为错误。
应用程序不一定需要在模式中使字段非空,因为在数据获取操作中的任何错误都会迫使GraphQL引擎将层次结构中的字段置为null。部分响应是GraphQL的关键特性,因此在设计模式时应考虑空值问题。
当字段参数可为空时,我们确保0个参数也是可为空的。 在这种情况下,如果用户输入破坏了空值契约,则不应将用户输入传递给应用程序,因为这会导致运行时失败。
要启用模式检查,请自定义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 | 未在任何方式覆盖的Schema字段 |
| 2 | DataFetcher 注册到不存在的字段 |
| 3 | DataFetcher 期望的参数不存在 |
| 4 | "title" schema 字段非空,但 Book.getTitle() 是 @Nullable |
| 5 | bookById(id: ID) 有一个可为空的 "id" 参数,但 Book bookById(@NonNull String id) 不为 null。 |
| 6 | 跳过的模式类型(下文解释) |
在某些情况下,Class 类型对于模式类型是未知的。或许 DataFetcher 未实现 SelfDescribingDataFetcher,或者声明的返回类型过于泛化(例如:Object),或者未知(例如:List<?>),或者 DataFetcher 完全缺失。
在这种情况下,由于无法验证,模式类型将被标记为跳过。对于每个跳过的类型,一条 DEBUG 消息会解释为何跳过它。
联合与接口
对于联合类型,检查会遍历成员类型并尝试找到对应的类。对于接口,检查会遍历实现类型并查找对应的类。
默认情况下,在以下情况下可以检测到相应的Java类:<br>
-
Class的简单名称与 GraphQL 联合成员的接口实现类型名称匹配,并且Class位于与联合或接口字段映射的控制器方法或控制器类的返回类型相同的包中。 -
The
Class在模式的其他部分中检查,其中映射字段是具体的联合成员或接口实现类型。 -
您已经注册了一个 TypeResolver ,它具有显式的从
Class到 GraphQL 类型的映射。
在上述方法均无法解决问题,且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中,您可以注册一个PreparsedDocumentProvider到GraphQlSource.Builder#configureGraphQl:
// 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))
请参阅GraphQL源部分,了解如何使用Spring Boot进行配置。
线程模型
大多数GraphQL请求在获取嵌套字段时可以从并发执行中受益。这就是为什么今天的大多数应用程序都依赖于GraphQL Java的AsyncExecutionStrategy,它允许数据获取器返回CompletionStage并并发执行而不是串行执行。
Java 21 和虚拟线程增加了高效使用更多线程的能力,但仍然需要并发执行而不是顺序执行,以使请求执行更快完成。
Spring for GraphQL 支持:<br/>
-
响应式数据获取器,这些适应了
CompletionStage,这是AsyncExecutionStrategy所期望的。 -
CompletionStage作为返回值。 -
使用 Kotlin 协程方法的 Controller 方法。
-
@SchemaMapping 和 @BatchMapping 方法可以返回
Callable,并将其提交给Executor,例如 Spring Framework 的VirtualThreadTaskExecutor。要启用此功能,您必须在AnnotatedControllerConfigurer上配置一个Executor。
Spring for GraphQL 在 Spring MVC 或 WebFlux 之上运行作为传输方式。Spring MVC 使用异步请求执行,除非GraphQL Java引擎返回后生成的 CompletableFuture 立即完成,在这种情况下,如果请求足够简单且不需要异步数据获取,则会如此。
GraphQL 请求超时
GraphQL客户端可以发送请求,这些请求会在服务器端消耗大量资源。 有多种方法可以保护服务器免受此类请求的影响,其中之一是配置请求超时。 这确保如果响应耗时过长,则在服务器端关闭请求。
Spring for GraphQL 提供了一个 TimeoutWebGraphQlInterceptor 用于 web 传输。
应用程序可以配置此拦截器以设置超时时间;如果请求超时,服务器将以特定的 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));
}
}
对于特定传输的超时设置,可以在处理器实现中找到专门的属性,例如GraphQlWebSocketHandler和GraphQlSseHandler。
响应式DataFetcher
The default GraphQlSource builder 启用支持从DataFetcher返回Mono或Flux,这些会被适配为一个CompletableFuture,其中Flux值被聚合并转换成一个 List,除非请求是 GraphQL 订阅请求,在这种情况下,返回值保持为用于流式处理 GraphQL 响应的 Reactive Streams Publisher。
一个响应式的DataFetcher可以依赖于从传输层传播而来的Reactor上下文,例如来自WebFlux请求处理,请参阅
WebFlux 上下文。
在订阅请求的情况下,GraphQL Java 将会一有可用数据并且所有请求的字段都获取到之后就生成项目。由于这涉及多层异步数据获取,因此项目可能以不同于原始顺序的方式发送。如果你希望 GraphQL Java 缓存项目并保留原始顺序,可以通过设置 SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED 配置标志在 GraphQLContext 中实现。例如,你可以通过自定义 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。
Web MVC
A DataFetcher 以及其他由 GraphQL Java 调用的组件可能不会始终在与 Spring MVC 处理器相同的线程上执行,例如,如果异步
WebGraphQlInterceptor 或 DataFetcher 切换到不同的线程。
Spring for GraphQL 支持从 Servlet 容器线程向由 GraphQL Java 调用并执行的DataFetcher和其他组件传递ThreadLocal值。为此,应用程序需要为感兴趣的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"部分表示数据获取时产生的异常。一个应用程序只能注册一个处理程序。
Spring for GraphQL 注册了一个 DataFetcherExceptionHandler,提供默认处理并启用 DataFetcherExceptionResolver 契约。应用程序可以通过 GraphQLSource 构建器注册任意数量的解析器,这些解析器按顺序执行,直到其中一个将 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 级别记录。
请求异常
The GraphQL Java引擎在解析请求时可能会遇到验证或其他错误,这会导致请求执行被阻止。在这种情况下,响应包含一个"data"键以及一个或多个全局的"errors"(即没有字段路径),这些错误是请求级别的。
DataFetcherExceptionResolver 无法处理此类全局错误,因为这些错误在执行开始前抛出,且在调用任何 DataFetcher 之前发生。应用程序可以使用传输级拦截器来检查和转换 ExecutionResult 中的错误。
请查看 WebGraphQlInterceptor 下的示例。
订阅异常
订阅请求中的Publisher可能以错误信号完成,在这种情况下,底层传输(例如WebSocket)会发送一个最终的"error"类型消息,并附带GraphQL错误列表。
DataFetcherExceptionResolver 无法解决来自订阅 Publisher 的错误,
因为数据 DataFetcher 只是在初始化时创建了 Publisher。之后,传输会订阅可能随后因错误而完成的 Publisher。
一个应用程序可以注册一个SubscriptionExceptionResolver,以便解析来自订阅Publisher的异常,并将这些异常转换为GraphQL错误发送给客户端。
分页
The GraphQL Cursor Connection 规范 defines a way to导航大型结果集,通过每次返回一部分项,并在每个项中配对一个游标,客户端可以使用该游标请求引用项之前或之后的更多项。
该规范将此模式称为 “连接(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!
}
定义的分页规范允许客户端通过给定的游标请求“之后”的前N个项目。具体来说,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
}
The Boot Starterregisters ConnectionTypeDefinitionConfigurer by default.
ConnectionAdapter
除了
连接类型 在模式中,
你还需要相应的 Java 类型。GraphQL Java 提供了这些类型,包括通用的
Connection 和 Edge 类型,以及 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 的 Window 和 Slice 提供了内置的 内置 ConnectionAdapter。您也可以创建自定义适配器。
ConnectionAdapter 实现依赖于
CursorStrategy 来
为返回的项目创建游标。相同的策略也用于支持包含分页输入的
Subrange 控制器方法参数。
CursorStrategy
CursorStrategy 是一个契约,用于编码和解码指向大结果集内项位置的字符串游标。游标可以基于索引也可以基于键值。
一个 ConnectionAdapter 使用此功能对返回项的光标进行编码。
注解控制器 方法、Querydsl 仓库以及 示例查询(Query by Example)
仓库使用它来解码分页请求中的光标,并创建 Subrange。
CursorEncoder 是一个相关的合约,进一步地对 String 游标进行编码和解码,使其对客户端是不透明的。EncodingCursorStrategy 结合了 CursorStrategy 和一个 CursorEncoder。您可以使用 Base64CursorEncoder、NoOpEncoder 或创建自己的版本。
存在一个CursorStrategyScrollPosition。 Boot Starter会在Spring Data存在时注册一个CursorStrategy<ScrollPosition>到Base64Encoder中。
批量加载
给定一个Book和其Author,我们可以为一本书创建一个DataFetcher,同时为其作者创建另一个DataFetcher。这使得可以选择带有或不带作者的书籍,但这也意味着书籍和作者不会一起加载,在查询多本书籍时,每个书籍的作者需要单独加载,这称为N+1次选择问题。
DataLoader
GraphQL Java 提供了一个 DataLoader 机制用于批量加载相关实体。
你可以在GraphQL Java 文档中找到全部详情。下面是一个
该机制工作原理的简要总结:
-
在可以加载实体且通过唯一键加载的
DataLoaderRegistry中注册DataLoaders。 -
DataFetchers可以访问DataLoaders并使用它们按ID加载实体。 -
一个
DataLoader通过返回一个未来来推迟加载,因此可以在批处理中进行。 -
DataLoader持有一个基于请求的实体加载缓存,这将进一步提高效率。
BatchLoaderRegistry
The complete batching loading mechanism in GraphQL Java requires implementing one of
several BatchLoader interface, then wrapping and registering those as DataLoaders
with a name in the DataLoaderRegistry.
Spring GraphQL 的 API 稍微有些不同。注册时,只有一个中心的 GraphQLSchema 暴露工厂方法和一个构建器来创建并注册任意数量的批加载函数:
@Configuration
public class MyConfig {
public MyConfig(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Mono<Map<Long, Author>
});
// more registrations ...
}
}
The Boot Starter 声明了一个 BatchLoaderRegistry Bean,你可以将其注入到你的配置中,如上所示,或者注入任何组件(例如控制器),以便注册批量加载功能。随后,BatchLoaderRegistry 被注入到 DefaultExecutionGraphQlService 中,在此确保每次请求的 DataLoader 注册。
默认情况下,DataLoader名称基于目标实体的类名。
这使得一个@SchemaMapping方法能够声明一个带有泛型类型的
DataLoader参数,
而无需指定名称。然而,如果需要的话,可以通过
BatchLoaderRegistry构建器自定义名称,并且还可以自定义其他DataLoaderOptions。
要将默认DataLoaderOptions全局配置为任何注册的起点,您可以重写 Boot 的 BatchLoaderRegistry 模块,并使用 DefaultBatchLoaderRegistry 接受 Supplier<DataLoaderOptions> 的构造函数。
对于许多情况,在加载相关实体时,你可以使用带有@BatchMapping注解的控制器方法,这相当于并替代了直接使用BatchLoaderRegistry和DataLoader的需求。
BatchLoaderRegistry 还提供了其他重要的好处。它支持从批量加载函数和 @BatchMapping 方法访问相同的 GraphQLContext,并且确保 Context Propagation 到这些地方。这也是为什么应用程序被期望使用它的原因。可以直接进行自己的DataLoader 注册,但这样的注册将放弃上述所有好处。
批量加载Recipes
对于简单的用例,@BatchMapping 注解往往是最佳选择,带有最少的样板代码。而对于更复杂的用例,BatchLoaderRegistry 提供了更多的灵活性。
如上所述,DataLoaders 将队列 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 级别过滤掉不必要的那些。这将在 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
}
}
}
将yield下列结果:
{
"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": []
}
]
}
}
测试批量加载
开始让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("...");
// ...