GraphQL 查询出的基础数据和业务需求往往有些差异,需要研发同学加工后才能渲染展示。而通过硬编码的方式对数据进行加工处理无法满足应用快速开发的需求,也与 GraphQL 配置化的思想相悖。本文将介绍如何通过指令和表达式实现 GraphQL 查询的计算能力,以减少代码开发和服务发版上线,提高业务迭代效率。
GraphQL 作为接口描述语言,可对其治理的数据进行便捷的查询,但真实业务场景除了获取基础数据外,往往还需要对数据进行加工处理,概括如下:
如果将 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 实现同理。
自定义指令实现思路相同:
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
ele
、value 为元素值。directive @sortBy(comparator: String!, reversed: Boolean = false) on FIELD
ele
、value 为元素值;使用 @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 # 列表类型参数元素转换
}
使用 @argumentTransform 对参数进行过滤的查询如下:
query simpleArgumentTransformTest($userIds:[Int]){
consumer{
userInfoList(userIds: $userIds)
@argumentTransform(argumentName: "userIds",operateType: FILTER,expression: "ele!=0")
{
userId
name
age
}
}
}
所谓的数据编排就是将一个字段的结果、作为另外一个字段的输入。例如从商品列表中抽取出商品的货主 id 列表、作为参数去获取卖家个人信息详情。
如果仅仅是用 GraphQL 来僵硬地获取数据,则做法为:
queryItemInfo
获取商品基本信息;queryItemInfo
查询结果,获取商品列表中的卖家 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
@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
使用 @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
使用 @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 的平台化开发。
领取专属 10元无门槛券
私享最新 技术干货