Spring for GraphQL 中文文档

参考官方文档:Spring for GraphQL Documentation Version 1.1.0

1. 概述 Overview

Spring for GraphQL为建立在GraphQL Java 上的Spring应用程序提供支持。它是GraphQL Java团队和Spring工程之间的联合合作。

Spring for GraphQL是GraphQL Java团队的GraphQL Java Spring项目的继承者。它的目标是成为所有Spring、GraphQL应用程序的基础。

请使用我们的问题跟踪器来报告问题,讨论设计问题,或请求一个功能。

请参阅我们的Wiki,了解新内容、基线要求和升级说明,以及其他跨版本信息。

要开始使用,请查看start.spring.io上的Samples启动器和该仓库中的样本。

2. 服务器传输 Server Transports

Spring for GraphQL支持通过HTTP、WebSocket和RSocket对GraphQL请求进行服务器处理。

2.1. HTTP

GraphQlHttpHandler 通过HTTP请求处理GraphQL,并委托给拦截链来执行请求。有两个变体,一个用于Spring MVC,一个用于Spring WebFlux。两者都以异步方式处理请求,并具有同等的功能,但在写HTTP响应时分别依赖阻塞和非阻塞I/O。

请求必须使用HTTP POST,并在请求体中包含GraphQL请求的细节,如拟议的GraphQL over HTTP规范中所定义的那样,作为JSON。一旦JSON主体被成功解码,HTTP响应状态总是200(OK),GraphQL请求执行的任何错误都会出现在GraphQL响应的 "错误 "部分。媒体类型的默认和首选是 “application/graphql+json”,但也支持 “application/json”,如规范中所述。

GraphQlHttpHandler可以通过声明一个RouterFunction Bean并使用Spring MVC或WebFlux的RouterFunctions来创建路由,从而作为一个HTTP端点暴露出来。Boot启动器就是这样做的,详情见Web端点部分,或者查看它所包含的GraphQlWebMvcAutoConfigurationGraphQlWebFluxAutoConfiguration ,了解实际配置。

Spring for GraphQL资源库包含一个Spring MVC HTTP示例应用程序。

2.2. WebSocket

GraphQlWebSocketHandler 根据graphql-ws库中定义的协议protocol处理GraphQL over WebSocket请求。使用GraphQL over WebSocket的主要原因是允许发送GraphQL响应流的订阅,但它也可用于具有单一响应的常规查询。处理程序将每个请求委托给拦截链,以便进一步执行请求。

GraphQL Over WebSocket协议
有两个这样的协议,一个在 subscriptions-transport-ws 库中,另一个在 graphql-ws 库中。前者并不活跃,由后者接替。阅读这篇博文了解历史。

GraphQlWebSocketHandler 有两个变体,一个用于Spring MVC,一个用于Spring WebFlux。两者都以异步方式处理请求,并具有同等的功能。WebFlux处理程序也使用非阻塞I/O和背压来流化消息,这很好,因为在GraphQL Java中,订阅响应是Reactive Streams Publisher

graphql-ws 项目列出了一些供客户端使用的方法recipes

GraphQlWebSocketHandler可以通过声明 SimpleUrlHandlerMapping Bean,并使用它将处理程序映射到URL路径,作为WebSocket端点暴露出来。默认情况下,Boot starter不公开GraphQL over WebSocket端点,但可以通过为端点路径添加一个属性来轻松启用它。详情请参见Web端点部分,或者查看 GraphQlWebMvcAutoConfigurationGraphQlWebFluxAutoConfiguration 的实际Boot启动器配置。

Spring for GraphQL资源库包含一个WebFlux WebSocket示例应用程序。

2.3. RSocket

GraphQlRSocketHandler 通过RSocket请求处理GraphQL。查询和突变被期望并作为RSocket request-response 互动来处理,而订阅则作为request-stream 来处理。

GraphQlRSocketHandler 可以被用来作为 @Controller 的一个委托,该委托被映射到GraphQL请求的路由。例如:

@Controller
public class GraphQlRSocketController {

 private final GraphQlRSocketHandler handler;

 GraphQlRSocketController(GraphQlRSocketHandler handler) {
        this.handler = handler;
 }

 @MessageMapping("graphql")
 public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
        return this.handler.handle(payload);
 }

 @MessageMapping("graphql")
 public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
        return this.handler.handleSubscription(payload);
 }
}

2.4. 拦截

服务器传输允许在GraphQL Java引擎被调用处理请求之前和之后拦截请求。

2.4.1. WebGraphQlInterceptor

HTTPWebSocket传输调用一个由0个或多个WebGraphQlInterceptor 组成的链,后跟一个调用GraphQL Java引擎的ExecutionGraphQl 服务。WebGraphQlInterceptor 允许应用程序拦截传入请求并执行以下操作之一:

  • 检查 HTTP 请求详细信息
  • 自定义 graphql.ExecutionInput
  • 添加 HTTP 响应标头
  • 自定义 graphql.ExecutionResult

例如,拦截器可以将 HTTP 请求标头传递给 DataFetcher

class HeaderInterceptor implements WebGraphQlInterceptor { //(1)

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        String value = request.getHeaders().getFirst("myHeader");
        request.configureExecutionInput((executionInput, builder) ->
                builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
        return chain.next(request);
    }
}

@Controller
class MyController { //(2)

    @QueryMapping
    Person person(@ContextValue String myHeader) {
        // ...
    }
}

(1)拦截器将HTTP请求头值添加到 GraphQLContext

(2)数据控制器方法访问该值

反过来说,拦截器可以访问由控制器添加到 GraphQLContext 的值。

@Controller
class MyController {

    @QueryMapping
    Person person(GraphQLContext context) { //(1)
        context.put("cookieName", "123");
    }
}

// Subsequent access from a WebGraphQlInterceptor

class HeaderInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { //(2)
        return chain.next(request).doOnNext(response -> {
            String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
            ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
            response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString());
        });
    }
}

(1)控制器向 GraphQLContext 添加值

(2)拦截器使用该值来添加一个HTTP响应标头

WebGraphQlHandler 可以修改 ExecutionResult,例如,检查和修改在执行开始前提出的请求验证错误,这些错误不能用DataFetcherExceptionResolver来处理。

static class RequestErrorInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        return chain.next(request).map(response -> {
            if (response.isValid()) {
                return response; //(1)
            }

            List<GraphQLError> errors = response.getErrors().stream() //(2)
                    .map(error -> {
                        GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
                        // ...
                        return builder.build();
                    })
                    .collect(Collectors.toList());

            return response.transform(builder -> builder.errors(errors).build()); //(3)
        });
    }
}

(1) 如果 ExecutionResult 有一个非空值的 "data "键,则返回相同的结果。
(2) 检查并转换GraphQL错误
(3) 用修改后的错误更新 ExecutionResult

使用WebGraphQlHandler配置WebGraphQlInterceptor链。这是Boot starter支持的,请参阅Web端点。

2.4.2. RSocketQlInterceptor

WebGraphQlInterceptor 类似,RSocketQlInterceptor 允许在GraphQL Java引擎执行前后拦截GraphQL over RSocket请求。你可以用它来定制 graphql.ExecutionInputgraphql.ExecutionResult

3. 请求执行 Request Execution

ExecutionGraphQlService 是Spring的主要抽象,用于调用GraphQL Java来执行请求。底层传输工具,如服务器传输工具 Server Transports,委托给ExecutionGraphQlService 来处理请求。

主要的实现,DefaultExecutionGraphQlService,被配置为 GraphQlSource,用于访问调用的 graphql.GraphQL 实例。

3.1. GraphQLSource

GraphQlSource 是一个合同,用于公开使用 graphql.GraphQL 实例,其中还包括一个构建器API来构建该实例。默认的构建器可以通过GraphQlSource.schemaResourceBuilder() 获得。Spring Boot启动程序创建了这个构建器的实例,并进一步初始化了它,具体如下:

  • 从一个可配置的位置加载模式文件
  • 公开适用于 GraphQlSource.Builder 的属性
  • 检测 RuntimeWiringConfigurer
  • 检测用于GraphQL度量的 Instrumentation Beans
  • 检测用于异常解决的 DataFetcherExceptionResolver Beans
  • 检测用于解决订阅异常的 SubscriptionExceptionResolver Beans

