此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring GraphQL 1.4.1! |
请求执行
ExecutionGraphQlService
是调用 GraphQL Java 执行的主要 Spring 抽象 请求。 基础传输(例如 HTTP)委托给ExecutionGraphQlService
来处理请求。
主要实现DefaultExecutionGraphQlService
,配置了GraphQlSource
用于访问graphql.GraphQL
实例调用。
GraphQLSource
GraphQlSource
是一个合约,用于公开graphql.GraphQL
实例也使用它包括一个构建器 API 来构建该实例。默认构建器可通过以下方式获得GraphQlSource.schemaResourceBuilder()
.
Boot Starter 创建此构建器的实例并进一步初始化它从可配置的位置加载模式文件,公开要应用到的属性GraphQlSource.Builder
,以检测RuntimeWiringConfigurer
bean、用于 GraphQL 指标的检测 bean、 和DataFetcherExceptionResolver
和SubscriptionExceptionResolver
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
可以配置一个或多个Resource
实例解析并合并在一起。这意味着模式文件几乎可以从任何 位置。
默认情况下,启动Starters会查找扩展名为“.graphqls” 或 “.gqls” 的架构文件。classpath:graphql/**
,这通常是src/main/resources/graphql
. 您还可以使用文件系统位置或任何位置由 Spring 支持Resource
层次结构,包括自定义实现从远程位置、存储或内存加载架构文件。
用classpath*:graphql/**/ 跨多个类路径查找模式文件位置,例如跨多个模块。 |
架构创建
默认情况下,GraphQlSource.Builder
使用 GraphQL JavaSchemaGenerator
创建graphql.schema.GraphQLSchema
. 这适用于典型用途,但如果您需要使用不同的生成器,您可以注册一个schemaFactory
回调:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
有关如何使用 Spring Boot 配置此功能,请参阅 GraphQlSource 部分。
如果对Federation感兴趣,请参阅Federation部分。
RuntimeWiringConfigurer
一个RuntimeWiringConfigurer
对于注册以下内容很有用:
-
自定义标量类型。
-
处理指令的代码。
-
直接
DataFetcher
注册。 -
以及更多...
Spring 应用程序通常不需要直接执行DataFetcher 注册。 相反,控制器方法被注册为DataFetcher 通过AnnotatedControllerConfigurer ,这是一个RuntimeWiringConfigurer . |
GraphQL Java,服务器应用程序仅使用 Jackson 进行数据映射的序列化。客户端输入被解析为映射。服务器输出根据字段选择集组装成映射。这意味着您不能依赖 Jackson 序列化/反序列化注释。相反,您可以使用自定义标量类型。 |
Boot Starter 检测类型为RuntimeWiringConfigurer
和 将它们注册在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
用于尚未进行此类注册的 GraphQL 接口和联合通过RuntimeWiringConfigurer
. 目的 一个TypeResolver
在 GraphQL 中,Java 是确定值的 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);
有关如何使用 Spring Boot 配置此功能,请参阅 GraphQlSource 部分。
指令
GraphQL 语言支持“描述替代运行时执行和 GraphQL 文档中的类型验证行为“。指令类似于 Java,但在 GraphQL 文档中的类型、字段、片段和作上声明。
GraphQL Java 提供了SchemaDirectiveWiring
帮助应用程序检测的契约
和处理指令。有关更多详细信息,请参阅
GraphQL Java 文档。
在 Spring GraphQL 中,您可以注册一个SchemaDirectiveWiring
通过RuntimeWiringConfigurer
.启动Starters检测到
这样的 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 会创建要使用的异常处理程序,如 Exceptions 中所述,并将其设置为GraphQL.Builder
.然后,GraphQL Java 使用它来创建AsyncExecutionStrategy
实例。
如果您需要创建自定义ExecutionStrategy
,您可以检测DataFetcherExceptionResolver
s 并以相同的方式创建一个异常处理程序,并使用
it 创建自定义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));
}
架构转换
您可以注册一个graphql.schema.GraphQLTypeVisitor
通过builder.schemaResources(..).typeVisitorsToTransformSchema(..)
如果要遍历
并在创建架构后对其进行转换,并对架构进行更改。记住
这比 Schema Traversal 更昂贵,因此通常
除非需要进行架构更改,否则更喜欢遍历而不是转换。
模式遍历
您可以注册一个graphql.schema.GraphQLTypeVisitor
通过builder.schemaResources(..).typeVisitors(..)
如果要在
它被创建,并可能对GraphQLCodeRegistry
.请记住,
但是,此类访问者无法更改架构。如果需要对架构进行更改,请参阅架构转换。
架构映射检查
如果查询、变更或订阅作没有DataFetcher
,它不会
返回任何数据,并且不会做任何有用的事情。同样,架构类型的字段
两者都没有通过DataFetcher
注册,也不会隐含地由
违约PropertyDataFetcher
找到匹配项Class
属性,将始终是null
.
GraphQL Java 不执行检查以确保覆盖每个模式字段,并且作为较低级别的库,GraphQL Java 根本不知道DataFetcher
可以返回或它所依赖的参数,因此无法执行此类验证。这可以导致差距,根据测试覆盖率,这些差距可能要到运行时才能被发现,而客户端可能会遇到“静默”null
值或非空字段错误。
这SelfDescribingDataFetcher
Spring for GraphQL 中的接口允许DataFetcher
自 公开返回类型和预期参数等信息。全部内置,SpringDataFetcher
控制器方法、Querydsl 和 Query by Example 的实现是此接口的实现。对于带注释的控制器,返回类型和expected 参数基于控制器方法签名。这使得在启动时检查模式映射以确保以下内容成为可能:
-
架构字段具有
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 | 未以任何方式涵盖的架构字段 |
2 | DataFetcher 对不存在的字段的注册 |
3 | DataFetcher 不存在的预期参数 |
4 | 已跳过的架构类型(下文介绍) |
在某些情况下,Class
模式类型的类型未知。也许DataFetcher
不
实现SelfDescribingDataFetcher
,或者声明的返回类型过于通用
(例如Object
)或未知(例如List<?>
),或DataFetcher
可能完全失踪。
在这种情况下,架构类型将列为跳过,因为它无法验证。对于每个
skipipped 类型,则 DEBUG 消息解释了跳过它的原因。
联合和接口
对于联合,检查会迭代成员类型并尝试找到相应的 类。对于接口,检查会迭代实现类型并查看 对于相应的类。
默认情况下,在以下情况下可以开箱即用地检测相应的 Java 类:
-
这
Class
的简单名称与接口实现的 GraphQL 联合成员匹配 类型名称,以及Class
与 控制器方法或控制器类,映射到联合或接口字段。 -
这
Class
在架构的其他部分中进行检查,其中映射字段为 具体联合成员或接口实现类型。 -
您已经注册了一个具有显式
Class
到 GraphQL 类型映射 。
在上述任何帮助中,GraphQL 类型在架构检查中报告为跳过 报表时,您可以进行以下自定义:
-
将 GraphQL 类型名称显式映射到一个或多个 Java 类。
-
配置一个函数,该函数自定义如何将 GraphQL 类型名称适应简单的
Class
名字。这可以帮助特定的 Java 类命名约定。 -
提供一个
ClassNameTypeResolver
映射 GraphQL 类型 a Java 类。
例如:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(
initializer -> initializer.classMapping("Author", Author.class)
logger::debug);
作缓存
GraphQL Java 必须在执行作之前对其进行解析和验证。这可能会影响
性能显着。为了避免需要重新解析和验证,应用程序可以
配置一个PreparsedDocumentProvider
缓存和重用文档实例。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))
有关如何使用 Spring Boot 配置此功能,请参阅 GraphQlSource 部分。
螺纹模型
大多数 GraphQL 请求都受益于获取嵌套字段的并发执行。这是
为什么当今大多数应用程序都依赖 GraphQL Java 的AsyncExecutionStrategy
,这允许
要返回的数据获取器CompletionStage
并发执行而不是串行执行。
Java 21 和虚拟线程增加了有效使用更多线程的重要功能,但 为了请求,仍然需要并发执行而不是串行执行 执行以更快地完成。
Spring for GraphQL 支持:
-
响应式数据获取器,这些是 适应
CompletionStage
正如预期的那样AsyncExecutionStrategy
. -
CompletionStage
作为返回值。 -
控制器方法,即 Kotlin 协程方法。
-
@SchemaMapping和@BatchMapping方法可以返回
Callable
提交给Executor
例如 Spring FrameworkVirtualThreadTaskExecutor
.要启用此功能,您必须配置Executor
上AnnotatedControllerConfigurer
.
Spring for GraphQL 在 Spring MVC 或 WebFlux 上运行作为传输。弹簧 MVC
使用异步请求执行,除非生成的CompletableFuture
完成了
GraphQL Java 引擎返回后立即返回,如果
请求非常简单,不需要异步数据获取。
GraphQL 请求超时
GraphQL 客户端可以发送的请求会在服务器端消耗大量资源。 有很多方法可以防止这种情况发生,其中之一是配置请求超时。 这可确保在响应时间过长时在服务器端关闭请求。
Spring for GraphQL 提供了一个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));
}
}
对于更多特定于传输的超时,处理程序实现上有专用属性,例如GraphQlWebSocketHandler
和GraphQlSseHandler
.
反应性的DataFetcher
默认值GraphQlSource
builder 启用了对DataFetcher
返回Mono
或Flux
它使这些调整为CompletableFuture
哪里Flux
值是聚合的
并转换为 List,除非该请求是 GraphQL 订阅请求,
在这种情况下,返回值仍为 Reactive StreamsPublisher
用于流媒体
GraphQL 响应。
反应性DataFetcher
可以依赖于对从
传输层,例如来自 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 支持通过 GraphQL Java 透明地传播上下文从 HTTP 传输,以及DataFetcher
以及它调用的其他组件。这包括两者ThreadLocal
上下文
来自 Spring MVC 请求处理线程和 ReactorContext
来自 WebFlux
处理管道。
WebMvc 网站
一个DataFetcher
GraphQL Java 调用的其他组件可能并不总是在
与 Spring MVC 处理程序相同的线程,例如,如果异步WebGraphQlInterceptor
或DataFetcher
切换到
不同的线程。
Spring for GraphQL 支持传播ThreadLocal
Servlet 容器中的值
线程到线程 aDataFetcher
以及 GraphQL Java 调用的其他组件
执行。为此,应用程序需要实现io.micrometer.context.ThreadLocalAccessor
对于一个ThreadLocal
感兴趣的值:
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();
}
}
您可以注册一个ThreadLocalAccessor
使用全局启动时手动ContextRegistry
实例,可通过以下方式访问io.micrometer.context.ContextRegistry#getInstance()
.您也可以注册它
自动通过java.util.ServiceLoader
机制。
WebFlux (网络通量)
一个反应性的DataFetcher
可以依赖于对 Reactor 上下文的访问,即
源自 WebFlux 请求处理链。这包括 Reactor 上下文
由 WebGraphQlInterceptor 组件添加。
异常
在 GraphQL Java 中,DataFetcherExceptionHandler
决定如何表示来自
响应的“错误”部分中的数据获取。应用程序可以注册
仅单个处理程序。
Spring for GraphQL 注册一个DataFetcherExceptionHandler
提供默认的handling 并启用DataFetcherExceptionResolver
合同。 应用程序可以通过以下方式注册任意数量的解析器GraphQLSource
builder 和那些在order 直到其中一个解决Exception
设置为List<graphql.GraphQLError>
. Spring Boot Starters检测这种类型的 bean。
DataFetcherExceptionResolverAdapter
是一个方便的基类,具有受保护的方法resolveToSingleError
和resolveToMultipleErrors
.
带注释的控制器编程模型支持使用
带有灵活方法签名的带注释的异常处理程序方法,请参阅@GraphQlExceptionHandler
了解详情。
一个GraphQLError
可以分配给基于 GraphQL Java 的类别graphql.ErrorClassification
,或 Spring GraphQLErrorType
,它定义了以下内容:
-
BAD_REQUEST
-
UNAUTHORIZED
-
FORBIDDEN
-
NOT_FOUND
-
INTERNAL_ERROR
如果异常仍未解决,则默认情况下将其分类为INTERNAL_ERROR
使用包含类别名称和executionId
从DataFetchingEnvironment
.该消息故意不透明以避免泄漏
实现详细信息。应用程序可以使用DataFetcherExceptionResolver
自定义
错误详细信息。
未解决的异常将与executionId
关联
发送到客户端的错误。已解决的异常记录在 DEBUG 级别。
请求例外
GraphQL Java 引擎在解析请求时可能会遇到验证或其他错误
这反过来又阻止了请求执行。在这种情况下,响应包含一个
“data”键与null
以及一个或多个全局的请求级“错误”,即不是
具有字段路径。
DataFetcherExceptionResolver
无法处理此类全局错误,因为它们被引发
在执行开始之前和任何执行之前DataFetcher
被调用。应用程序可以使用
传输级拦截器,用于检查和转换ExecutionResult
.
请参阅下面的示例WebGraphQlInterceptor
.
分页
GraphQL 游标连接规范定义了一种导航大型结果集的方法,方法是一次返回项目子集其中每个项目都与一个游标配对,客户端可以使用该游标在之前请求更多项目或在引用的项目之后。
规范将此模式称为“连接”,名称结束的模式类型 跟~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!
}
定义的规范first
和after
正向分页的参数允许客户端
请求给定游标“之后”的“第一个”N 个项目。同样,last
和before
反向分页参数的参数允许请求“最后”N 项“之前”
给定的游标。
规范不鼓励同时包含first 和last 并说明结果
因为分页变得不清楚。在 Spring 中,对于 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
}
启动Starters注册ConnectionTypeDefinitionConfigurer
默认情况下。
ConnectionAdapter
除了架构中的连接类型之外,
您还需要等效的 Java 类型。GraphQL Java 提供了这些,包括通用Connection
和Edge
types,以及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 s. |
2 | 拒绝类型访问者。 |
有内置内置 ConnectionAdapter
s
对于 Spring Data 的Window
和Slice
.您还可以创建自己的自定义适配器。ConnectionAdapter
实现依赖于CursorStrategy
自
为退回的项目创建游标。同样的策略也用于支持Subrange
控制器方法
包含分页输入的参数。
CursorStrategy
CursorStrategy
是一个用于编码和解码 String 游标的合约,该游标引用
项在大型结果集中的位置。游标可以基于索引或
在键集上。
一个ConnectionAdapter
使用它对返回项的游标进行编码。带注释的控制器方法、Querydsl 存储库和按示例查询存储库使用它来解码分页请求中的游标,并创建一个Subrange
.
CursorEncoder
是一个相关的合约,它进一步编码和解码 String 游标
使它们对客户不透明。EncodingCursorStrategy
结合CursorStrategy
使用CursorEncoder
.您可以使用Base64CursorEncoder
,NoOpEncoder
或创建您自己的。
有一个内置的 CursorStrategy
对于 Spring DataScrollPosition
.Boot Starter 注册一个CursorStrategy<ScrollPosition>
跟Base64Encoder
当 Spring Data 存在时。
排序
在 GraphQL 请求中没有提供排序信息的标准方法。然而 分页取决于稳定的排序顺序。您可以使用默认订单,或者其他方式 公开输入类型并从 GraphQL 参数中提取排序详细信息。
内置支持 Spring Data 的Sort
作为控制器
method 参数。为此,您需要有一个SortStrategy
豆。
批量装载
给定一个Book
及其Author
,我们可以创建一个DataFetcher
为一本书和另一本书
对于它的作者。这允许选择有或没有作者的书籍,但这意味着书籍
并且作者不会一起加载,这在查询多个
books 作为每本书的作者被单独加载。这称为 N+1 选择
问题。
DataLoader
GraphQL Java 提供了一个DataLoader
相关实体批量加载的机制。
您可以在 GraphQL Java 文档中找到完整的详细信息。下面是一个
工作原理摘要:
-
注册
DataLoader
s 在DataLoaderRegistry
可以加载给定唯一键的实体。 -
DataFetcher
可以访问DataLoader
s 并使用它们按 id 加载实体。 -
一个
DataLoader
通过返回 future 来延迟加载,以便可以批量完成。 -
DataLoader
维护加载实体的每个请求缓存,这些实体可以进一步 提高效率。
BatchLoaderRegistry
GraphQL Java 中的完整批处理加载机制需要实现
几个BatchLoader
接口,然后将它们包装并注册为DataLoader
s
在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
name 基于目标实体的类名。
这允许@SchemaMapping
方法来声明具有泛型类型的 DataLoader 参数,以及
无需指定名称。但是,可以通过BatchLoaderRegistry
构建器,如有必要,以及其他DataLoaderOptions
.
配置默认值DataLoaderOptions
全局,用作任何
注册,您可以覆盖 Boot 的BatchLoaderRegistry
bean 并使用
为DefaultBatchLoaderRegistry
接受Supplier<DataLoaderOptions>
.
在许多情况下,在加载相关实体时,可以使用@BatchMapping控制器方法,这是一种快捷方式
为和取代需要使用BatchLoaderRegistry
和DataLoader
径直。
BatchLoaderRegistry
还提供其他重要的好处。它支持访问
一样GraphQLContext
从 batch loading 函数和 from@BatchMapping
方法
并确保上下文传播到它们。这就是需要应用的原因
使用它。可以自己执行DataLoader
直接注册,但
此类注册将放弃上述好处。
批量加载配方
对于简单的情况,@BatchMapping注释通常是
最佳选择,样板最少。对于更高级的用例,请BatchLoaderRegistry
提供更大的灵活性。
如上所述,DataLoader
s 将排队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
实例
在DataLoader
cache 并使用更多内存,但它可能会执行更少的 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": []
}
]
}
}
测试批量加载
首先拥有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("...");
// ...