根据sql解析获取到表到表, 字段到字段间的关系,即血缘关系。实际上这是从sql文本获取到数据流的过程。
大致步骤如下:
1.sql文本进行词法分析
2.sql语法分析获取到AST抽象语法树
3.访问AST抽象语法树根据语法结构推测出数据的流向,例如create as select from 这种结构,数据就是从from的表流向select出来的ResultSet中间结果集最后流程create的表中, 字段可以使用字段名进行匹配,如果可以连接到元数据库是可以查询到表和字段的,如果仅仅根据sql文本分析,那么一般只能根据select中的字段名结合语法进行猜测。
一般来说,步骤1,2比较底层会有很多工具为我们完成, 像antlr(开源语法分析器),druid(基于antlr),使用这些工具可以直接从sql获取到AST语法树, 然后我们直接进入第3个步骤遍历语法树获取自己想要的信息即可。比如在解析create table A as select * from table B; 后判断出语句为create from句式,从语法树中获取create的表A和from的表B,这样表级别的血缘关系就获取到了。
下面以antlr为例对这三个步骤进行详细的说明:
-
sql文本进行Lexer词法分析
词法分析是将sql文本字符序列转换为单词(Token)序列的过程。
进行词法分析的程序或者函数叫作词法分析器(Lexical analyzer,简称Lexer),也叫扫描器(Scanner)。
比如,sql中的关键字select, from, table等,antlr根据一个规则g4文件去识别这些关键字。
g4文件是antlr生成词法解析规则和语法解析规则的基础。该文件是我们自定义的,文件名后缀需要是.g4。g4文件描述了Token及Token间语法。antlr根据g4文件的描述先进行词法解析,将字符串解析为单词(Token)序列。
借个图做个示例:
-
sql语法分析parse获取到AST抽象语法树
语法分析器根据g4文件描述的规则将收到的Tokens组织起来,并转换成为目标语言语法定义所允许的序列,并组装为AST抽象语法树结构。
例如下面的sql可以被解析为下图中的ast语法树。
create table stu_tj row format delimited as SELECT b.id,b.name FROM (select oldId id ,name from stu WHERE id =‘2’) b ;
-
访问AST抽象语法树根据语法结构推测出数据的流向。
语法树中包含了sql的全部信息, 可以从中获取到所需信息。但是,访问代码并不好写,因为语法树中封装的对象类型很多,不同的类型需要不同的访问方式,一般使用visitor访问者模式进行访问。大部分的业务代码就在这里访问sql解析后的AST语法树(Abstract Syntax Tree),然后获取源表和目标表之间的对应关系,即所谓血缘关系。
例如create as select from 这种结构,数据就是从from的表流向select出来的ResultSet中间结果集最后流程create的表中, 字段可以使用字段名结合语法进行匹配推理。
druid提供了visitor接口访问AST语法树,需要借用visitor设计模式,调用accept方法即可访问AST语法树,注意accept方法是调用入参对象的接口方法。druid已实现的visitor功能不够全,无法处理别名等一些需求,需要自己实现visitor。可以直接继承
SQLASTVisitorAdapter 也可以根据不同数据库类型继承不同的visitor。
// 自定义访问者 继承SQLASTVisitorAdapter
class SQLCustomedVisitor extends SQLASTVisitorAdapter {
protected boolean hasLimit = false;
@Override
public boolean visit(SQLLimit x) {
System.out.println(x.getRowCount());
hasLimit = true;
return false;
}
public boolean isHasLimit() { return hasLimit; }
}
不同数据库类型继承不同的visitor
https://github.com/alibaba/druid/wiki/SQL_Parser_Demo_visitor
public class MySqlSchemaStatVisitor extends SchemaStatVisitor implements MySqlASTVisitor {
public MySqlSchemaStatVisitor() {
super (JdbcConstants.MYSQL);
}
public boolean visit(SQLSelectStatement x) {
if (repository != null
&& x.getParent() == null) {
repository.resolve(x);
}
return true;
}
}
参考
https://www.jianshu.com/p/21f2afca65e8
https://baike.baidu.com/item/antlr/9368750
https://zhuanlan.zhihu.com/p/121545985
https://github.com/alibaba/druid/wiki/SQL_Parser_Demo_visitor
https://github.com/alibaba/druid/wiki/SQL-Parser
https://blog.csdn.net/shy_snow/article/details/133373567