为了进一步定制,你可以声明一个GraphQlSourceBuilderCustomizer Bean。例如,配置你自己的 ExecutionIdProvider

@Configuration(proxyBeanMethods = false)
class GraphQlConfig {

    @Bean
    public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
        return (builder) ->
                builder.configureGraphQl(graphQlBuilder ->
                        graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
    }
}
3.1.1. 模式资源 Schema Resources

GraphQlSource.Builder 可以配置一个或多个 Resources 实例来进行解析和合并。这意味着模式文件可以从任何位置加载。

默认情况下,Spring Boot启动器会在 classpath:graphql/** 位置下寻找扩展名为 ".graphqls " 或 ".gqls " 的模式文件,该位置通常是 src/main/resources/graphql。你也可以使用文件系统位置,或者Spring资源层次结构支持的任何位置,包括从远程位置、存储或内存中加载模式文件的自定义实现。

使用 classpath*:graphql/**/ 来查找跨越多个 classpath 位置的模式文件,例如跨越多个模块。

3.1.2. 模式创建 Schema Creation

默认情况下,GraphQlSource.Builder 使用GraphQL Java SchemaGenerator来创建 graphql.schema.GraphQLSchema。这适用于典型的使用,但如果你需要使用不同的生成器,例如用于联盟,你可以注册一个 schemaFactory回调:

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
            // create GraphQLSchema
        })

GraphQlSource部分 解释了如何用Spring Boot进行配置。

3.1.3. 模式遍历 Schema Traversal

如果你想在模式创建后遍历它,并可能应用对 GraphQLCodeRegistry 的更改,你可以通过 builder.schemaResources(.).typeVisitors(.) 注册一个 graphql.schema.GraphQLTypeVisitor。然而,请记住,这样的访问者不能改变模式。如果你需要对模式进行修改,请参阅模式转换

3.1.4. 模式转换 Schema Transformation

如果你想在模式创建后对其进行遍历和转换,并对模式进行更改,你可以通过 builder.schemaResources(.).typeVisitorsToTransformSchema(.) 注册一个 graphql.schema.GraphQLTypeVisitor。请记住,这比模式遍历更昂贵,所以一般来说,除非你需要对模式进行修改,否则首选遍历而不是转换。

3.1.5. 运行时线路配置 RuntimeWiringConfigurer

你可以使用 RuntimeWiringConfigurer 来注册。

  • 自定义标量类型。

  • 指令处理代码。

  • TypeResolver,如果你需要为一个类型覆盖 默认的TypeResolver

  • 一个字段的 DataFetcher,尽管大多数应用程序将简单地配置 AnnotatedControllerConfigurer,它可以检测注释的、DataFetcher 处理方法。Spring Boot启动器默认添加了 AnnotatedControllerConfigurer

与Web框架不同,GraphQL不使用Jackson注解来驱动JSON序列化/反序列化。自定义数据类型和它们的序列化必须被描述为Scalars

Spring Boot启动器会检测 RuntimeWiringConfigurer 类型的Bean,并将其注册到 GraphQlSource.Builder 中。这意味着在大多数情况下,你的配置中会有类似以下内容。

@Configuration
public class GraphQlConfig {

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {

        GraphQLScalarType scalarType = ... ;
        SchemaDirectiveWiring directiveWiring = ... ;
        DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();

        return wiringBuilder -> wiringBuilder
                .scalar(scalarType)
                .directiveWiring(directiveWiring)
                .type("Query", builder -> builder.dataFetcher("book", dataFetcher));
    }
}

如果你需要添加一个 WiringFactory,例如进行考虑到模式定义的注册,实现替代的configure方法,该方法同时接受RuntimeWiring.Builder和输出List<WiringFactory> 。这允许你添加任何数量的工厂,然后按顺序调用。

3.1.6. 默认类型解析器 Default TypeResolver

GraphQlSource.BuilderClassNameTypeResolver 注册为默认的 TypeResolver,以用于尚未通过 [RuntimeWiringConfigurer](Spring for GraphQL Documentation)注册的GraphQL接口和联盟。GraphQL Java中 TypeResolver 的目的是为GraphQL接口或联盟字段从 DataFetcher 返回的值确定GraphQL对象类型。

ClassNameTypeResolver 试图将值的简单类名与GraphQL对象类型相匹配,如果不成功,它也会浏览它的超级类型,包括基类和接口,寻找匹配。ClassNameTypeResolver 提供了一个选项,可以配置一个名称提取函数以及类到GraphQL对象类型的名称映射,这应该有助于涵盖更多的角落情况。

GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
    // Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);

GraphQlSource部分 解释了如何用Spring Boot进行配置。

3.1.7. 操作缓存 Operation Caching

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 = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))

GraphQlSource部分 解释了如何用Spring Boot进行配置。

3.1.8. 指令 Directives

GraphQL语言支持 "描述GraphQL文档中备用的运行时执行和类型验证行为 "的指令。指令类似于Java中的注解,但在GraphQL文档中对类型、字段、片段和操作进行声明。

GraphQL Java提供了 SchemaDirectiveWiring 合约,以帮助应用程序检测和处理指令。更多细节,请参见GraphQL Java文档中的Schema Directives

在Spring GraphQL中,你可以通过 RuntimeWiringConfigurer 注册一个 SchemaDirectiveWiring 。Spring Boot启动器会检测到这些 beans,所以你可能会有这样的东西:

@Configuration
public class GraphQlConfig {

     @Bean
     public RuntimeWiringConfigurer runtimeWiringConfigurer() {
          return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
     }
}

关于指令支持的例子,请看Graphql Java库的扩展验证

3.2. 响应式数据提取器 Reactive DataFetcher

默认的 GraphQlSource构造器能够支持 DataFetcher 返回 MonoFlux ,并将其调整为 CompletableFuture,其中 Flux 值被聚合并转化为List,除非请求是GraphQL订阅请求,在这种情况下,返回值仍然是 Reactive Streams Publisher,用于流式GraphQL响应。

反应式 DataFetcher 可以依赖从传输层传播的Reactor上下文的访问,例如从WebFlux请求处理,见 WebFlux Context)。

3.3. 上下文传播 Context Propagation

Spring for GraphQL提供了支持,通过GraphQL Java将上下文从服务器传输系统透明地传播到 DataFetcher 和它调用的其他组件。这包括来自Spring MVC请求处理线程的 ThreadLocal上下文和来自WebFlux处理管道的Reactor Context

3.3.1. WebMvc

由GraphQL Java调用的 DataFetcher 和其他组件可能并不总是在与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 实例手动注册一个 ThreadLocalAccessor,该实例可以通过io.micrometer.context.ContextRegistry#getInstance() 访问。你也可以通过java.util.ServiceLoader 机制自动注册它。

3.3.2. WebFlux

Reactive DataFetcher可以依赖对源自WebFlux请求处理链的Reactor context 的访问。这包括由 WebGraphQlInterceptor组件添加的Reactor context 。

3.4. 异常解决 Exception Resolution

GraphQL Java应用程序可以注册一个 DataFetcherExceptionHandler 来决定如何在GraphQL响应的 "错误 "部分表示来自数据层的异常。

Spring for GraphQL有一个内置的 DataFetcherExceptionHandler,它被配置为由默认的 GraphQLSource构造器使用。它允许应用程序注册一个或多个 Spring DataFetcherExceptionResolver 组件,这些组件被依次调用,直到一个组件将 Exception 解析为graphql.GraphQLError 对象的(可能是空的)列表。

DataFetcherExceptionResolver 是一个异步合约。对于大多数实现来说,扩展 DataFetcherExceptionResolverAdapter 并覆盖它的 resolveToSingleErrorresolveToMultipleErrors 方法之一就足够了,这些方法可以同步地解决异常。

