请求执行
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,以及用于 异常解析 的 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()));
}
}
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
返回 Mono
或 Flux
,这些会被适配为 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 在同一线程上执行,例如如果异步 WebGraphQlInterceptor
或 DataFetcher
切换到不同的线程。
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
是一个方便的基类,包含受保护的方法 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 级别记录日志。
请求异常
GraphQL Java 引擎在解析请求时可能会遇到验证或其他错误,这反过来会阻止请求执行。在这种情况下,响应包含一个带有 null
的“data”键和一个或多个请求级别的“errors”,这些错误是全局性的,即没有字段路径。
DataFetcherExceptionResolver
无法处理此类全局错误,因为它们在执行开始和任何 DataFetcher
被调用之前就被抛出。应用可以使用传输层拦截器来检查和转换 ExecutionResult
中的错误。请参阅 WebGraphQlInterceptor
下的示例。
分页
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!
}
规范中定义的用于向前分页的 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
}
Boot Starter 默认注册 ConnectionTypeDefinitionConfigurer
。
ConnectionAdapter
除了 Schema 中的 连接类型,你还需要等效的 Java 类型。GraphQL Java 提供了这些类型,包括泛型 Connection
和 Edge
类型,以及 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 的 Window
和 Slice
有内置的 ConnectionAdapter
。你也可以创建自己的自定义适配器。ConnectionAdapter
实现依赖于一个 CursorStrategy
来为返回的项目创建游标。相同的策略也用于支持包含分页输入的 Subrange
控制器方法参数。
CursorStrategy
CursorStrategy
是一个契约,用于编码和解码引用大型结果集中项目位置的 String 游标。游标可以基于索引或键集。
一个 ConnectionAdapter
使用它来为返回的项目编码游标。注解控制器 方法、Querydsl 仓库和 Query by Example 仓库使用它来解码分页请求中的游标,并创建一个 Subrange
。
CursorEncoder
是一个相关的契约,它进一步编码和解码 String 游标,使其对客户端不透明。EncodingCursorStrategy
结合了 CursorStrategy
和 CursorEncoder
。你可以使用 Base64CursorEncoder
、NoOpEncoder
或创建自己的实现。
Spring Data 的 ScrollPosition
有内置的 CursorStrategy
。当存在 Spring Data 时,Boot Starter 会注册一个带有 Base64Encoder
的 CursorStrategy<ScrollPosition>
。
排序
在 GraphQL 请求中提供排序信息没有标准方法。然而,分页依赖于稳定的排序顺序。你可以使用默认顺序,或者暴露输入类型并从 GraphQL 参数中提取排序详情。
作为控制器方法参数,Spring Data 的 Sort
内置了支持。为了使其工作,你需要一个 SortStrategy
bean。
批量加载 (Batch Loading)
给定一个 Book
及其 Author
,我们可以为一个图书创建一个 DataFetcher
,为它的作者创建另一个。这允许选择带或不带作者的图书,但这意味着图书和作者不是一起加载的,当查询多本图书时,每本图书的作者都是单独加载的,效率尤其低下。这被称为 N+1 查询问题。
DataLoader
GraphQL Java 提供了一种 DataLoader
机制,用于批量加载相关实体。你可以在 GraphQL Java 文档中找到完整的详细信息。下面是它的工作原理摘要:
-
在
DataLoaderRegistry
中注册DataLoader
,它们可以根据唯一的键加载实体。 -
DataFetcher
可以访问DataLoader
并使用它们按 ID 加载实体。 -
DataLoader
通过返回 Future 来推迟加载,以便可以在一个批次中完成。 -
DataLoader
维护一个按请求缓存的已加载实体,这可以进一步提高效率。
BatchLoaderRegistry
GraphQL Java 中完整的批量加载机制需要实现几个 BatchLoader
接口之一,然后将它们包装并注册为带有名称的 DataLoader
到 DataLoaderRegistry
中。
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 控制器方法,它们是使用 BatchLoaderRegistry
和 DataLoader
的快捷方式和替代方案。
BatchLoaderRegistry
还提供了其他重要的好处。它支持批量加载函数和 @BatchMapping
方法访问相同的 GraphQLContext
,并确保 上下文传播 (Context Propagation) 到它们。这就是为什么应用程序应使用它的原因。直接执行自己的 DataLoader
注册是可能的,但这样的注册会放弃上述好处。
测试批量加载 (Batch Loading)
首先让 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("...");
// ...