EasyQuery开发笔记 - 抽象语法树
easy query 项目地址: github
按照最开始的目的,easy query期望使用统一的条件查询表达式,来描述查询的条件。以简化企业级项目开发中的查询参数传递问题
比如
// 简单的条件
name = "quitee"
// 复杂的条件
(name = "quitee" and age <= 18) or age > 28
由于这种表达式并不是一个类似json一样的结构化信息,所以不得不需要通过另外一种途径去处理了,也就是通过抽象语法树来实现了
为什么使用抽象语法树
在谈实现之前,还是有必要说明一下
设计上,这个看似是SQL的条件表达式在定义上是个sql没有太大关系,只是在描述一个查询条件
所以在我眼中他不是SQL
预期上,通过对这个表达式的处理。它可以变成mysql的查询,SQL server的查询,甚至是es的查询条件
所以这不是replace或者正则表达式就能够完成的
且不说这个场景是否需要,对自己也当个是学习了
实现流程
了解了抽象语法树的必要性后,就开始着手对我们的表达式语句进行定义与解析
大体流程如下图
1. 定义表达式长什么样
先别急着开始码代码
在开始实现语法树以及构造语法树之前,需要定义好我们的表达式是啥,长什么样子
- 词法:定义的是我们的表达式中,有哪些关键词,关键词都代表了什么
- 语法:定义了我们表达式是否通顺,表达含义正确
在easy query的词法设计如下
比较操作:
=,!=,>,<,>=,<=,in,not in,contains,startWith,endWith,between,match
逻辑组合操作:
and,or
其他:
由 双引号 "..." 包住的内容为 文本 string类型
非双引号的为特殊类型
number,date,bool,text,NULL 等
语法设计如下,相对不复杂
// 比较语法
<比较字段> <比较操作> <比较操作值>
// 组合语法
<比较语法|组合语法> <and|or> <比较语法|组合语法>
2. 分词
有了上述的定义以后,那么第一步就是对表达式进行一个字符一个字符的扫描
这里借鉴了fastjson的思路
定义对象 QueryLexer ,由它来主要负责分词,并且进行词法分析
QueryLexer::nextToken() // 返回下一个分词结果
3. 构造语法树
拿到分词结果以后就开始处理语法以及生成语法树了
语法树采用了二叉树的形式表现
实现上,设计到了三个关键对象
- token栈:将未被接收的token压入栈中,以备后续使用
- 节点栈:将生成的新的节点压入,以备后续使用。最终该栈只有一个节点,即根节点
- 期望处理器:处理token并且生成节点。目前有处理比较条件和处理and/or条件的两个
3.1 token栈
token栈的引入,能够解决当前token无人认领的时候的尴尬局面
比如 name = "quitee"
首先获取到的token是text类型的name,对于期望处理器来说,还不足以确定是自己的处理范畴,因此会选择忽略,于是这个token就会被入栈/等到 =
这个token来了以后,比较处理器 AstNodeBuilderFieldCondition
发现这是自己的活,就能从栈中获取到name,并且把它当成比较的字段来处理了
3.2 节点栈
节点栈同token栈一样,当以处理节点为主的期望处理器需要获取到之前处理过的节点时,则从中获取上一个节点
3.3 期望处理器
期望处理器是一个一个处理token,生成节点的处理器
同时还要告诉构造器,下一个token期望由哪些处理器来处理
比如 处理name = "quitee"
的比较处理器 AstNodeBuilderFieldCondition
的下一个期望就不会再是比较处理器 AstNodeBuilderFieldCondition
即
name = "quitee" age < 10 // 不是我们定下的语法
// 我们期望的语法如下
name = "quitee" and age <10
(name = "quitee")
name = "quitee"
(... and name = "quitee")
最终,就能将表达式,转化成结构化的树结构。为我们后续转成sql,dsl query等等都打下了基础
拓展
由于词法是我们自己解析的,因此在代码编写合适的情况下,我们可以拓展出很多新的花样
1. 自定义token关键词
我们可以将token类型添加别名,比如下面的配置
结果同样可以解析成功
2. 遍历
由于生成了树结构,那么我们就可以通过常用的遍历手段来进行遍历