GraphQLError可以通过 graphql.ErrorClassification 分配给一个类别。在Spring GraphQL中,你也可以通过 ErrorType 进行分配,它有以下常见的分类,应用程序可以用来对错误进行分类。

  • BAD_REQUEST 错误的请求
  • UNAUTHORIZED 未授权的
  • FORBIDDEN 禁用
  • NOT_FOUND 未找到
  • INTERNAL_ERROR 内部错误

如果一个异常没有被解决,默认情况下,它被归类为 INTERNAL_ERROR ,有一个通用的消息,包括类别名称和来自DataFetchingEnvironmentexecutionId。该消息是故意不透明的,以避免泄露执行细节。应用程序可以使用DataFetcherExceptionResolver 来定制错误细节。

未解决的异常与executionId 一起被记录在ERROR级别,以便与发送给客户端的错误相关联。已解决的异常记录在DEBUG级别。

3.4.1. 请求异常 Request Exceptions

GraphQL Java引擎在解析请求时可能会遇到验证或其他错误,这反过来会阻止请求执行。在这种情况下,响应包含一个空的 “数据” 键和一个或多个请求级的 “错误”,这些错误是全局的,即没有字段路径。

DataFetcherExceptionResolver不能处理这样的全局错误,因为它们是在执行开始之前和任何 DataFetcher 被调用之前引发的。应用程序可以使用传输级拦截器来检查和转换 ExecutionResult 中的错误。参见WebGraphQlInterceptor下的例子。

3.4.2. 订阅异常 Subscription Exceptions

订阅请求的 Publisher 可能会以错误信号完成,在这种情况下,底层传输(例如WebSocket)会发送最后的 "错误 "类型消息,其中包含GraphQL错误列表。

DataFetcherExceptionResolver 不能解决来自订阅 Publisher 的错误,因为数据 DataFetcher 最初只创建 Publisher。之后,传输器订阅 Publisher,然后可能完成错误。

应用程序可以注册一个 SubscriptionExceptionResolver,以便解决来自订阅Publisher的异常,从而将这些异常解决为GraphQL错误,并发送给客户端。

3.5. 批量加载 Batch Loading

给定一本书Book和它的作者Author,我们可以为一本书Book创建一个 DataFetcher,为其作者Author创建另一个。这允许选择有或没有作者的书,但这意味着书和作者不是一起加载的,这在查询多本书时效率特别低,因为每本书的作者是单独加载的。这就是所谓的N+1选择问题。

3.5.1. 数据加载器 DataLoader

GraphQL Java提供了一个用于批量加载相关实体的 DataLoader 机制。你可以在 GraphQL Java 文档中找到完整的细节。下面是对其工作原理的总结。

  • DataLoaderRegistry 中注册可以加载实体的 DataLoader,并给出唯一的键
  • DataFetcher 可以访问 DataLoader,并使用它们来加载实体的ID
  • DataLoader 通过返回一个future来推迟加载,所以它可以在一个批次中完成
  • DataLoader 维护每个请求的加载实体的缓存,可以进一步提高效率
3.5.2. 批量加载器注册表 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 ...
    }
}

Spring Boot启动器声明了一个 BatchLoaderRegistry Bean,你可以把它注入到你的配置中,如上图所示,或者注入到任何组件中,如控制器,以便注册批处理加载功能。反过来,BatchLoaderRegistry 被注入到 DefaultExecutionGraphQlService 中,在那里它确保每个请求的 DataLoader 注册。

默认情况下,DataLoader的名字是基于目标实体的类名的。这允许 @SchemaMapping方法以通用类型声明一个 DataLoader 参数,并且不需要指定名称。然而,如果有必要,可以通过 BatchLoaderRegistry 构造器来定制该名称,以及其他 DataLoaderOptions

要全局配置默认的 DataLoaderOptions ,作为任何注册的起点,你可以覆盖Boot的 BatchLoaderRegistry Bean,并使用接受 Supplier<DataLoaderOptions>DefaultBatchLoaderRegistry 构造函数。

在许多情况下,在加载相关实体时,你可以使用@BatchMapping控制器方法,这是一个捷径,可以取代直接使用 BatchLoaderRegistryDataLoader 的需要。

BatchLoaderRegistry也提供了其他重要的好处。它支持从批处理加载函数和 @BatchMapping 方法中访问相同的GraphQLContext,以及确保Context Propagation到它们。这就是为什么应用程序被期望使用它。可以直接执行你自己的 DataLoader 注册,但这种注册将放弃上述的好处。

3.5.3. 测试批量加载 Testing Batch Loading

首先,让 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("...");
// ...

4. 数据集成 Data Integration

Spring for GraphQL让你可以利用现有的Spring技术,遵循常见的编程模型,通过GraphQL暴露底层数据源。

本节讨论了Spring Data的集成层,它提供了一种简单的方法来将Querydsl或Query by Example资源库改编为 DataFetcher,包括对标有 @GraphQlRepository 的资源库进行自动检测和GraphQL查询注册的选项。

4.1. Querydsl

Querydsl 是一个Java开源框架用于构建类型安全的SQL查询语句。它采用API代替拼凑字符串来构造查询语句。可跟 Hibernate 和 JPA 等框架结合使用。

Spring for GraphQL支持使用Querydsl,通过Spring Data Querydsl 扩展 来获取数据。Querydsl提供了一种灵活但类型安全的方法,通过使用注解处理器生成元模型来表达查询谓词。

例如,将一个存储库声明为 QuerydslPredicateExecutor

public interface AccountRepository extends Repository<Account, Long>,
            QuerydslPredicateExecutor<Account> {
}

然后用它来创建一个 DataFetcher

// For single result queries
DataFetcher<Account> dataFetcher =
        QuerydslDataFetcher.builder(repository).single();

// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
        QuerydslDataFetcher.builder(repository).many();

现在你可以通过 RuntimeWiringConfigurer 注册上述 DataFetcher

DataFetcher 从GraphQL请求参数中建立了一个Querydsl Predicate,并使用它来获取数据。Spring Data支持JPA、MongoDB和LDAP的 QuerydslPredicateExecutor

如果存储库是 ReactiveQuerydslPredicateExecutor,构造器会返回 DataFetcher<Mono<Account>>DataFetcher<Flux<Account>>。Spring Data支持MongoDB的这种变体。

4.1.1. 构造设置 Build Setup

要在你的构造中配置Querydsl,请遵循官方参考文档

比如说:

# Gradle

dependencies {
    //...

    annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa",
            'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
            'javax.annotation:javax.annotation-api:1.3.2'
}

compileJava {
     options.annotationProcessorPath = configurations.annotationProcessor
}    
# Maven
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydsl.version}</version>
        <classifier>jpa</classifier>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.hibernate.javax.persistence</groupId>
        <artifactId>hibernate-jpa-2.1-api</artifactId>
        <version>1.0.2.Final</version>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
</dependencies>
<plugins>
    <!-- Annotation processor configuration -->
    <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>${apt-maven-plugin.version}</version>
        <executions>
            <execution>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

webmvc-http 样本对 artifactRepositories 使用Querydsl。

4.1.2. 定制化 Customizations

QuerydslDataFetcher 支持自定义GraphQL参数如何被绑定到属性上,以创建一个Querydsl谓词。默认情况下,参数被绑定为每个可用的属性的 “等于”。要定制这一点,你可以使用 QuerydslDataFetcher 构造器方法来提供 QuerydslBinderCustomizer

存储库本身可以是 QuerydslBinderCustomizer 的一个实例。这在自动注册过程中会被自动检测到并透明地应用。然而,当手动构建QuerydslDataFetcher 时,你将需要使用构造器方法来应用它。

QuerydslDataFetcher 支持接口和DTO投影,以便在返回查询结果进行进一步的GraphQL处理之前对这些结果进行转换。

要了解什么是投影,请参考Spring Data文档。要了解如何在GraphQL中使用投影,请看 选择集与投影(Selection Set vs Projections)

