AICon日程100%就绪,9折倒计时最后一周 了解详情
写点什么

如何优雅地扩展 GraphQL 系统能力

  • 2021-09-21
  • 本文字数:4832 字

    阅读完需:约 16 分钟

如何优雅地扩展GraphQL系统能力

背景

为什么要扩展 GraphQL 系统能力


GraphQL 可将 API 表示的数据通过解析函数映射到 GraphQL 的 schema 中,为 API 提供一套类型化的完整描述,使得客户端能够根据所需准确地获取相应数据。

 

在真实业务场景中,除了获取基础数据外,往往还会有一些对数据进行加工转换和编排控制的需求,例如对数值字段取精或者转换成展示文案、对列表字段进行排序过滤去重、根据条件判断是否请求查询中的某些字段、将一个字段的解析结果作为另外一个字段的入参等。

 

原生的 GraphQL 查询为获取基础数据提供了便捷,但是计算能力不足导致其结果经常不能满足业务需求,数据往往需要加工转换、甚至经过多次编排查询,才能展示给用户。

GraphQL 的能力扩展机制


GraphQL 提供指令作为执行和校验能力的扩展机制。指令的定义包括指令名称、参数列表、可使用位置和是否可在同一位置重复使用等四个元素,用户可以使用指令描述自定义的执行行为或校验规则。

 

以内置指令@skip为例,该指令定义如下:

directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
复制代码

 

@skip主要是解决指定条件满足时跳过某些字段的获取解析。判断条件结果为指令参数if。该指令可使用的位置有查询字段、命名片段和内联片段,使用时将指令放置在要生效的元素后即可,示例如下:

query myQuery($someTest: Boolean!) {  experimentalField @skip(if: $someTest)}
复制代码

 

在实际业务场景中,是否跳过某些字段获取的条件大多情况需要根据请求变量进行计算判断。例如为 App 渲染数据时,低于指定版本的客户端不用请求某些字段,该条件判断无法通过请求变量只有客户端版本号的原生查询实现。

 

GraphQL 原生指令只有@skip@include@deprecated@specifiedBy ,说明见Type-System.Directives,提供的能力有限,不能满足业务计算所需。

GraphQL 系统能力扩展实践


本文以GraphQL Calculator为例,介绍对 GraphQL 系统能力进行扩展的实践。

 

开源代码托管地址:https://github.com/graphql-calculator/graphql-calculator

指令分类


指令使用位置分为两类:可执行位 ExecutableDirectiveLocation 和类型系统位 TypeSystemDirectiveLocation。

# ExecutableDirectiveLocationQUERY # 查询操作MUTATION # 更新操作SUBSCRIPTION # 订阅操作FIELD # 查询字段FRAGMENT_DEFINITION # 命名片段定义FRAGMENT_SPREAD # 命名片段INLINE_FRAGMENT # 内联片段VARIABLE_DEFINITION # 查询变量

# TypeSystemDirectiveLocationSCALAR # 标量OBJECT # 对象FIELD_DEFINITION # 字段定义ARGUMENT_DEFINITION # 参数定义INTERFACE # 接口UNION # 联合类型ENUM # 枚举ENUM_VALUE # 枚举值INPUT_OBJECT # 输入对象INPUT_FIELD_DEFINITION # 输入字段定义
复制代码

 

GraphQL 规范并不会限制指令只能定义在可执行位或者类型系统位,但是为了明确指令是用在查询上、还是对于类型系统生效,往往只将指令的生效位置限定在其中一种:

 

  • 对于可执行位指令,其作用往往跟业务场景相关。例如,每个查询所要跳过的字段都可能不同,因此@skip的生效位置为FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

  • 对于类型系统位指令,主要是对类型系统本身额外信息、执行行为的描述。 例如@deprecated说明了一个字段将要被废弃的原因,其定义位置为FIELD_DEFINITION | ENUM_VALUE

 

本文重点讲解查询指令的实现:根据不同的业务场景,对查询进行不同的计算。

定义指令


指令应该服务于特定类型的数据结构和通用的算法处理,而不是特定的业务场景,为特定的业务场景定义指令将使得指令系统变得臃肿、难以维护。GraphQL Calculator参考了常见的编程概念对指令进行定义:

  • 字段加工:通过表达式对结果字段进行加工转换;

  • 数组处理:对结果中的数组字段进行过滤、排序、去重;

  • 参数转换:对请求参数进行转换,包括加工、过滤、使用其他字段获取结果进行替换;

  • 数据编排:将指定字段的获取结果作为全局可获取的上下文,为其他字段或参数的加工转换提供可依赖的数据;

  • 控制流:@skip@include拓展版本,通过表达式判断是否请求注解的字段或片断。


