【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

如何优雅地扩展 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:004563

评论

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

net.coobird.thumbnailator.tasks.UnsupportedFormatException: No suitable ImageReader found for source data.

wjchenge

阿里Java岗个人面经分享(技术三面+技术HR面):Java基础+Spring+JVM+并发编程+算法+缓存

Java架构之路

Java 程序员 架构 面试 编程语言

Python 日期格式和时间以及当前时间和时间戳

HoneyMoose

代码审查:从 ArrayList 说线程安全

mzlogin

Java 代码审查

Day01:VBA和Python入门

披头

办公自动化 IT蜗壳教学 数据科学探究

谈产品和创业方向

Ryan Zheng

创业 产品

Zookeeper.01 - 简介

insight

zookeeper 3月日更

浅谈数仓、数仓模型分层

白程序员的自习室

大数据 解决方案 通用设计模型 数仓

太简单了!看完这篇还能不会SpringCloud+Nginx高并发?

Java架构追梦

Java nginx 架构 面试 SpringCloud

说说RPC架构

Kylin

读书笔记 3月日更 日常积累 RPC架构

熟练使用SSH客户端常用工具SecureCRT

xiezhr

Linux SSH securecrt SSH工具

微服务学习笔记

lenka

3月日更

进入大厂的简历应该是什么样子

我是程序员小贱

3月日更

MySQL主从复制机制

luojiahu

MySQL 主从复制

滚雪球学 Python 之怎么玩转时间和日期库

梦想橡皮擦

28天写作 3月日更

种春草肥禾,织数字天下

脑极体

你最喜欢的奥斯卡电影是哪部?

wbliu85

【LeetCode】设计哈希集合Java题解

Albert

算法 LeetCode 28天写作 3月日更

寻找被遗忘的勇气(十三)

Changing Lin

3月日更

美团工作7年,精华全在这份学习笔记里了,已成功帮助多位朋友拿到5个大厂Offer

Java架构之路

Java 程序员 架构 面试 编程语言

[转]html5设计原理

小江

资深大牛带你了解源码!面试题解析已整理成文档,已拿offer

欢喜学安卓

android 程序员 面试 移动开发

如何实现可靠UDP传输

赖猫

计算机网络 udp TCP/IP

5年Java开发,面试4大厂(阿里、拼多多、字节、美团)后,我总结出大厂高频面试真题及解析

Java架构之路

Java 程序员 架构 面试 编程语言

css高度坍塌与清除浮动

依旧廖凯

28天写作 3月日更

Elasticsearch 近实时搜索 Near Real-Time Search

escray

elastic 28天写作 死磕Elasticsearch 60天通过Elastic认证考试 3月日更

万事开头难——人为推进效应

Justin

心理学 28天写作 游戏设计

资深大牛带你了解源码!最详细的docker中安装并配置redis,实战解析

欢喜学安卓

android 程序员 面试 移动开发

《经济学人》2021年3月13日刊精彩文章导读及资源免费下载

wbliu85

对话微众和红枣:预言机是区块链提供可信数据的基础设施

CECBC

区块链

2021字节面经最新整理: 面试真经/思维导图/学习笔记!火遍全网

比伯

Java 编程 架构 面试 计算机

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