要在Querydsl资源库中使用Spring Data投影,请创建一个投影接口或一个目标DTO类,并通过 projectAs方法对其进行配置,以获得产生目标类型的 DataFetcher

class Account {

    String name, identifier, description;

    Person owner;
}

interface AccountProjection {

    String getName();

    String getIdentifier();
}

// For single result queries
DataFetcher<AccountProjection> dataFetcher =
        QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single();

// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
        QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
4.1.3. 自动注册 Auto-Registration

如果一个资源库被 @GraphQlRepository 注解,它就会被自动注册,用于那些还没有注册的 DataFetcher 的查询,并且其返回类型与资源库的域类型相匹配。这包括单值和多值查询。

默认情况下,查询返回的 GraphQL 类型的名称必须与资源库域类型的简单名称相匹配。如果需要,你可以使用 @GraphQlRepositorytypeName 属性来指定目标 GraphQL 类型名称。

自动注册会检测一个给定的资源库是否实现了 QuerydslBinderCustomizer,并通过 QuerydslDataFetcher 构造器方法透明地应用。

自动注册是通过内置的 RuntimeWiringConfigurer进行的,可以从 QuerydslDataFetcher获得。Boot starter会自动检测@GraphQlRepository Bean并使用它们来初始化 RuntimeWiringConfigurer

自动注册并不支持 定制。如果你需要,你需要使用 QueryByExampleDataFetcher,通过 RuntimeWiringConfigurer 手动构建和注册DataFetcher

4.2. Query by Example (QBE)

Spring Data支持使用 示例查询 来获取数据。示例查询(QBE)是一种简单的查询技术,不需要你通过商店特定的查询语言编写查询。

首先声明一个存储库是 QueryByExampleExecutor

public interface AccountRepository extends Repository<Account, Long>,
            QueryByExampleExecutor<Account> {
}

使用 QueryByExampleDataFetcher 将资源库变成一个DataFetcher

// For single result queries
DataFetcher<Account> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).single();

// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).many();

现在你可以通过RuntimeWiringConfigurer注册上述 DataFetcher

DataFetcher使用GraphQL参数图来创建存储库的领域类型,并将其作为示例对象来获取数据。Spring Data支持JPA、MongoDB、Neo4j和Redis的 QueryByExampleDataFetcher

如果存储库是 ReactiveQueryByExampleExecutor,构造器会返回 DataFetcher<Mono<Account>>DataFetcher<Flux<Account>>。Spring Data支持MongoDB、Neo4j、Redis和R2dbc的这种变体。

4.2.1. 构造设置 Build Setup

示例查询已经包含在支持它的数据存储的Spring Data模块中,所以不需要额外的设置就可以启用它。

4.2.2. 定制化 Customizations

QueryByExampleDataFetcher支持接口和DTO投影,以便在返回查询结果进行进一步的GraphQL处理之前对这些结果进行转换。

要了解什么是投影,请参考Spring Data文档。要了解如何在GraphQL中使用投影,请看 选择集与投影(Selection Set vs Projections)

要使用Spring Data投影与Query by Example资源库,请创建一个投影接口或一个目标DTO类,并通过 projectAs 方法进行配置,以获得产生目标类型的 DataFetcher

class Account {

    String name, identifier, description;

    Person owner;
}

interface AccountProjection {

    String getName();

    String getIdentifier();
}

// For single result queries
DataFetcher<AccountProjection> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single();

// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
4.2.3. 自动注册 Auto-Registration

如果一个资源库被 @GraphQlRepository 注解,它就会被自动注册,用于那些还没有注册的 DataFetcher 的查询,并且其返回类型与资源库的域类型相匹配。这包括单值和多值查询。

默认情况下,查询返回的 GraphQL 类型的名称必须与资源库域类型的简单名称相匹配。如果需要,你可以使用 @GraphQlRepositorytypeName 属性来指定目标 GraphQL 类型名称。

自动注册是通过内置的 RuntimeWiringConfigurer进行的,可以从 QueryByExampleDataFetcher 获得。Boot starter会自动检测@GraphQlRepository Bean并使用它们来初始化 RuntimeWiringConfigurer

自动注册并不支持定制。如果你需要,你需要使用 QueryByExampleDataFetcher,通过RuntimeWiringConfigurer手动构建和注册DataFetcher

4.3. 选择集与投影 Selection Set vs Projections

一个常见的问题是,GraphQL选择集与 Spring Data projections 相比如何,各自发挥什么作用?

简短的回答是,Spring for GraphQL不是一个直接将GraphQL查询翻译成SQL或JSON查询的数据网关。相反,它可以让你利用现有的Spring技术,并且不假设GraphQL模式和底层数据模型之间的一对一映射。这就是为什么客户端驱动的选择和服务器端的数据模型转换可以发挥互补的作用。

为了更好地理解,考虑到Spring Data提倡领域驱动(DDD)设计,作为管理数据层复杂性的推荐方法。在DDD中,坚持聚合的约束是很重要的。根据定义,一个聚合只有在完全加载的情况下才有效,因为部分加载的聚合可能会对聚合功能造成限制。

在Spring Data中,你可以选择是否希望你的聚合按原样暴露,或者是否在将其作为GraphQL结果返回之前对数据模型进行转换。有时做前者就足够了,默认情况下,QuerydslQuery by Example 集成会将GraphQL选择集转化为属性路径提示,底层的Spring Data模块会用来限制选择。

在其他情况下,为了适应GraphQL模式,减少甚至转换底层数据模型是有用的。Spring Data通过 Interface 和 DTO Projections来支持这一点。

Interface projections定义了一组固定的属性,以公开这些属性,根据数据存储的查询结果,这些属性可能是也可能不是空的。有两种接口投射,它们都决定了从底层数据源加载哪些属性。

  • 如果你不能将聚合对象部分物化,但你仍然想公开一个属性子集,那么 Closed interface projections 是有帮助的。

  • Open interface projections 利用Spring的@Value注解和SpEL表达式来应用轻量级的数据转换,如连接、计算或对属性应用静态函数。

DTO projections 提供了更高层次的定制,因为你可以将转换代码放在构造器或获取器方法中。

DTO projections 从一个查询中实现,其中的各个属性由投影本身决定。DTO projections通常与全参构造器 full-args constructors(例如Java记录)一起使用,因此只有当所有需要的字段(或列)都是数据库查询结果的一部分时,它们才能被构造。

5. 注解控制器 Annotated Controllers

Spring for GraphQL提供了一个基于注解的编程模型,其中 @Controller组件使用注解来声明具有灵活方法签名的处理方法,以获取特定GraphQL字段的数据。比如说:

@Controller
public class GreetingController {

        @QueryMapping //(1)
        public String hello() { //(2)
            return "Hello, world!";
        }
}

(1)将此方法绑定到一个查询,即查询类型下的一个字段。
(2)如果没有在注解上声明,则从方法名称中确定查询。

Spring for GraphQL使用 RuntimeWiring.Builder 将上述处理方法注册为名为 "hello "的查询的 graphql.schema.DataFetcher

5.1. 声明 Declaration

你可以将 @Controller Bean定义为标准的Spring Bean定义。@Controller定型允许自动检测,与Spring对检测classpath上的@Controller@Component 类并为其自动注册Bean定义的一般支持一致。它也是注释类的定型,表明其在GraphQL应用中作为数据获取组件的作用。

AnnotatedControllerConfigurer 检测 @Controller Bean并通过 RuntimeWiring.Builder 将其注释的处理方法注册为DataFetchers 。它是 RuntimeWiringConfigurer 的一个实现,可以被添加到 GraphQlSource.Builder 中。Spring Boot启动器会自动将 AnnotatedControllerConfigurer 声明为一个bean,并将所有 RuntimeWiringConfigurer Bean添加到GraphQlSource.Builder 中,这样就可以支持注释的DataFetchers ,请参见Boot启动器文档中的 GraphQL RuntimeWiring 部分。

