SQL parser
SQL解析是根据语法与词法分析SQL,理解SQL含义,才能按照SQL语义处理数据,SQL解析是实现分库分表组件最基础的功能,熟悉Mysql架构的,内部也有很重要的一个模块就是SQL parser。
Sharding-JDBC目前SQL解析采用的是ANTLR解析器,先前1.x版本是采用的是Druid SQL解析,个人觉得druid SQL解析还是比较容易上手,分库分表中间件Mycat、Dble内部解析都是基于Druid实现的。Druid官方称解析性能比ANTLR这类解析器高10倍。至于sharding为什么还要使用ANTLR解析器,我觉得最主要的因素和灵活性有关吧。
有几个概念需要温习下
- AST:在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。
- 词法分析(Lexer):将语句拆分为一个个最小的词法单元(Tokens)比如常见的“>” 、 “=”、“in” 等。词法分析只负责语句的拆分,剩下的就交给语法分析了。词法中主要包含以下几部分:关键字、常量、标识符、运算符和分界符。
- 语法分析(parser):根据词法解析的结果,使用某种算法对词法解析的词法单元进行分析,生成抽象语法树(AST),外部可以遍历语法树,获取需要的内容。
举个栗子,以简单的select 语句为例
SQL语句主要由关键字、常量、标识符、运算符和分界符这五部分组成,根据词法分析的结果进行语法分析,首先有关键字select
说明这是一个查询语句, items
中有 “id”和“age” 说明我们查询返回的列是这两个,依次类推,最后彻底理解SQL的含义。
ANTLR
什么是ANTLR?
ANTLR(另一种语言识别工具)是功能强大的解析器生成器,用于读取,处理,执行或翻译结构化文本或二进制文件。它被广泛用于构建语言,工具和框架。ANTLR通过语法生成可以构建和遍历语法树的语法分析器。(创始者特伦斯·帕尔(**Terence Parr)**是ANTLR背后的疯子,自1989年以来一直致力于语言工具。计算机科学教授)。
与Druid sql parser最大的区别就是ANTLR不是针对SQL解析而生
,而Druid sql parser是为SQL解析而生
,提供一站式SQL解析服务,无需使用者这定义规则,也许你会问那各种数据库的方言怎么解析?这点Druid的作者想到了,为各种数据库适配了SQL解析,而使用ANTLR,则需要使用者自己定义识别规则。各有所长吧。
- 以官方hello为例,识别“hello”字符串,首先粘贴官网.g4文件
// Define a grammar called Hello
grammar Hello;
root : 'hello' ID ; // match keyword hello followed by an identifier
ID : [a-z|0-9]+ ; // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
解析字符串 hello 1001
- 在看一个稍微复杂一点的对运算表达式的解析,.g4文件如下
grammar Expr;
@header {
package tools;
import java.util.*;
}
@parser::members {
/** "memory" for our calculator; variable/value pairs go here */
Map<String, Integer> memory = new HashMap<String, Integer>();
int eval(int left, int op, int right) {
switch ( op ) {
case MUL : return left * right;
case DIV : return left / right;
case ADD : return left + right;
case SUB : return left - right;
}
return 0;
}
}
stat: e NEWLINE {System.out.println($e.v);}
| ID '=' e NEWLINE {memory.put($ID.text, $e.v);}
| NEWLINE
;
e returns [int v]
: a=e op=('*'|'/') b=e {$v = eval($a.v, $op.type, $b.v);}
| a=e op=('+'|'-') b=e {$v = eval($a.v, $op.type, $b.v);}
| INT {$v = $INT.int;}
| ID
{
String id = $ID.text;
$v = memory.containsKey(id) ? memory.get(id) : 0;
}
| '(' e ')' {$v = $e.v;}
;
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
ID : [a-zA-Z]+ ; // match identifiers
INT : [0-9]+ ; // match integers
NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal)
WS : [ \t]+ -> skip ; // toss out whitespace
在IDEA antlr4插件中解析如下表达式
1*(10+22)-33+80
我们运行下sharding-JDBC中对select 解析,让大家有个整体印象
select id ,name from t_order where id=1001 or name ='qiqsa'
解析结果
关于ANTLR更多知识,可以参考官网资料,而且提供了很多examples
Sharding-JDBC ANTLR
Sharding-JDBC使用了ANTLR定义了各种SQL解析规则,就是项目中 .g4文件
有兴趣的可以研究,本人觉得还是比较复杂,可能是由于对ANTLR语法还不是很熟悉吧,这里对这些规则就不做分析了。
Source code
直奔主题吧(在路由前调用的解析)sharding-JDBC内部提供了SQL解析引擎SQLParseEngine,看下入口
public SQLStatement parse(final String sql, final boolean useCache) {
//内部实现钩子方法,植入其他逻辑,主要作用是链路监控
ParsingHook parsingHook = new SPIParsingHook();
parsingHook.start(sql);
try {
//解析入口
SQLStatement result = parse0(sql, useCache);
parsingHook.finishSuccess(result);
return result;
// CHECKSTYLE:OFF
} catch (final Exception ex) {
// CHECKSTYLE:ON
parsingHook.finishFailure(ex);
throw ex;
}
}
钩子方法在上篇分享sharding-JDBC源码分析(一)标准JDBC接口实现 已经提到过了,接续分析parse0方法
private SQLStatement parse0(final String sql, final boolean useCache) {
//根据是否使用缓存获取解析结果
if (useCache) {
Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql);
if (cachedSQLStatement.isPresent()) {
return cachedSQLStatement.get();
}
}
//这块是对SQL进行解析,并将解析结果遍历封装在SQLStatement对象中
SQLStatement result = new SQLParseKernel(ParseRuleRegistry.getInstance(), databaseType, sql).parse();
if (useCache) {
cache.put(sql, result);
}
return result;
}
这块根据开关对解析结果使用了缓存,个人觉得对于缓存应该分为预编译和非预编译,对于非预编译其实没必要缓存,不过从另一方面来看,现在大部分ORM框架,比如mybatis使用基本都是预编译,非预编译很少
SQLParseKernel中的parse方法
public SQLStatement parse() {
//解析获取抽象语法树AST
SQLAST ast = parserEngine.parse();
//从语法树中获取sql 片段,稍后解释sql片段是什么意思
Collection<SQLSegment> sqlSegments = extractorEngine.extract(ast);
Map<ParserRuleContext, Integer> parameterMarkerIndexes = ast.getParameterMarkerIndexes();
//将解析获取的语法树遍历填充在SQLStatement对象中返回,供后续路由使用
return fillerEngine.fill(sqlSegments, parameterMarkerIndexes.size(), ast.getSqlStatementRule());
}
上边提到的sql片段即SQLSegment,其实是定义的一种数据结构,是对词法解析中每一个Token的封装,SQLSegment是接口,分析其中一个实现就明白怎么回事了
public class ColumnSegment implements SQLSegment, PredicateRightValue, OwnerAvailable<TableSegment> {
//在sql中开始index
private final int startIndex;
//结束index
private final int stopIndex;
//字段名column
private final String name;
//是否有 `,’等, 比如mysql中查询中 select `id`,`name` from xxx
private final QuoteCharacter quoteCharacter;
//字段属于哪个表
private TableSegment owner;
现在明白SQLSegment怎么回事了吧,可以看下解析结果,更清晰
select `id` from t_order where `id` = 1001
有了抽象语法树,需要将抽象语法树封装成我们需要的数据结构
public SQLStatement fill(final Collection<SQLSegment> sqlSegments, final int parameterMarkerCount, final SQLStatementRule rule) {
//根据语法规则中配置的class name获取实现类,比如有SQLSelectStatement 、SQLinsertStatement等
SQLStatement result = rule.getSqlStatementClass().newInstance();
Preconditions.checkArgument(result instanceof AbstractSQLStatement, "%s must extends AbstractSQLStatement", result.getClass().getName());
((AbstractSQLStatement) result).setParametersCount(parameterMarkerCount);
result.getAllSQLSegments().addAll(sqlSegments);
//对应的SQLSegment实现了filter,fiter作用是将有关键的SQLSegment放在对应的数据结构中,比如有orderby 或者
for (SQLSegment each : sqlSegments) {
Optional<SQLSegmentFiller> filler = parseRuleRegistry.findSQLSegmentFiller(databaseType, each.getClass());
if (filler.isPresent()) {
filler.get().fill(each, result);
}
}
return result;
}
<filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.order.GroupBySegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.GroupByFiller" />
<filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.order.OrderBySegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.OrderByFiller" />
<filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.column.InsertColumnsSegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.InsertColumnsFiller" />
<filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.assignment.InsertValuesSegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.InsertValuesFiller" />
<filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.assignment.SetAssignmentsSegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.SetAssignmentsFiller" />
看下其中一个filter实现
public final class OrderByFiller implements SQLSegmentFiller<OrderBySegment> {
@Override
public void fill(final OrderBySegment sqlSegment, final SQLStatement sqlStatement) {
((SelectStatement) sqlStatement).setOrderBy(sqlSegment);
}
}
将orderBySegment保存在了SQLStatement中,就是这个作用
解析到这块基本结束,文中并没有说怎么从抽象语法树AST中提取解析结果,ANTLRT提供了两种实现方式,有兴趣的可以去查阅,我觉得和Druid的相似吧,druid中提供一种手写遍历语法树,用户自己实现,另外一种是基于visitor访问者模式实现的遍历。
对于上边提到的SQL最终解析结果如下,其中包含tables
,where
等,看到这块应该理解了SQL解析的意义。
解析结束后,到了路由阶段
public SQLRouteResult route(final String logicSQL) {
//解析获得结果
SQLStatement sqlStatement = shardingRouter.parse(logicSQL, false);
//根据解析结果sqlStatement进行路由,路由会用到解析中的条件或者insert语句中的分片键
return masterSlaveRouter.route(shardingRouter.route(logicSQL, Collections.emptyList(), sqlStatement));
}
结束语
本文主要简单的介绍了什么是ANTLR4,如果你准备用ANTLR,看这些远远不够的,网上这方面资料很全,官网也有很多demo,对sharding-JDBC中解析流程做了分析,读者可以按照这个思路来看源码;如果你对ANTLR不怎么熟悉,那么可以选择Druid sql parser作为SQL解析,个人认为还是算比较好上手,毕竟不用自己定义语法规则,本人对druid sql parser还算熟悉,有问题可以共同探讨,下篇对sharding-JDBC路由模块做分析。