指令的命名会直接影响指令的易用性。GraphQL Calculator指令的命名和语义参考了java.util.stream.Stream和 GraphQL 规范原生指令,易于理解和使用,例如@filter@sort@skipBy

执行引擎


GraphQL 的 Java 实现提供了Instrumentation机制,该机制可在查询的各个阶段获取到执行上下文,可对执行信息进行记录、修改。该机制的核心接口有InstrumentationInstrumentationContextInstrumentationState

 

  • Instrumentation

 

Instrumentation主要可获取指令及执行上下文信息,并对数据进行记录、修改。该接口部分方法及说明例举如下:

public interface Instrumentation {    // 创建执行上下文对象    default InstrumentationState createState() {        return null;    }

// 根据本次请求上下文和schema创建执行上下文对象 default InstrumentationState createState(InstrumentationCreateStateParameters parameters) { return createState(); } // 在解析查询dsl前调用 InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters);

// 在验证查询对象前调用 InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters);

// 修改DataFetcher行为 default DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) { return dataFetcher; } // 在列表字段结束之后可进行的回调动作 default InstrumentationContext<ExecutionResult> beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) { return noOp(); }

// 对最终的查询结果进行修改 default CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return CompletableFuture.completedFuture(executionResult); }

......

}
复制代码

 

  • InstrumentationContext

 

InstrumentationContextInstrumentation 部分方法的返回结果,该对象包含两个回调方法,回调动作将在Instrumentation 的方法对应的执行阶段被调用。

public interface InstrumentationContext<T> {

void onDispatched(CompletableFuture<T> result); void onCompleted(T result, Throwable t);}
复制代码

 

  • InstrumentationState

 

InstrumentationState可保存查询产生的中间数据,经常在记录中间数据或在不同的执行线程间传递数据。为了保证该对象可被多个线程同时读写,其实现一般是线程安全的。

 

此外,指令的合法使用往往有些前置条件,例如过滤指令不可用在简单对象或基本类型字段上。GraphQL 的 Java 库提供了基于访问者模式实现的QueryVisitor ,可在其方法中获取到查询的字段、内联片段和片段定义的上下文信息,便于实现自定义的校验规则。

 

public interface QueryVisitor {    void visitField(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment);        void visitInlineFragment(QueryVisitorInlineFragmentEnvironment queryVisitorInlineFragmentEnvironment);

void visitFragmentSpread(QueryVisitorFragmentSpreadEnvironment queryVisitorFragmentSpreadEnvironment);}
复制代码

实现示例


  • 定义指令

 

定义@filter对数组类型字段进行过滤,保留断言表达式predicate 结果为 true 的元素,predicate参数为所注解数组元素的字段名称与字段值的映射 Map。

directive @filter(predicate: String!) on FIELD
复制代码

 

  • 实现指令

 

GraphQL 执行引擎获取查询结果可分为 fetch 和 complete 两个阶段:fetch 阶段根据请求参数和上下文获取该节点的原始数据,并分析该节点类型,递归获取其子孙节点的原始数据;complete 阶段对应 fetch 的递归出栈,处于 complete 阶段的节点及其子孙节点已经全部完成解析和异常处理。

 

例如,对于获取用户详情列表的查询 queryUserList,对应的示意图如下。

 