5.2. @SchemaMapping

@SchemaMapping 注解将一个处理方法映射到GraphQL模式中的一个字段,并声明它是该字段的 DataFetcher 。该注解可以指定父类型名称和字段名称:

@Controller
public class BookController {

    @SchemaMapping(typeName="Book", field="author")
    public Author getAuthor(Book book) {
        // ...
    }
}

@SchemaMapping注解也可以不使用这些属性,在这种情况下,字段名默认为方法名,而类型名默认为注入方法的源/父对象的简单类名。例如,下面的内容默认为类型 "Book "和字段 “author”:

@Controller
public class BookController {

    @SchemaMapping
    public Author author(Book book) {
        // ...
    }
}

@SchemaMapping 注解可以在类的层次上声明,以便为类中的所有处理方法指定一个默认的类型名称:

@Controller
@SchemaMapping(typeName="Book")
public class BookController {

    // @SchemaMapping methods for fields of the "Book" type

}

@QueryMapping@MutationMapping@SubscriptionMapping 是元注解,它们本身被 @SchemaMapping 所注解,并且类型名分别被预设为查询、突变或订阅。有效地,这些是查询、突变和订阅类型下的字段的快捷注解。比如说:

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(@Argument Long id) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInput bookInput) {
        // ...
    }

    @SubscriptionMapping
    public Flux<Book> newPublications() {
        // ...
    }
}

@SchemaMapping 处理程序方法有灵活的签名,可以从一系列的方法参数和返回值中选择。

5.2.1. 方法签名 Method Signature

模式映射处理程序方法(Schema mapping handler methods)可以有以下任何方法参数:

