写点什么

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

评论

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

今日分享丨微服务架构下查询数据缓存策略

inBuilder低代码平台

微服务 数据缓存

一种PyInstaller中优雅的控制包大小的方法

不在线第一只蜗牛

Java JavaScript 数据库

从零开始带你玩转 AI 变现公开课

霍格沃兹测试开发学社

在 Go 中如何让结构体不可比较?

秃头小帅oi

通过观测云 eBPF Tracing 实现无埋点的全链路追踪

观测云

ebpf

Java元注解介绍

不在线第一只蜗牛

Java

清华大学ChatGLM大模型

霍格沃兹测试开发学社

淘宝商品详情API返回值中的商品材质与成分

技术冰糖葫芦

API Explorer API 测试 API 策略 pinduoduo API

人工智能赋能经管学科创新发展,和鲸助力同济大学打造教研训一体化实践

ModelWhale

人工智能 交叉学科 同济大学 经济与管理 同济大学MBA

openGauss- 智能基数估计

Gauss松鼠会

opengauss

聚焦 AI 加持下泛娱乐场景的技术革新|RTE Plus 声网城市沙龙杭州站

声网

视频审核架构实践

京东科技开发者

汽车乘客热舒适度大挑战,如何利用仿真技术提高汽车环境舒适度

Altair RapidMiner

人工智能 汽车 仿真 智能制造 altair

如何通过关键词搜索API接口,淘宝商品关键词搜索电商API接口揭秘

联讯数据

智谱开源 CogVideoX-5B 视频生成模型,RTX 3060 显卡可运行;曝 OpenAI 模型「草莓」今秋推出

声网

再获殊荣 | 澳鹏Appen获评2024全球数据标注领域Top领军者

澳鹏Appen

数据标注

2025杭州国际智能建筑展览会

AIOTE智博会

智能楼宇展 智能楼宇展会 智能楼宇展览会 智能楼宇博览会

京东平台内容合规的技术与挑战

京东科技开发者

流程挖掘价值实现的加速器!望繁信科技全链路解决方案惊艳刷屏

望繁信科技

数字化转型 流程挖掘 流程资产 流程智能 望繁信科技

京东商品信息快速获取:API返回值实战教程

技术冰糖葫芦

API Explorer API 测试 API 策略 pinduoduo API

高效整理,创造灵活工作空间——iCollections for Mac 🚀🌟

柠檬与橘子

感谢 Fluent Editor 开源富文本首位贡献者!

OpenTiny社区

富文本 OpenTiny 开源前端

Proxyless的多活流量和微服务治理

京东科技开发者

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