2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

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

评论

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

【论文速读】| APILOT:通过避开过时API陷阱,引导大语言模型生成安全代码

云起无垠

Elasticsearch开源仓库404 7万多star一夜清零

吴脑的键客

数据库 搜索引擎

10分钟写一个tidb-ai机器人帮你解答tidb问题

TiDB 社区干货传送门

新版本/特性解读

TiDB 集群组件间开启 TLS(双向认证

TiDB 社区干货传送门

7.x 实践

TiKV Raft Store 内存管理的原理与实现丨TiKV 源码解读(二十三)

TiDB 社区干货传送门

知乎 PB 级数据:超大规模TiDB集群管控实践

TiDB 社区干货传送门

实践案例 集群管理 数据库架构设计 HTAP 场景实践

基于微服务SDK框架与JavaAgent技术,低成本助力应用高效发布

华为云开发者联盟

微服务 灰度发布 java-agent Java Chassis 3

HyperWorks二维网格划分及拓扑改进

智造软件

Hypermesh 网格划分 有限元

ChatGPT Search 上线 允许用户像使用搜索引擎一样完成即时搜索

吴脑的键客

ChatGPT Azure OpenAI

TiDB数据库出现性能问题,如何利用数据库性能诊断工具DBdoctor一分钟诊断!

TiDB 社区干货传送门

性能调优 实践案例 管理与运维 安装 & 部署 应用适配

大数据集群搭建,CDH让你事半功倍!

敏捷调度TASKCTL

cloudera CDH 集群搭建 CDH 大数据 Hadoop 集群迁移

软件测试学习笔记丨Flask框架-集成Swagger文档

测试人

软件测试

TiDB Vector 本地部署体验

TiDB 社区干货传送门

8.x 实践 TiDB Cloud TiDB Vector

GreptimeDB vs. SQLite —— 高通 8155 平台上的性能对比报告

Greptime 格睿科技

sqlite 数据库 边缘计算 嵌入式 性能报告

Llama 3.2 Vision & Molmo:多模态开源生态系统基础

Baihai IDP

程序员 AI Baihai IDP Llama 3.2 Vision Molmo

鸿蒙网络编程系列42-仓颉版域名解析示例

长弓三石

DevEco Studio 开发实例 HarmonyOS NEXT 网络与连接

实验室辅助管理系统(源码+文档+部署+讲解)

深圳亥时科技

商协会管理系统(源码+文档+部署+讲解)

深圳亥时科技

智慧停车系统(源码+文档+部署+讲解)

深圳亥时科技

Merge-region 原理及常见问题

TiDB 社区干货传送门

故障排查/诊断

CST如何进行局部网格剖分

思茂信息

cst cst使用教程 电磁仿真

观测云:简化复杂的云账单,让企业轻松掌控云成本

观测云

云账单

1024程序员节 | 华为与开发者共筑智能应用新生态

极客天地

突破 RAG 局限,KAG 专业领域知识服务框架正式开源!

可信AI进展

人工智能如何从神话走向科学的?

天津汇柏科技有限公司

人工智能 AI 人工智能

用二维码展示信息,有哪些常见应用场景

草料二维码

Emeritus硅谷AI深度研学之旅圆满落幕,探索AI前沿投资洞察!

科技汇

TiDB br日志备份PermissionDenied

TiDB 社区干货传送门

实践案例 备份 & 恢复

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