Method ArgumentDescription
@Argument用于访问一个命名的字段参数,该参数被绑定到一个更高级别的类型化的Object。参见@Argument.
@Argument Map<String, Object>用于访问参数的原始映射,其中@Argument没有name属性
ArgumentValue用于访问一个命名的字段参数,该字段参数与一个标志一起被绑定到一个更高层次的类型化的Object上,该标志表示输入参数是否被省略与设置为 null。参见ArgumentValue.
@Arguments用于访问绑定到更高层次的、类型化的Object的所有字段参数。参见@Arguments.
`@Arguments Map<String, Object>用于访问参数的原始映射
@ProjectedPayload Interface用于通过项目接口访问字段参数。参见 @ProjectedPayload Interface.
“Source”用于访问字段的源(即父/容器)实例。参见 Source.
DataLoader用于访问 DataLoaderRegistry 中的 DataLoader 。参见 DataLoader.
@ContextValue用于从 DataFetchingEnvironment 中的主 GraphQLContext 访问属性
@LocalContextValue用于从 DataFetchingEnvironment 中的本地 GraphQLContext 访问属性
GraphQLContext用于从 DataFetchingEnvironment 访问上下文
java.security.Principal如果有的话,从Spring Security上下文中获取
@AuthenticationPrincipal从Spring Security上下文访问 Authentication#getPrincipal()
DataFetchingFieldSelectionSet用于通过 DataFetchingEnvironment 访问查询的选择集
Locale, Optional<Locale>用于从 DataFetchingEnvironment 访问 Locale
DataFetchingEnvironment用于直接访问底层的 DataFetchingEnvironment

模式映射处理程序方法(Schema mapping handler methods )可以返回:

  • 一个任何类型的解析值
  • MonoFlux 的异步值。支持控制器方法和任何 DataFetcher ,如 Reactive DataFetcher 中所述
  • java.util.concurrent.Callable 可以异步产生值。为了实现这一点,AnnotatedControllerConfigurer 必须配置一个Executor
5.2.2. @Argument

在GraphQLJava中,DataFetchingEnvironment 提供了对字段特定参数值映射的访问。这些值可以是简单的标量值(如String、Long),也可以是更复杂输入的值映射或值列表。。

使用 @Argument 注解可以将参数绑定到目标对象并注入到处理方法中。绑定是通过将参数值映射到预期的方法参数类型的主数据构造器,或者通过使用默认构造器来创建对象,然后将参数值映射到其属性。这个过程是递归重复的,使用所有嵌套的参数值并相应地创建嵌套的目标对象。比如说:

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(@Argument Long id) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInput bookInput) {
        // ...
    }
}

默认情况下,如果方法参数名称是可用的(需要使用 Java 8+的 -parameters 编译器标志或来自编译器的调试信息),它将被用来查找参数。如果需要,你可以通过注解自定义名称,例如:@Argument("bookInput")

@Argument 注解没有一个 "required "标志,也没有指定默认值的选项。这两点都可以在GraphQL模式层面指定,并由GraphQL Java执行。

如果绑定失败,就会产生一个 BindException,绑定问题被累积为字段错误,每个错误的field是发生问题的参数路径。

你可以用 Map<String, Object> 参数来使用 @Argument ,以获得所有参数值的原始映射。@Argument 上的name属性不能被设置。

5.2.3. ArgumentValue

默认情况下,GraphQL中的输入参数是空的和可选的,这意味着一个参数可以被设置为空字面,或者根本不提供。这种区别对于有突变的部分更新是很有用的,在这种情况下,基础数据也可能被设置为null或者根本没有相应的变化。当使用 @Argument 时,没有办法进行这样的区分,因为在这两种情况下你都会得到null或空的 Optional

如果你想知道一个值是否根本没有被提供,你可以声明一个 ArgumentValue 方法参数,它是一个简单的容器,用来存放结果的值,同时还有一个标志来指示输入参数是否被完全省略。你可以用它来代替 @Argument ,在这种情况下,参数名是由方法参数名决定的,或者和@Argument 一起指定参数名。

比如说:

@Controller
public class BookController {

    @MutationMapping
    public void addBook(ArgumentValue<BookInput> bookInput) {
        if (!bookInput.isOmitted()) {
            BookInput value = bookInput.value();
            // ...
        }
    }
}

ArgumentValue 也被支持作为 @Argument 方法参数的对象结构中的一个字段,可以通过构造函数参数初始化,也可以通过 setter初始化,包括作为嵌套在顶层对象下面任何一层的对象的一个字段。

5.2.4. @Arguments

如果要将完整的参数映射绑定到单个目标对象,请使用 @Arguments 注释,而不是 @Argument,后者绑定特定的命名参数。

例如,@Argument BookInput BookInput 使用参数“BookInput”的值来初始化 BookInput ,而 @Arguments 使用完整的参数映射,在这种情况下,顶级参数绑定到BookInput属性。

可以将 @ArgumentsMap<String,Object> 参数一起使用,以获得所有参数值的原始映射。

5.2.5. @ProjectedPayload 接口

作为使用带有 @Argument的完整对象的替代方案,你也可以使用一个投影接口,通过一个定义明确的最小接口来访问GraphQL请求参数。当Spring Data在类路径上时,参数投射由 Spring Data’s Interface projections 提供。

为了利用这一点,创建一个用 @ProjectedPayload 注释的接口,并将其声明为控制器方法参数。如果该参数用 @Argument 注释,它适用于 DataFetchingEnvironment.getArguments() 映射中的单个参数。在没有@Argument的情况下声明时,投影将在完整参数映射中的顶级参数上工作。

比如说:

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(BookIdProjection bookId) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInputProjection bookInput) {
        // ...
    }
}

@ProjectedPayload
interface BookIdProjection {

    Long getId();
}

@ProjectedPayload
interface BookInputProjection {

    String getName();

    @Value("#{target.author + ' ' + target.name}")
    String getAuthorAndName();
}
5.2.6. Source

在GraphQL Java中,DataFetchingEnvironment 提供对字段的源(即父/容器)实例的访问。要访问这个,只需声明一个预期目标类型的方法参数。

@Controller
public class BookController {

    @SchemaMapping
    public Author author(Book book) {
        // ...
    }
}

源方法参数也有助于确定映射的类型名称。如果Java类的简单名称与GraphQL类型相匹配,那么就不需要在 @SchemaMapping 注解中明确指定类型名称。

@BatchMapping 处理程序方法可以在给定源/父书籍对象列表的情况下批量加载查询的所有作者。

5.2.7. DataLoader

当您为实体注册批加载函数时,如批加载中所述,您可以通过声明DataLoader类型的方法参数来访问实体的DataLoader,并使用它来加载实体:

@Controller
public class BookController {

    public BookController(BatchLoaderRegistry registry) {
        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
            // return Map<Long, Author>
        });
    }

    @SchemaMapping
    public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
        return loader.load(book.getAuthorId());
    }
}

默认情况下,BatchLoaderRegistry 使用值类型的完整类名(例如 Author 的类名)作为注册的键,因此简单地用泛型类型声明DataLoader 方法参数就可以提供足够的信息来在 DataLoaderRegistry 中找到它。作为回退,DataLoader 方法参数解析器还将尝试将方法参数名称作为键,但通常不需要这样做。

请注意,对于许多加载相关实体的情况,@SchemaMapping 只是委托给 DataLoader ,您可以使用 @BatchMapping 方法减少样板,如下一节所述。

5.2.8. Validation

当找到一个 javax.validation.Validator Bean时,AnnotatedControllerConfigurer 会在注释的控制器方法上启用对 Bean Validation 的支持。通常情况下,Bean的类型是 LocalValidatorFactoryBean

Bean验证可以让你声明对类型的约束:

public class BookInput {

    @NotNull
    private String title;

    @NotNull
    @Size(max=13)
    private String isbn;
}

然后你可以用@Valid注解控制器方法参数,在方法调用前对其进行验证:

@Controller
public class BookController {

    @MutationMapping
    public Book addBook(@Argument @Valid BookInput bookInput) {
        // ...
    }
}

如果在验证过程中发生错误,就会引发一个 ConstraintViolationException。你可以使用异常解析链来决定如何将其转化为GraphQL响应中的错误,从而将其呈现给客户端。

除了@Valid,你还可以使用Spring的 @Validated,允许指定验证组。

Bean验证对 @Argument, @Arguments@ProjectedPayload 方法参数很有用,但更普遍适用于任何方法参数。

验证和Kotlin Coroutines
Hibernate验证器与Kotlin Coroutine方法不兼容,在反省其方法参数时失败。请参阅 spring-projects/spring-graphql#344 (comment) ,了解相关问题的链接和建议的解决方法。

5.3. @BatchMapping

批量加载通过使用 org.cataloader.dataloader 来延迟单个实体实例的加载,从而可以将它们一起加载,从而解决N+1选择问题。例如:

@Controller
public class BookController {

    public BookController(BatchLoaderRegistry registry) {
        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
            // return Map<Long, Author>
        });
    }

    @SchemaMapping
    public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
        return loader.load(book.getAuthorId());
    }
}

对于上面显示的加载一个相关实体的直接案例,@SchemaMapping方法除了委托给 DataLoader之外,没有做其他事情。这是一个模板,可以用 @BatchMapping 方法来避免。比如说:

@Controller
public class BookController {

    @BatchMapping
    public Mono<Map<Book, Author>> author(List<Book> books) {
        // ...
    }
}

上述内容成为 BatchLoaderRegistry 中的批加载函数,其中键是Book实例,加载的值是其作者。此外,DataFetcher 还透明地绑定到Book 类型的 author 字段,只要给定其源/父Book实例,即可将其委托给作者的DataLoader

要用作唯一密钥,Book 必须实现 hashcodeequals

默认情况下,字段名默认为方法名,而类型名默认为输入List元素类型的简单类名。两者都可以通过注释属性进行自定义。类型名称也可以从类级别 @SchemaMapping 继承。

5.3.1. 方法签名 Method Signature

批映射方法支持以下参数:

Method ArgumentDescription
List<K>源/父对象
java.security.Principal如果有的话,从Spring Security上下文中获取
@ContextValue用于访问 BatchLoaderEnvironmentGraphQLContext 中的值,该上下文与DataFetchingEnvironment中的上下文相同
GraphQLContext用于从BatchLoaderEnvironment 访问上下文,该上下文与DataFetchingEnvironment 中的上下文相同
BatchLoaderEnvironmentGraphQL Java中可用于 org.dataloader.BatchLoaderWithContext 的环境

批映射方法可以返回:

Return TypeDescription
Mono<Map<K,V>>以父对象为关键点,以批加载对象为值的贴图
Flux<V>批加载对象的序列,其顺序必须与传递到方法中的源/父对象的顺序相同
Map<K,V>, Collection<V>强制变体,例如无需远程调用
Callable<Map<K,V>>, Callable<Collection<V>>异步调用的强制变量。要使其工作,必须使用 Executor 配置 AnnotatedControllerConfig

6. 安全 Security

Web GraphQL端点的路径可以用HTTP URL安全来保证,以确保只有经过认证的用户才能访问它。然而,这并不能区分单一URL上的这种共享端点的不同GraphQL请求。

要应用更细粒度的安全,可将Spring安全注释(如 @PreAuthorize@Secured )添加到参与获取GraphQL响应特定部分的服务方法中。这应该是由于 Context Propagation 的作用,其目的是使安全和其他上下文在数据获取层面可用。

Spring for GraphQL资源库包含 Spring MVCWebFlux的示例。

7. 客户端 Client

Spring for GraphQL包括通过HTTP、WebSocket和RSocket执行GraphQL请求的客户端支持。

7.1. GraphQlClient

GraphQlClient 是一个合约,它为GraphQL请求声明了一个独立于底层传输的通用工作流。这意味着无论底层传输是什么,都使用相同的API执行请求,并且在构建时配置特定于传输的任何内容。

要创建 GraphQlClient ,您需要以下扩展之一:

每个都定义了一个具有与传输相关的选项的生成器。所有构建器都是从一个通用的基本 GraphQlClient Builder 扩展而来的,带有与所有扩展相关的选项。

一旦有了GraphQlClient,就可以开始进行请求。

7.1.1. HTTP

HttpGraphQlClient 使用 WebClient 来通过HTTP执行GraphQL请求

WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

一旦 HttpGraphQlClient 被创建,你就可以开始使用相同的API来 执行请求 execute requests ,与底层传输无关。如果你需要改变任何特定的传输细节,在现有的 HttpGraphQlClient 上使用 mutate() 来创建一个具有自定义设置的新实例:

WebClient webClient = ... ;

HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Perform requests with graphQlClient...

HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Perform requests with anotherGraphQlClient...
7.1.2. WebSocket

WebSocketGraphQlClient 通过共享WebSocket连接执行GraphQL请求。它是使用 SpringWebFlux的 WebSocketClient 构建的,您可以按如下方式创建它:

String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();

HttpGraphQlClient 相比,WebSocketGraphQlClient 是面向连接的,这意味着它需要在发出任何请求之前建立一个连接。当你开始发出请求时,连接就会透明地建立起来。或者,使用客户端的 start() 方法,在任何请求之前明确建立连接。

除了面向连接外,WebSocketGraphQlClient 也是多路复用的。它为所有请求维护一个单一的、共享的连接。如果连接丢失,它将在下一个请求或再次调用 start() 时重新建立。你也可以使用客户端的 stop() 方法,它可以取消正在进行的请求,关闭连接,并拒绝新的请求。

为每个服务器使用一个 WebSocketGraphQlClient 实例,以便为该服务器的所有请求提供一个单一、共享的连接。每个客户端实例建立自己的连接,这通常不是单一服务器的意图。

一旦创建了 WebSocketGraphQlClient,你就可以开始使用相同的API 执行请求execute requests ,而不依赖于底层传输。如果你需要改变任何特定的传输细节,请在现有的 WebSocketGraphQlClient 上使用 mutate() 来创建一个具有自定义设置的新实例。

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use graphQlClient...

WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherGraphQlClient...
Interceptor

除了执行请求之外,GraphQL over WebSocket 协议还定义了许多面向连接的消息。例如,客户端发送“connection_init”,服务器在连接开始时以“connection_ack”响应。

对于WebSocket传输特定的拦截,您可以创建 WebSocketGraphQlClientInterceptor

static class MyInterceptor implements WebSocketGraphQlClientInterceptor {

    @Override
    public Mono<Object> connectionInitPayload() {
        // ... the "connection_init" payload to send
    }

    @Override
    public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
        // ... the "connection_ack" payload received
    }
}

将上述拦截器 Register 为任何其他 GraphQlClientInterceptor,并使用它拦截GraphQL请求,但请注意,最多可以有一个WebSocketGraphQl客户端 interceptor 类型的拦截器。

7.1.3. RSocket

RSocketGraphQlClient使用 RSocketRequester 在RSocket请求上执行GraphQL请求。

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlClient client = RSocketGraphQlClient.builder()
        .clientTransport(transport)
        .build();

HttpGraphQlClient 相反,RSocketGraphQlClient 是面向连接的,这意味着它需要在发出任何请求之前建立一个会话。当你开始提出请求时,会话就会透明地建立。或者,使用客户端的 start() 方法,在任何请求之前明确地建立会话。

RSocketGraphQlClient 也是复用的。它为所有请求维护一个单一的、共享的会话。如果会话丢失,它将在下一个请求或再次调用 start() 时被重新建立。你也可以使用客户端的stop() 方法,该方法取消正在进行的请求,关闭会话,并拒绝新请求。

为每个服务器使用一个RSocketGraphQlClient 实例,以便对该服务器的所有请求拥有一个单一的共享会话。每个客户端实例建立自己的连接,这通常不是单一服务器的意图。
一旦创建了 RSocketGraphQlClient ,你就可以开始使用相同的API执行请求 execute requests ,与底层传输无关。

7.1.4. 构造器 Builder

GraphQlClient 为所有扩展的构建器定义了一个具有通用配置选项的父构建器。目前,它允许您配置:

  • DocumentSource 策略,用于加载文件请求的文档

  • 拦截 Interception 已执行的请求

7.2. 请求 Requests

一旦有了 GraphQlClient,就可以开始通过 retrieve()execute() 执行请求,其中前者只是后者的快捷方式。

7.2.1. 检索 Retrieve

下面将检索并解码查询的数据:

String document = "{" +
        "  project(slug:\"spring-framework\") {" +
        "   name" +
        "   releases {" +
        "     version" +
        "   }"+
        "  }" +
        "}";

Mono<Project> projectMono = graphQlClient.document(document) //(1)
        .retrieve("project") //(2)
        .toEntity(Project.class); //(3)

(1)要执行的操作
(2)响应地图中 "data "键下的路径,要从这里解码
(3)将该路径下的数据解码为目标类型

输入文档是一个字符串,可以是字面意思,也可以通过代码生成的请求对象产生。你也可以在文件中定义文档,并使用文档源( Document Source )来按文件名重命名它们。

路径是相对于 “data “键的,对于嵌套字段使用简单的点(”.”)分隔符号,对于列表元素使用可选的数组索引,例如 "project.name ""project.release[0].version"

如果给定的路径不存在,或者字段值为空并且有错误,解码会导致 FieldAccessExceptionFieldAccessException 提供对响应和字段的访问:

Mono<Project> projectMono = graphQlClient.document(document)
        .retrieve("project")
        .toEntity(Project.class)
        .onErrorResume(FieldAccessException.class, ex -> {
            ClientGraphQlResponse response = ex.getResponse();
            // ...
            ResponseField field = ex.getField();
            // ...
        });
7.2.2. 执行 Execute

检索(Retrieve )只是从响应映射中的单个路径解码的快捷方式。要获得更多控制,请使用 execute 方法并处理响应:

例如:

Mono<Project> projectMono = graphQlClient.document(document)
        .execute()
        .map(response -> {
            if (!response.isValid()) {
                // Request failure... //(1)
            }

            ResponseField field = response.field("project");
            if (!field.hasValue()) {
                if (field.getError() != null) {
                    // Field failure... //(2)
                }
                else {
                    // Optional field set to null... //(3)
                }
            }

            return field.toEntity(Project.class); //(4)
        });
  • 响应没有数据,只有错误
  • 字段是空的,并且有一个相关的错误
  • 被其 DataFetcher 设置为 null 的字段
  • 解码给定路径的数据
7.2.3. 文档源 Document Source

请求的文件是一个字符串,可以定义在一个本地变量或常量中,也可以通过代码生成的请求对象产生。

你也可以在classpath上的 "graphql-documents/"下创建扩展名为 .graphql.gql 的文档文件,并通过文件名来引用它们。

例如,在 src/main/resources/graphql-documents 中给定一个名为 projectReleases.graphql 的文件,其内容为:

src/main/resources/graphql-documents/projectReleases.graphql

query projectReleases($slug: ID!) {
    project(slug: $slug) {
        name
        releases {
            version
        }
    }
}

然后,您可以:

Mono<Project> projectMono = graphQlClient.documentName("projectReleases") //(1)
        .variable("slug", "spring-framework") //(2)
        .retrieve()
        .toEntity(Project.class);

(1)从 "projectReleases.graphql "加载文件
(2)提供变量值。

IntelliJ的 "JS GraphQL "插件支持具有代码完成功能的GraphQL查询文件。

你可以使用GraphQlClient Builder来定制 DocumentSource,以便按名称加载文件

7.3. 订阅请求 Subscription Requests

GraphQlClient 可以通过支持它的传输方式执行订阅。目前,只有WebSocket传输支持GraphQL流,所以你需要创建一个 WebSocketGraphQlClient

7.3.1. 检索 Retrieve

要启动订阅流,请使用 retrieveSubscription,它类似于检索单个响应,但返回一个响应流,每个响应流都解码为一些数据:

Flux<String> greetingFlux = client.document("subscription { greetings }")
        .retrieveSubscription("greeting")
        .toEntity(String.class);

订阅流可以以下列结尾:

  • SubscriptionErrorException,如果服务器以包含一个或多个GraphQL错误的显式“错误”消息结束订阅。该异常提供对从该消息中解码的GraphQL错误的访问。
  • GraphQlTransportException,如 WebSocketDisconnectedException,如果基础连接已关闭或丢失,在这种情况下,您可以使用重试运算符重新建立连接并再次启动订阅。
7.3.2. 执行 Execute

检索(Retrieve)只是从每个响应映射中的单个路径解码的快捷方式。要获得更多控制,请使用 executeSubscription方法并直接处理每个响应:

Flux<String> greetingFlux = client.document("subscription { greetings }")
        .executeSubscription()
        .map(response -> {
            if (!response.isValid()) {
                // Request failure...
            }

            ResponseField field = response.field("project");
            if (!field.hasValue()) {
                if (field.getError() != null) {
                    // Field failure...
                }
                else {
                    // Optional field set to null... (3)
                }
            }

            return field.toEntity(String.class)
        });

7.4. 拦截 Interception

您可以创建 GraphQlClientInterceptor 来拦截通过客户端的所有请求:

static class MyInterceptor implements GraphQlClientInterceptor {

    @Override
    public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
        // ...
        return chain.next(request);
    }

    @Override
    public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
        // ...
        return chain.next(request);
    }
}

创建拦截器后,通过client builder 注册它:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
        .interceptor(new MyInterceptor())
        .build();

8. 测试 Testing

Spring for GraphQL为通过HTTP、WebSocket和RSocket测试GraphQL请求以及直接针对服务器测试提供了专门的支持。

要利用这一点,请将 spring-graphql-test 添加到你的构建中:

# gradle
dependencies {
    // ...
    testImplementation 'org.springframework.graphql:spring-graphql-test:1.1.0'
}
<!-- maven -->
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.graphql</groupId>
        <artifactId>spring-graphql-test</artifactId>
        <version>1.1.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

8.1. GraphQlTester

GraphQlTester是一个契约,它声明了一个测试GraphQL请求的通用工作流程,它独立于底层传输。这意味着无论底层传输是什么,都可以用相同的API来测试请求,任何特定的传输都是在构建时配置的。

要创建一个通过客户端执行请求的GraphQlTester,你需要以下扩展之一:

要创建一个GraphQlTester,它在服务器端执行测试,没有客户端:

每一个都定义了一个与传输相关的选项的构建器。所有的构建器都是从一个共同的、基本的GraphQlTester生成器扩展而来,具有与所有扩展相关的选项。

8.1.1. HTTP

HttpGraphQlTester 使用 WebTestClient 通过HTTP执行GraphQL请求,无论是否使用实时服务器,这取决于 WebTestClient 的配置方式。

要在没有实时服务器的情况下在Spring WebFlux中进行测试,请指向声明GraphQL HTTP端点的Spring配置:

ApplicationContext context = ... ;

WebTestClient client =
        WebTestClient.bindToApplicationContext(context)
                .configureClient()
                .baseUrl("/graphql")
                .build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

要在Spring MVC中进行测试,不需要实时服务器,使用 MockMvcWebTestClient 进行同样的测试。

ApplicationContext context = ... ;

WebTestClient client =
        MockMvcWebTestClient.bindToApplicationContext(context)
                .configureClient()
                .baseUrl("/graphql")
                .build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

或者针对运行在一个端口上的实时服务器进行测试:

WebTestClient client =
        WebTestClient.bindToServer()
                .baseUrl("http://localhost:8080/graphql")
                .build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

一旦 HttpGraphQlTester 被创建,你就可以开始使用相同的API来执行请求( execute requests ),与底层传输无关。如果你需要改变任何特定的传输细节,在现有的 HttpSocketGraphQlTester 上使用 mutate() 来创建一个具有自定义设置的新实例:

HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use tester...

HttpGraphQlTester anotherTester = tester.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherTester...
8.1.2. WebSocket

WebSocketGraphQlTester 通过一个共享的WebSocket连接执行GraphQL请求。它是使用Spring WebFlux的 WebSocketClient 构建的,你可以按以下方式创建它:

String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();

WebSocketGraphQlTester 是面向连接和多路复用的。每个实例都为所有请求建立自己的单个共享连接。通常,您希望每个服务器只使用一个实例。

一旦创建了 WebSocketGraphQlTester,就可以开始使用相同的API执行请求,而不依赖于底层传输。如果您需要更改任何特定于传输的详细信息,请在现有 WebSocketGraphQlTester 上使用 mutate() 创建具有自定义设置的新实例:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use tester...

WebSocketGraphQlTester anotherTester = tester.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherTester...

WebSocketGraphQlTester 提供了一个 stop()方法,你可以用它来关闭WebSocket连接,例如在测试运行后。

8.1.3. RSocket

RSocketGraphQlTester 使用来自spring消息传递的 RSocketRequest 通过RSocket执行GraphQL请求:

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlTester client = RSocketGraphQlTester.builder()
        .clientTransport(transport)
        .build();

RSocketGraphQlTester 是面向连接和多路复用的。每个实例为所有请求建立自己的单个共享会话。通常,您希望每个服务器只使用一个实例。您可以在测试仪上使用 stop() 方法显式关闭会话。

一旦创建了 RSocketGraphQlTester,就可以开始使用相同的API执行请求( execute requests ),而不依赖于底层传输。

8.1.4. GraphQlService

很多时候,在服务器端测试GraphQL请求就足够了,而无需使用客户端通过传输协议发送请求。要直接针对 ExecutionGraphQlService 进行测试,请使用 ExecutionGrafQlServiceTester 扩展:

GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);

创建 ExecutionGraphQlServiceTester 后,就可以开始使用相同的API执行请求( execute requests ),而不依赖于底层传输。

8.1.5. WebGraphQlHandler

GraphQlService 扩展允许您在服务器端进行测试,而无需客户端。然而,在某些情况下,使用给定的模拟传输输入进行服务器端传输处理是有用的。

WebGraphQlTester 扩展允许您在切换到 ExecutionGraphQl 服务以执行请求之前,通过 WebGraphQlInterceptor 链处理请求:

WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.create(handler);

这个扩展的构造器允许你定义HTTP请求的细节:

WebGraphQlHandler handler = ... ;

WebGraphQlTester tester = WebGraphQlTester.builder(handler)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

一旦 WebGraphQlServiceTester 被创建,你就可以开始使用相同的API执行请求( execute requests ),与底层传输无关。

8.1.6. 构造器 Builder

GraphQlTester为所有扩展的构造器定义了一个具有通用配置选项的父构造器。它允许您配置以下内容:

  • errorFilter – 用于抑制预期错误的谓词,因此您可以检查响应的数据
  • documentSource – 一种策略,用于从类路径上的文件或其他任何位置加载请求的文档
  • responseTimeout – 在超时之前等待请求执行完成的时间

8.2. 请求 Requests

一旦你有了 GraphQlTester,你就可以开始测试请求。下面是执行一个项目的查询,并使用 JsonPath 从响应中提取项目的发布版本:

String document = "{" +
        "  project(slug:\"spring-framework\") {" +
        "   releases {" +
        "     version" +
        "   }"+
        "  }" +
        "}";

graphQlTester.document(document)
        .execute()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

JsonPath是相对于响应中的 "数据 "部分而言的。

你也可以在classpath上的 "graphql-test/" 下创建扩展名为 .graphql.gql 的文档文件,并通过文件名来引用它们。

例如,在 src/main/resources/graphql-test 中给定一个名为 projectReleases.graphql 的文件,其内容为:

query projectReleases($slug: ID!) {
    project(slug: $slug) {
        releases {
            version
        }
    }
}

然后你可以使用:

graphQlTester.documentName("projectReleases") //(1)
        .variable("slug", "spring-framework") //(2)
        .execute()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

(1)参照名为 "项目 "的文件

(2)设置slug变量

IntelliJ的 "JS GraphQL "插件支持具有代码完成功能的GraphQL查询文件。

如果一个请求没有任何响应数据,例如突变,使用 executeAndVerify 而不是 execute 来验证响应中没有错误:

graphQlTester.query(query).executeAndVerify();

关于错误处理的更多细节,请参见 Errors

8.3. 订阅 Subscriptions

为了测试订阅,调用 executeSubscription 而不是 execute 来获得响应流,然后使用 Project ReactorStepVerifier来检查该流:

Flux<String> greetingFlux = tester.document("subscription { greetings }")
        .executeSubscription()
        .toFlux("greetings", String.class);  // decode at JSONPath

StepVerifier.create(greetingFlux)
        .expectNext("Hi")
        .expectNext("Bonjour")
        .expectNext("Hola")
        .verifyComplete();

只有 WebSocketGraphQlTester或服务器端的 GraphQlServiceWebGraphQlHandler 扩展支持订阅。

8.4. 错误 Errors

当你使用 verify() 时,响应中 "errors "键下的任何错误将导致断言失败。要抑制一个特定的错误,在 verify() 之前使用错误过滤器:

graphQlTester.query(query)
        .execute()
        .errors()
        .filter(error -> ...)
        .verify()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

你可以在构造器级别注册一个错误过滤器,以应用于所有测试:

WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
        .errorFilter(error -> ...)
        .build();

如果你想验证一个错误确实存在,并且与 filter 相反,如果不存在就抛出一个断言错误,那么就用 exepect 代替:

graphQlTester.query(query)
        .execute()
        .errors()
        .expect(error -> ...)
        .verify()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

你也可以通过一个 Consumer 来检查所有的错误,这样做也会把它们标记为过滤的,所以你也可以检查响应中的数据:

graphQlTester.query(query)
        .execute()
        .errors()
        .satisfy(errors -> {
            // ...
        });

9. 样例 Samples

这个Spring for GraphQL资源库包含了各种情况下的示例应用程序。

你可以通过克隆这个资源库并从你的IDE中运行主要的应用程序类,或者在命令行中输入以下内容来运行这些应用程序:

$ ./gradlew :samples:{sample-directory-name}:bootRun
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值