author:彭程
介绍
访问器(Visitor)能够对语法分析器(Parser)生成的抽象语法树进行遍历,获取需要的信息。本文主要介绍复杂SQL的Visitor构造过程
SQL语句
以如下SQL语句为例介绍dble的Visitor构造过程
select * from tb_enum_sharding a left join tb_hash_sharding b on a.id = b.id;
Visitor访问过程
上一篇中说到dble会在NonBlockingSession#executeMultiSelect方法中构建Visitor,并调用Visitor的MySQLPlanNodeVisitor#visit方法访问druid生成的语法树SQLSelectStatement(ast),并会调用其重载方法visit(SQLSelectQuery node)
public boolean visit(SQLSelectStatement node) {TraceManager.TraceObject traceObject = TraceManager.threadTrace("visit-for-sql-structure");try {SQLSelectQuery sqlSelect = node.getSelect().getQuery();MySQLPlanNodeVisitor mtv = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtv.visit(sqlSelect); //调用其重载方法this.tableNode = mtv.getTableNode();this.containSchema = mtv.isContainSchema();MySQLItemVisitor.clearCache();return true;} finally {TraceManager.finishSpan(traceObject);}}
visit函数会先判断SQL中是否包含union,示例SQL没有union,所以进入visit(MySqlSelectQueryBlock node)方法
public void visit(SQLSelectQuery node) {if (node instanceof MySqlSelectQueryBlock) { //如果是查询语句visit((MySqlSelectQueryBlock) node);} else if (node instanceof SQLUnionQuery) { //如果是union查询语句visit((SQLUnionQuery) node);}}
visit(MySqlSelectQueryBlock node)方法访问到from后的SQL,此时需要判断是否有SQL嵌套,如select code from (select * from tb_enum_sharding where id > 1) as a;本例没有,则进入from的访问。
public boolean visit(MySqlSelectQueryBlock sqlSelectQuery) {SQLTableSource from = sqlSelectQuery.getFrom(); // 获取from后的SQLif (from != null) {//判断from后是否还有select,如select code from (select * from tb_enum_sharding where id > 1) as a;String innerFuncSelectSQL = createInnerFuncSelectSQL(sqlSelectQuery.getSelectList());if (innerFuncSelectSQL != null) {MySQLPlanNodeVisitor mtv = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtv.visit(from);NoNameNode innerNode = new NoNameNode(currentDb, innerFuncSelectSQL);innerNode.setFakeNode(true);List<Item> selectItems = handleSelectItems(selectInnerFuncList(sqlSelectQuery.getSelectList()));if (selectItems != null) {innerNode.select(selectItems);}this.tableNode = new JoinInnerNode(innerNode, mtv.getTableNode());this.containSchema = mtv.isContainSchema();} else {visit(from); //进入from的访问}if (this.tableNode instanceof NoNameNode) {this.tableNode.setSql(SQLUtils.toMySqlString(sqlSelectQuery));}} else {this.tableNode = new NoNameNode(currentDb, SQLUtils.toMySqlString(sqlSelectQuery));String innerFuncSelectSQL = createInnerFuncSelectSQL(sqlSelectQuery.getSelectList());if (innerFuncSelectSQL != null) {((NoNameNode) tableNode).setFakeNode(true);}}...return true;}
访问from会根据from后面的语句类型进行区分,不同的类型又会进行不同的访问,如本例中from是left join,因此会进入visit(SQLJoinTableSource)方法
public void visit(SQLTableSource tables) {if (tables instanceof SQLExprTableSource) { //from后面接简单的表名SQLExprTableSource table = (SQLExprTableSource) tables;MySQLPlanNodeVisitor mtv = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtv.visit(table);this.tableNode = mtv.getTableNode();this.containSchema = mtv.isContainSchema();} else if (tables instanceof SQLJoinTableSource) { //from后面的表使用了连接操作SQLJoinTableSource joinTables = (SQLJoinTableSource) tables; //将from转为SQLJoinTableSource对象MySQLPlanNodeVisitor mtv = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtv.visit(joinTables);this.tableNode = mtv.getTableNode();this.containSchema = mtv.isContainSchema();} else if (tables instanceof SQLUnionQueryTableSource) { ////from后面内部持有一个SQLUnionQuery对象if (tables.getAlias() == null) {throw new MySQLOutPutException(ErrorCode.ER_DERIVED_MUST_HAVE_ALIAS, "", "Every derived table must have its own alias");}SQLUnionQueryTableSource unionTables = (SQLUnionQueryTableSource) tables;MySQLPlanNodeVisitor mtv = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtv.visit(unionTables);this.tableNode = new QueryNode(mtv.getTableNode());this.tableNode.setContainsSubQuery(mtv.getTableNode().isContainsSubQuery());this.containSchema = mtv.isContainSchema();} else if (tables instanceof SQLSubqueryTableSource) { //子查询表if (tables.getAlias() == null) {throw new MySQLOutPutException(ErrorCode.ER_DERIVED_MUST_HAVE_ALIAS, "", "Every derived table must have its own alias");}SQLSubqueryTableSource subQueryTables = (SQLSubqueryTableSource) tables;MySQLPlanNodeVisitor mtv = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtv.visit(subQueryTables);this.tableNode = new QueryNode(mtv.getTableNode());this.tableNode.setContainsSubQuery(mtv.getTableNode().isContainsSubQuery());this.containSchema = mtv.isContainSchema();}if (tables.getAlias() != null) {this.tableNode.setAlias(tables.getAlias());}}
这里进行进一步进行访问时,dble会将from强制转化为一个SQLJoinTableSource对象,该对象有left、right分别代表左、右表,是一种二叉树结构,除此之外joinType属性表示连接的类型,condition属性表示连接条件
visit(SQLJoinTableSource)中会对left和right继续调用上文的visit(SQLTableSource)方法,直到left和right中没有子树,之后会根据JoinType的类型,本例是left join,因此设置node.LeftOuterJoin=true
public boolean visit(SQLJoinTableSource joinTables) {SQLTableSource left = joinTables.getLeft();MySQLPlanNodeVisitor mtvLeft = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtvLeft.visit(left);SQLTableSource right = joinTables.getRight();MySQLPlanNodeVisitor mtvRight = new MySQLPlanNodeVisitor(this.currentDb, this.charsetIndex, this.metaManager, this.isSubQuery, this.usrVariables);mtvRight.visit(right);JoinNode joinNode = new JoinNode(mtvLeft.getTableNode(), mtvRight.getTableNode(), this.charsetIndex);joinNode.setContainsSubQuery(mtvLeft.getTableNode().isContainsSubQuery() || mtvRight.getTableNode().isContainsSubQuery());switch (joinTables.getJoinType()) {case JOIN:case CROSS_JOIN:case INNER_JOIN:case STRAIGHT_JOIN:joinNode.setInnerJoin();break;case LEFT_OUTER_JOIN:if ((joinTables.getCondition() == null) && (joinTables.getUsing() == null || joinTables.getUsing().size() == 0) && !joinTables.isNatural()) {throw new MySQLOutPutException(ErrorCode.ER_PARSE_ERROR, "42000", "left join without join_condition!");}joinNode.setLeftOuterJoin();break;case RIGHT_OUTER_JOIN:if ((joinTables.getCondition() == null) && (joinTables.getUsing() == null || joinTables.getUsing().size() == 0) && !joinTables.isNatural()) {throw new MySQLOutPutException(ErrorCode.ER_PARSE_ERROR, "42000", "right join without join_condition!");}joinNode.setRightOuterJoin();break;case NATURAL_JOIN:// never happenbreak;default:break;}...this.tableNode = joinNode;this.containSchema = mtvLeft.isContainSchema() || mtvRight.isContainSchema();return true;}
访问器访问到底后返回到visit(MySqlSelectQueryBlock node)方法,之后会继续判断是否有orderby、groupby等,并在各自的处理函数中对node进行更新。
public boolean visit(MySqlSelectQueryBlock sqlSelectQuery) {SQLTableSource from = sqlSelectQuery.getFrom(); // 获取from后的SQLif (from != null) {...} else {visit(from);}if (this.tableNode instanceof NoNameNode) {this.tableNode.setSql(SQLUtils.toMySqlString(sqlSelectQuery));}} else {this.tableNode = new NoNameNode(currentDb, SQLUtils.toMySqlString(sqlSelectQuery));String innerFuncSelectSQL = createInnerFuncSelectSQL(sqlSelectQuery.getSelectList());if (innerFuncSelectSQL != null) {((NoNameNode) tableNode).setFakeNode(true);}}if (tableNode != null && (sqlSelectQuery.getDistionOption() == SQLSetQuantifier.DISTINCT || sqlSelectQuery.getDistionOption() == SQLSetQuantifier.DISTINCTROW)) {this.tableNode.setDistinct(true);}List<SQLSelectItem> items = sqlSelectQuery.getSelectList();if (items != null) {List<Item> selectItems = handleSelectItems(items);if (selectItems != null) {this.tableNode.select(selectItems);}}SQLExpr whereExpr = sqlSelectQuery.getWhere();if (whereExpr != null) {handleWhereCondition(whereExpr);}SQLOrderBy orderBy = sqlSelectQuery.getOrderBy();if (orderBy != null) {handleOrderBy(orderBy);}SQLSelectGroupByClause groupBy = sqlSelectQuery.getGroupBy();if (groupBy != null) {handleGroupBy(groupBy);}SQLLimit limit = sqlSelectQuery.getLimit();if (limit != null) {handleLimit(limit);}return true;}
访问器执行结束后就能看到visitor.PlanNode的值如下
JoinjoinStrategy: SORTMERGEtype: left outter joinjoinFilter: a.id = b.idotherJoinOnFilter: nullleftJoinOnOrder:rightJoinOnOrder:isDistinct: falsecolumns: *where: nullhaving: nullgroupBy:orderBy:sql: nullleft:Query from tb_enum_sharding as aisDistinct: falsecolumns:where: nullhaving: nullgroupBy:orderBy:sql: nullright:Query from tb_hash_sharding as bisDistinct: falsecolumns:where: nullhaving: nullgroupBy:orderBy:sql: null
