问题背景
在实际的业务场景中,有时候有对列表元素进行去重的需求,比如对于用户列表、每个年龄段只保留一个用户;对于商品列表、每种品类只保留一个商品。
不同的业务场景往往有不同的去重需求,本文介绍通过表达式和指令、在graphql查询中以配置化的方式实现对列表去重,满足需求的快速开发和即时生效。
解决方案
定义指令
graphql提供了指令机制用于执行能力的动态拓展、指令机制类似于java中的注解。
我们通过定义@distinct指令对列表数据进行去重,该指令只可用在列表类型字段上,其命名和语义参考了
java.util.stream.Stream。
directive @distinct(comparator:String) on FIELD
- comparator:使用该表达式计算元素的唯一key,唯一key相同的元素会被去重,保留有序列表第一个元素。
comparator为可选参数,当未设置该参数时使用
System.identityHashCode(object)判断元素是否为相同对象。对列表元素进行去重,当元素为基本类型时、表达式变量为key为ele、value为元素值。
示例查询
根据年龄对用户列表进行去重,每个年龄只保留一个用户。
query distinctUserInfoListByAge($userIds:[Int]){
consumer{
distinctUserInfoList: userInfoList(userIds: $userIds)
@distinct(comparator: "age")
{
userId
name
age
email
}
}
}
代码实现
该能力可通过graphql-calculator实现,该组件是一款轻量级、高性能的graphql查询计算引擎。使用该组件对原有的GraphQLSchema对象进行包装转换即可,具体步骤如下
继承AsyncDataFetcherInterface
如果项目中使用了异步DataFetcher,则使其则继承AsyncDataFetcherInterface,并在方法实现中返回被包装的DataFetcher和使用的线程池。
public interface AsyncDataFetcherInterface<T> {
/**
* Return the dataFetcher which async dataFetcher wrapped.
*
* @return the dataFetcher which async dataFetcher wrapped
*/
DataFetcher<T> getWrappedDataFetcher();
/**
* Return the {@code Executor} which async dataFetcher used.
*
* @return the {@code Executor} which async dataFetcher used
*/
Executor getExecutor();
}
创建GraphQLSource
使用配置类Config创建GraphQLSource对象,GraphQLSource包含GraphQLSchema和GraphQL,配置类可指定脚本执行引擎、计算指令引擎使用的线程池和对象转换工具。
Config wrapperConfig = DefaultConfig.newConfig().build();
DefaultGraphQLSourceBuilder graphqlSourceBuilder = new DefaultGraphQLSourceBuilder();
GraphQLSource graphqlSource = graphqlSourceBuilder
.wrapperConfig(wrapperConfig)
.originalSchema(GraphQLSourceHolder.getDefaultSchema())
.preparsedDocumentProvider(new DocumentParseAndValidationCache()).build();
脚本语法使用了aviatorscript,aviator是graphql-java-calculator的默认表达行引擎,
可通过ScriptEvaluator和Config自定义脚本执行引擎。
执行前校验
使用Validator对计算指令的使用进行语法校验、该校验包含graphql原生语法校验,
建议实现CalculatorDocumentCachedProvider缓存校验结果。
完整示例参考Example
- 项目地址: github.com/graphql-cal…
- 项目介绍: 开源:基于指令和表达式实现查询的动态计算