InfoQ Geekathon 大模型技术应用创新大赛 了解详情
写点什么

GraphQL-Calculator 开源:基于指令和表达式实现查询的动态计算

  • 2021-08-18
  • 本文字数:5625 字

    阅读完需:约 18 分钟

GraphQL-Calculator开源:基于指令和表达式实现查询的动态计算

GraphQL 查询出的基础数据和业务需求往往有些差异,需要研发同学加工后才能渲染展示。而通过硬编码的方式对数据进行加工处理无法满足应用快速开发的需求,也与 GraphQL 配置化的思想相悖。本文将介绍如何通过指令和表达式实现 GraphQL 查询的计算能力,以减少代码开发和服务发版上线,提高业务迭代效率。

背景

计算需求概述

GraphQL 作为接口描述语言,可对其治理的数据进行便捷的查询,但真实业务场景除了获取基础数据外,往往还需要对数据进行加工处理,概括如下:


  1. 结果字段加工:对基础数据进行加工后展示。例如将‘分’单位的数字价格转为‘元’单位的价格文案、使用默认值兜底 null、将状态 code 转换成对应文案等;

  2. 列表过滤、排序:通过 id 列表查询出数据详情列表之后,往往需要根据详情信息对结果列表进行过滤排序,例如过滤掉商品列表中在售状态为 false 的商品,将商品按照销量进行排序;

  3. 参数处理:对参数列表进行过滤,例如过滤掉 itemIdList 中为 0 的 itemId;对参数进行转换,例如将 Redis 的 key 前缀拼接到 itemId 前边、作为请求 Redis 数据源的 key;

  4. 数据编排依赖:类似于 MySQL 中的子查询,将一个字段的解析结果作为另一个字段的获取参数;

  5. 控制流:通过请求变量判断是否请求指定的字段,GraphQL 原生指令 @include 和 @skip 只支持 bool 类型的变量,但真实的业务场景判断规则更加复杂,往往存在逻辑计算。

为何使用指令

如果将 GraphQL 仅作为僵硬的取数工具,就违背了 GraphQL 配置化的初衷,也忽略了 GraphQL 的扩展能力。 作为“接口查询语言”,GraphQL 提供指令作为查询执行能力的扩展机制。指令类似于 Java 注解,可对其进行注解的语言元素进行额外的信息描述。

 

Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.

 

In some cases, you need to provide options to alter GraphQL’s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.

 

作为 GraphQL 官方指定的能力拓展机制,GraphQL 生态的框架对指令有更好的支持,基于指令的能力拓展和框架本身也具有更好的兼容性。

如何使用指令

指令主要是对 GraphQL 语言元素的信息描述,例如使用 @include 指令描述是否请求某个字段:

