|
对于最新的稳定版本,请使用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,例如:
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()));
}
}
架构资源
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个参数与模式字段参数匹配。
要启用模式检查,请自定义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 | 跳过的模式类型(下文解释) |
在某些情况下,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 立即完成,在这种情况下,如果请求足够简单且不需要异步数据获取,则会如此。
响应式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在启动时透明地添加一些定义,如果未显式声明的话。这意味着你只需要下面的内容,连接和边类型将会被自动添加:
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 文档中找到全部详情。下面是一个
该机制工作原理的简要总结:
-
注册
DataLoader在可以加载实体的DataLoaderRegistry中,给定唯一键。 -
DataFetcher's 可以访问DataLoader's 并使用它们通过 ID 加载实体。 -
一个
DataLoader通过返回一个未来来推迟加载,因此可以在批处理中进行。 -
DataLoader's维护一个在每次请求中加载实体的缓存,这可以进一步提高效率。
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 注册,但这样的注册将放弃上述所有好处。
测试批量加载
开始让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("...");
// ...