query queryUserList($userId:[Int]){    # userInfoList类型为 [UserInfo]    userInfoList(userIds: $userId)    {        userId        age        firstName        lastName    }}
复制代码



Instrumentation#beginFieldListComplete中可获取到解析完成的列表字段结果,该方法可过滤不符合断言的元素。继承实现如下:

 

@Overridepublic InstrumentationContext<ExecutionResult> beginFieldListComplete(InstrumentationFieldCompleteParameters parameters) {    return new InstrumentationContext<ExecutionResult>() {        @Override        public void onDispatched(CompletableFuture<ExecutionResult> result) {            // ignored        }        @Override        public void onCompleted(ExecutionResult result, Throwable t) {            if (result == null || result.getData() == null || CollectionUtil.arraySize(result.getData()) == 0) {                return;            }            List<Directive> directives = parameters.getExecutionStepInfo().getField().getSingleField().getDirectives();            if (directives != null && !directives.isEmpty()) {                // 数据过滤                filterResultByDirective(result, directives);            }        }    };}
复制代码

 

由于在Instrumentation#beginFieldListComplete 节点只能获取到数组对象,但不能返回新的对象进行替换,因此需要保证在此获取到的数组类型是可进行过滤操作的,例如java.util.Collection的实现类,不可是不能改变大小的数组类型。可在Instrumentation#instrumentDataFetcher中对 fetch 阶段的结果进行转换,替换为可进行过滤操作的集合类型。

 

  • 校验指令使用

 

通过QueryVisitor实现自定义指令的校验规则,以校验@filter参数表达式不可为空为例,其实现核心代码如下:

public class BasicRule implements QueryVisitor {       @Override    public void visitField(QueryVisitorFieldEnvironment environment) {        // 只在进入该节点的时候进行校验处理,避免重复处理        if (environment.getTraverserContext().getPhase() != TraverserContext.Phase.ENTER) {            return;        }               for (Directive directive : environment.getField().getDirectives()) {            if (Objects.equals(directive.getName(), FILTER.getName())) {                GraphQLType innerType = GraphQLTypeUtil.unwrapNonNull(                        environment.getFieldDefinition().getType()                );                // 判断指令指令是否注解在列表类型字段上                if (!GraphQLTypeUtil.isList(innerType)) {                    String errorMsg = String.format("@filter must define on list type, instead {%s}.", fieldFullPath);                    addValidError(location, errorMsg);                    continue;                }}
复制代码


  • 使用指令

 

获取用户详情列表时,通过@filter过滤出年龄大于等于 18 的用户。

query filterUserByAge($userId:[Int]){    userInfoList(userIds: $userId)    @filter(predicate: "age>=18")    {        userId        age        firstName        lastName    }}
复制代码


参考资料:


 

作者介绍:


杜艮魁,GraphQL Calculator 作者,GraphQL Java 活跃 contributor。

2021-09-21 08:004787

评论

发布
暂无评论
发现更多内容

2021|南吕

南吕

生活随想 4月日更

Redis的常见问题

赖猫

c++ redis Linux 后端

安卓rxjava面试,面试一路绿灯Offer拿到手软,吊打面试官系列!

欢喜学安卓

android 程序员 面试 移动开发

【Node专题】Node 与 Go 的认识

南吕

后端 Node 4月日更

容器 & 服务: 扩容

程序员架构进阶

容器 k8s 28天写作 弹性扩容 4月日更

2个月从0到1,一年5次迭代,百度“量桨”效率喷涌背后的工作秘诀

脑极体

Vue源码思想在工作中的应用

执鸢者

Vue 大前端

小米java社招面试记录,带备战思路

Java架构师迁哥

想拿到10k-40k的offer,这些技能必不可少!作为程序员的你了解吗?

Java架构师迁哥

区块链如何推动数字化转型?

CECBC

区块链

BUG!从编写 Loader 到窥探大佬 Debug 全过程

HZFEStudio

小程序 webpack 构建工具

当我看技术文章的时候,我在想什么?

why技术

Java

一场关于演讲的演讲

Jxin

读《小岛经济学有感》

箭上有毒

读书笔记 4月日更

图算法系列之深度优先搜索(一)

Silently9527

Java 深度优先搜索 图算法

怎么做到的?3个月入职蚂蚁金服(Java岗)从年薪10W到年薪30W

Java架构师迁哥

150页的剑指Offer解答PDF,它来了!!!

秦怀杂货店

安卓rxjava使用,4面字节跳动拿到Offer,面试必问

欢喜学安卓

android 程序员 面试 移动开发

聪明人的训练(二十四)

Changing Lin

4月日更

深入理解Spring框架之AOP子框架

邱学喆

aop 动态代理 cglib ProxyConfig AspectJ

四面拿到京东Java岗 30K offer 全过程分享

Java架构师迁哥

Java虚拟机原理

风翱

JVM 4月日更

翻译:《实用的Python编程》InstructorNotes

codists

Python

MBP恢复记(体验rm -rf /*)

SamGo

学习

如何减少管理层级?

石云升

团队建设 28天写作 职场经验 管理经验 4月日更

你的故事,触动了我的心

小天同学

读后感 读书总结 4月日更 皮囊

【go专题】Context的理解

南吕

Go 语言 4月日更

当时尚撞上区块链,为潮酷创意赋予专属

CECBC

时尚产业

比微信文件传输助手更好用的传输工具|Telegram

彭宏豪95

微信 效率 文件传输 4月日更 Telegram

解决方案的设计与积累——课程总结

Deborah

网络协议学习笔记Day3

穿过生命散发芬芳

网络协议 4月日更

如何优雅地扩展GraphQL系统能力_文化 & 方法_杜艮魁_InfoQ精选文章