query userInfo($userId:Int, $needEmail:Boolean!){    userInfo(userId:$userId){        userId        userName        age        # 当 $needEmail 为true时才会请求、返回email字段        email @include(if:$needEmail)    }}
复制代码


GraphQL-java框架集成了GraphQL协议原生指令:在执行引擎中判断每个字段是否带有 @incldue 指令,有的话则根据起用到的变量信息判断是否请求该字段,@skip 实现同理。

 

自定义指令实现思路相同:

  1. 根据数据处理需求设计指令;

  2. 在查询中使用指令对查询元素进行注解描述;

  3. 在查询引擎中获取指令信息和查询上下文,执行符合指令语义的行为。

 

GraphQL-java 提供了Instrumentation机制,该机制类似于 spring 中的切面,可在数据处理的各个阶段获取到校验、查询各个阶段的上下文信息,并可改变执行上下文信息和结果、或中断查询的执行。

问题和方案

基于 Instrumentation,GraphQL-calculator实现了一套具有参数处理、结果字段加工、数据依赖编排和控制流能力的指令集。该指令集可使表达式对上下文数据进行加工转换,其默认表达式引擎为aviatorscript

集合过滤、排序

问题简述

通过 id 列表获取到数据详情集合之后,往往需要根据数据详情对集合进行过滤,或者按照指定规则对集合进行排序。

 

如下查询,通过商品 id 列表获取到商品详情集合,业务场景需要将库存为 0、非在售状态的商品过滤掉,然后按照售价递增排序。 如果硬编码形式实现则需要走编码、调试、部署、上线等步骤,流程长、响应慢。

 

query commodityInfo($ItemIds:[Int]){    commodity{        filteredItemList: itemList(itemIds: $ItemIds){            itemId            onSale            name            salePrice            stockAmount        }    }}
复制代码

解决方案

针对集合过滤、排序的需求,GraphQL-calculator 定义了 @filter 和 @srotBy 指令对集合进行动态处理:

 

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


  • predicate:过滤判断表达式,会应用在每个集合元素上,结果为 true 的元素会被保留,当 @filter 用在叶子节点上时,表达式变量为 key 为ele、value 为元素值。

 

directive @sortBy(comparator: String!, reversed: Boolean = false) on FIELD
复制代码


  • comparator:用户比较列表元素顺序的比较器,当 @filter 用在叶子节点上时,表达式变量为 key 为ele、value 为元素值;

  • reversed:是否逆序排序;

 

使用 @filter 和 @sortBy 指令对商品列表进行过滤并排序的查询如下:

query filterUnSaleAndSortCommodity($ItemIds:[Int]){    commodity{        filteredItemList: itemList(itemIds: $ItemIds)        @filter(predicate: "onSale && stockAmount>0")        @sortBy(comparator: "salePrice")        {            itemId            onSale            name            salePrice            stockAmount        }    }}
复制代码

参数处理

问题简述

在调用数据源接口时,经常需要把上游传递的参数进行过滤、去重或者转换等,不同的业务场景可能有不同的转换规则。有时候线上出现意想不到的参数,也需要我们通过配置化的方式对参数进行即刻生效的处理,而非紧急修改代码、上线这种漫长的流程。

 

例如下述查询,查询在线用户详情信息。调用方传递的参数可能存在未登录用户参数,即 userId 为 0。如果数据源接口没有兼容这种异常情况、则会导致接口意想不到的行为或结果。此时需要我们对参数进行过滤。

 

query simpleArgumentTransformTest($userIds:[Int]){   consumer{       userInfoList(userIds: $userIds){           userId           name           age       }   }}
复制代码

解决方案

针对需要对参数进行处理的场景,GraphQL-calculator 定义了 @argumentTransform 对请求参数进行处理,包括参数转换、列表参数过滤、元素转换:

directive @argumentTransform(argumentName:String!, operateType:ParamTransformType!, expression:String!, dependencySources:[String!]) on FIELD

enum ParamTransformType{ MAP # 参数转换 FILTER # 列表类型参数过滤 LIST_MAP # 列表类型参数元素转换}
复制代码


  • argumentName:进行转换的参数名称,参数必须定义在被注解的字段上;

  • operateType:操作类型;

  • expression:计算新值、或者对参数进行过滤的表达式;

  • dependencySources:表达式依赖的 source,如果和参数变量同名则会覆盖后者,source 具体含义见数据编排。

 

使用 @argumentTransform 对参数进行过滤的查询如下:

query simpleArgumentTransformTest($userIds:[Int]){   consumer{       userInfoList(userIds: $userIds)       @argumentTransform(argumentName: "userIds",operateType: FILTER,expression: "ele!=0")       {           userId           name           age       }   }}
复制代码

数据编排

问题简述

所谓的数据编排就是将一个字段的结果、作为另外一个字段的输入。例如从商品列表中抽取出商品的货主 id 列表、作为参数去获取卖家个人信息详情。

 

如果仅仅是用 GraphQL 来僵硬地获取数据,则做法为:


  1. 通过第一次查询queryItemInfo获取商品基本信息;

  2. 解析queryItemInfo查询结果,获取商品列表中的卖家 id 列表;

  3. 使用第 2 步解析的卖家 id 列表,获取卖家个人信息;

# step 1: 获取商品详情列表

query queryItemInfo($itemIds:[Int]){ commodity{ itemList(itemIds: $itemIds){ itemId # 商品货主id sellerId name salePrice stockAmount } }}



# step 2:解析queryItemInfo结果,获取$sellerIds;





# step 3:获取卖家详情列表

query querySellerInfo($sellerIds:[Int]){ business{ sellerInfoList(sellerIds: $sellerIds){ sellerId name age email } }}
复制代码

解决方案

类似 MySQL 中的子查询,如果依赖逻辑合理,任何字段的获取结果都应当可以作为请求其他字段的参数。GraphQL-calculator 通过 @fetchSource 对作为参数的字段进行描述:

directive @fetchSource(name: String!, sourceConvert:String) on FIELD
复制代码


  • name:被注解的字段作为被依赖数据时的 source 名称,一个查询中的 source 名称具有唯一性;

  • sourceConvert:对 source 进行转换的表达式,如果被注解的字段在列表中、则每个元素都会被该表达式转换。

 

@fetchSource 是进行数据编排的基础,不管是作为参数进行流程编排、还是后续讲到的数据加工。当要用到其他字段结果作为参数进行计算时、都是通过 @fetchSource 将被依赖的数据进行描述、保存为其他字段指令可获取的数据。

 

通过指令实现数据依赖编排的查询如下:


query simpleOrchestration($itemIds:[Int]){    commodity{        itemList(itemIds: $itemIds){            itemId            # 将被依赖的数据使用@fetchSource进行描述            sellerId @fetchSource(name: "sellerIdList")            name            salePrice            stockAmount        }    }

business{ sellerInfoList(sellerIds: 1) # 用@argumentTransform对参数进行转换 @argumentTransform(argumentName: "sellerIds",operateType: MAP,expression: "sellerIdList",dependencySources: ["sellerIdList"]) { sellerId name age email } }}
复制代码

结果加工

问题简述

当从某个业务域接口获取到基础数据后,往往需要对数据进行加工处理后才能在页面展示,例如根据用户 id 拼接出用户主页链接,将‘分’单位的数字价格转为‘元’单位的价格文案、使用默认值兜底 null、将状态 code 转换成对应文案等。

 

示例为获取商品基本信息的查询,‘#’ 注解的信息为需要加工处理出的字段,该查询所要加工的字段已经结构化的清晰的展示出来,要执行的加工逻辑通用简单。

 

query itemBaseInfo_case01($itemIds:[Int]){    commodity{        itemList(itemIds: $itemIds){            itemId            name                        # 分->元:salePrice/100            salePrice                       # 1. 自营;2.第三方店铺:分别使用文案 自营正品、三方好货 描述            itemType        }    }}
复制代码

解决方案

GraphQL-calculator 定义了 @map 指令用于字段结果的加工计算,该指令可通过参数 dependencySources 获取到其他字段结果、实现类似于 mysql 中 join 计算的能力。


directive @map(mapper:String!, dependencySources:String) on FIELD
复制代码


  • mapper:计算被注解字段值的表达式,被注解字段绑定的 DataFetcher 不会执行;

  • dependencySources:表达式依赖的 source,sourceName 如果和父节点绑定 DataFetcher 的获取结果 key 相同,则计算表达式时会覆父节点中的数据。

 

使用 @map 对字段结果进行加工的查询如下:

query itemBaseInfo_case01($itemIds:[Int]){    commodity{        itemList(itemIds: $itemIds){            itemId            name            # 分->元:salePrice/100            salePrice @map(mapper:"salePrice/100")             # 1. 自营;2.第三方店铺:分别使用文案 自营正品、三方好货 描述            itemTypeDesc: name @map(mapper:"itemType==1?' 自营正品':'三方好货'")        }    }}
复制代码

控制流

问题简述

GraphQL 内置了 @skip 和 @include 来决定是否请求指定字段,其参数为 bool 类型。但真实的场景往往存在逻辑计算,无法使用一个简单的 bool 类型参数表示是否请求指定字段。

 

如下查询,期望只有 v2 版本的客户端才可以看到 email 字段。这种if控制流的实现放在 DataFetcher 中硬编码实现则不够灵活,难以满足各种场景的控制需求。


query userInfoQuery($userId:Int){    consumer{        userInfo(userId: $userId){            userId            age            name            # 期望只有v2版本的客户端可以获取到该字段            # 客户端版本可以作为请求变量            email        }    }}
复制代码

解决方案

GraphQL-calculator 定义了 @includeBy 指令判断是否请求指定字段,该指令可理解为 GraphQL 内置指令 @include 的拓展版本,但起判断逻辑为表达式、表达式参数为所有请求变量。


directive @includeBy(predicate: String!, dependencySources:[String!]) on FIELD
复制代码


  • predicate:判断是否解析该字段的表达式;

  • dependencySources:表达式参数除了请求变量外,还可使用其他 source。

 

使用 @includeBy 判断是否请求 email 的查询如下:


query queryMoreDetail_case01($userId:Int,$clientVersion:String){    consumer{        userInfo(            userId: $userId,            # 受限于GraphQL原生语法校验,变量必须被明确的作为参数使用            clientVersion: $clientVersion){            userId            age            name            # 只在v2版本的客户端中展示            email @includeBy(predicate: "clientVersion == 'v2'")        }    }}
复制代码


参考资料:



作者介绍:


杜艮魁,开源组件 GraphQL-java 的活跃 contributor,主要参与了 15、16 版本的指令能力升级和语法校验,GraphQL 协议 contributor。先后在美团快手从事 GraphQL 的平台化开发。

活动推荐:

2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。

2021-08-18 09:003416

评论

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

解决问题

Nydia

2021年11月券商App行情刷新及交易体验评测报告

博睿数据

链路分析 K.O “五大经典问题”

阿里巴巴云原生

负载均衡 阿里云 云原生 流量 链路分析

架构模块二作业

Vincent

「架构实战营」

请回答,轻巧又见效的数字化转型,企业该如何做?

ToB行业头条

电脑安全小知识

喀拉峻

黑客 网络安全 安全 计算机基础

5分钟认识802.11标准,言简意赅!

Ethereal

网络技术 无线技术 网络技术联盟站 802.11

46 K8S之集群高可用

穿过生命散发芬芳

k8s 28天写作 12月日更

LabVIEW软件、驱动安装及编程方法(理论篇—2)

不脱发的程序猿

机器视觉 LabVIEW 工业自动化

实现macOS热门功能,我只用了60行代码!

Jackpop

读《思辨与立场》-04自我理解

wood

28天写作 批判性思维 思辨与立场

CSS之选择器(八):+ 和 ~

Augus

CSS 12月日更

15.  《重学JAVA》--Lambda表达式

杨鹏Geek

Java 25 周年 28天写作 12月日更

Holos: 我来了,我是个大块头。

mtfelix

28天写作

Hoo虎符研究院 | 币海寻珠——近期公链事件(2021.12.16)

区块链前沿News

Hoo虎符 虎符交易所

Hystrix Dashboard

李子捌

微服务 28天写作 12月日更

增长方法论:五步验证法

石云升

产品经理 28天写作 产品增长 12月日更

低代码助推生物制药企业CDMO的“数字化转型”之路

优秀

低代码 制药企业CDMO

【Redis核心原理专题】(1)「技术提升系列」分析探究如何实现LFU的热点key发现机制以及内部的Scan扫描技术的原理

洛神灬殇

redis 12月日更 Redis Scan Redis 热点key Redis服务

东数西算,风起中原

脑极体

【架构师训练营】模块二作业

樰巳-堕~Horry

架构实战营 「架构实战营」

阿里云分布式容器平台即将全面启动公测!

阿里巴巴云原生

阿里云 容器 分布式 云原生

从 “香农熵” 到 “告警降噪” ,如何提升告警精度?

阿里巴巴云原生

阿里云 云原生 告警 可观测 信息熵

分析一下微信朋友圈的高性能复杂度

Sindorei

「架构实战营」

工业机器视觉系统的构成与开发过程(理论篇—1)

不脱发的程序猿

计算机视觉 机器视觉 图像处理 LabVIEW 工业自动化

帮你积累音视频知识,Agora 开发者漫游指南正式启航

声网

人工智能 音视频

重磅|火山引擎边缘计算节点服务正式发布!

火山引擎边缘云

边缘计算 网络 云计算,

设计模式的提出

卢卡多多

28天写作 12月日更

架构实战营 4 期 - 第2周作业

周念

「架构实战营」

云原生新边缘:火山引擎边缘计算最佳实践

火山引擎边缘云

云计算 云原生 边缘计算

2021的科技卦象·震·到元宇宙玩“躲猫猫”

脑极体

  • 扫码添加小助手
    领取最新资料包
GraphQL-Calculator开源:基于指令和表达式实现查询的动态计算_语言 & 开发_杜艮魁_InfoQ精选文章