ShardingSphere源码解析(二)---SQL改写

915 阅读2分钟

这节分析一下sql改写
1.接着Route之后,我们看下怎么去进行sql改写,进入BasePrepareEngine#prepare方法,我们可以看到executeRewrite方法,从名字上看应该是做sql改写,registerRewriteDecorator这个方法其实和我们看路由的时候很像,这里是在SQLRewriteEntry中注册了ShardingSQLRewriteContextDecorator,进入rewriter.createSQLRewriteContext方法

private Collection<ExecutionUnit> executeRewrite(final String sql, final List<Object> parameters, final RouteContext routeContext) {
        registerRewriteDecorator();
        SQLRewriteContext sqlRewriteContext = rewriter.createSQLRewriteContext(sql, parameters, routeContext.getSqlStatementContext(), routeContext);
        return routeContext.getRouteResult().getRouteUnits().isEmpty() ? rewrite(sqlRewriteContext) : rewrite(routeContext, sqlRewriteContext);
    }
public void decorate(final ShardingRule shardingRule, final ConfigurationProperties properties, final SQLRewriteContext sqlRewriteContext) {
        for (ParameterRewriter each : new ShardingParameterRewriterBuilder(shardingRule, routeContext).getParameterRewriters(sqlRewriteContext.getSchemaMetaData())) {
            if (!sqlRewriteContext.getParameters().isEmpty() && each.isNeedRewrite(sqlRewriteContext.getSqlStatementContext())) {
                each.rewrite(sqlRewriteContext.getParameterBuilder(), sqlRewriteContext.getSqlStatementContext(), sqlRewriteContext.getParameters());
            }
        }
        sqlRewriteContext.addSQLTokenGenerators(new ShardingTokenGenerateBuilder(shardingRule, routeContext).getSQLTokenGenerators());
    }

3.进入BasePrepareEngine#rewrite,这里对sql做了重写,new SQLRouteRewriteEngine().rewrite方法其实就是组做sql重写,最终得到Map<RouteUnit, SQLRewriteResult>对象,

Collection<ExecutionUnit> result = new LinkedHashSet<>();
        for (Entry<RouteUnit, SQLRewriteResult> entry : new SQLRouteRewriteEngine().rewrite(sqlRewriteContext, routeContext.getRouteResult()).entrySet()) {
            result.add(new ExecutionUnit(entry.getKey().getDataSourceMapper().getActualName(), new SQLUnit(entry.getValue().getSql(), entry.getValue().getParameters())));
        }
        return result;
    }
  1. 根据RouteUnit分组去做sql重写,具体的sql重写在toSQL()中
public Map<RouteUnit, SQLRewriteResult> rewrite(final SQLRewriteContext sqlRewriteContext, final RouteResult routeResult) {
        Map<RouteUnit, SQLRewriteResult> result = new LinkedHashMap<>(routeResult.getRouteUnits().size(), 1);
        for (RouteUnit each : routeResult.getRouteUnits()) {
            result.put(each, new SQLRewriteResult(new RouteSQLBuilder(sqlRewriteContext, each).toSQL(), getParameters(sqlRewriteContext.getParameterBuilder(), routeResult, each)));
        }
        return result;
    }
  1. SQLToken是一个抽象类,抽象类中存的是startIndex,起始位置,重写了compareTo方法,位置越靠前的排前面,子类其实是各种sql中的解析对象,比如tableToken,OrderByToken等等,for (SQLToken each : context.getSqlTokens())就是去进行sql重写的关键点,这里去深入看下所有子类的toString方法,就在这里进行了改写
public final String toSQL() {
        if (context.getSqlTokens().isEmpty()) {
            return context.getSql();
        }
        Collections.sort(context.getSqlTokens());
        StringBuilder result = new StringBuilder();
        result.append(context.getSql().substring(0, context.getSqlTokens().get(0).getStartIndex()));
        for (SQLToken each : context.getSqlTokens()) {
            result.append(getSQLTokenText(each));
            result.append(getConjunctionText(each));
        }
        return result.toString();
    }
  1. 改写之后的sql就是我们最终要执行的sql,也就是ExecutionUnit这个对象,看下ExecutionUnit对象,里面存了真实的数据源名称和SQLUnit,SQLUnit里面存了真实的sql和parameters参数
public final class ExecutionUnit {
    private final String dataSourceName;
    private final SQLUnit sqlUnit;
}
public final class SQLUnit {
    private final String sql;
    private final List<Object> parameters;
}

7.回到BasePrepareEngine#prepare中,ExecutionContext中放入我们实例化的下ExecutionUnit集合,根据是否打印sql参数判断是否打印sql,我们看到的sql日志就是在这里打印,这里已经完成了sql改写,打印的是真实sql。

放一张官网的图片,改写引擎做了哪些事情

这里说下分页修正,如果查询的数据不是来源一张表,需要做分页修正,这个时候shardingsphere为了达到分页的正确性,会改写sql成limit0,xx 这种形式 ,越获取偏移量位置靠后数据,使用LIMIT分页方式的效率就越低。
有很多方法可以避免使用LIMIT进行分页。比如构建行记录数量与行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式等