数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写

1. 概述

本文分享SQL 改写的源码实现。主要涉及两方面:

  1. SQL 改写:改写 SQL,解决分库分表后,查询结果需要聚合,需要对 SQL 进行调整,例如分页。
  2. SQL 生成:生成分表分库的执行 SQL。

SQLRewriteEngine,SQL 重写引擎,实现 SQL 改写、生成功能。

2. SQLToken

SQLToken 在本文中很重要,所以即使在《SQL 解析》已经分享过,我们也换个姿势,再来一次。

SQLToken,SQL 标记对象接口。SQLRewriteEngine 基于 SQLToken 实现 SQL 改写。SQL 解析器在 SQL 解析过程中,很重要的一个目的是标记需要 SQL 改写的部分,也就是 SQLToken。

数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写_第1张图片

各 SQLToken 生成条件如下:

  1. GeneratedKeyToken 自增主键标记对象

    • 插入SQL自增列不存在并且客户端设置了自增主键属性 generate-key-column: INSERT INTO t_order(nickname) VALUES... 中没有自增列 order_id
  2. TableToken 表标记对象

    • 查询的表名: SELECT * FROM t_order 的 t_order
  3. ItemsToken 选择项标记对象

    • AVG 查询列: SELECT AVG(price) FROM t_order 的 AVG(price)

    • ORDER BY 字段不在查询列: SELECT order_id FROM t_order ORDER BY create_time 的 create_time

    • GROUP BY 字段不在查询列: SELECT COUNT(order_id) FROM t_order GROUP BY user_id 的 user_id

    • 自增主键未在插入列中并且客户端设置了自增主键属性 generate-key-column: INSERT INTO t_order(nickname) VALUES... 中没有自增列 order_id

  4. OffsetToken 分页偏移量标记对象

    • 分页有偏移量,但不是占位符 ?
  5. RowCountToken 分页长度标记对象

    • 分页有长度,但不是占位符 ?
  6. OrderByToken 排序标记对象

    • 有 GROUP BY 条件,无 ORDER BY 条件: SELECT COUNT(*) FROM t_order GROUP BY order_id 的 order_id

3. SQL 改写

SQLRewriteEngine#rewrite() 实现了 SQL改写 功能。

public SQLBuilder rewrite(final boolean isRewriteLimit) {
        SQLBuilder result = new SQLBuilder();
        if (sqlTokens.isEmpty()) {
            result.appendLiterals(originalSQL);
            return result;
        }
        int count = 0;
        // 对 SQLToken 根据起始位置进行排序
        sortByBeginPosition();
        for (SQLToken each : sqlTokens) {
            if (0 == count) {
                // 拼接第一个 SQLToken 前的字符串
                result.appendLiterals(originalSQL.substring(0, each.getBeginPosition()));
            }
            //  拼接每个 SQLToken
            if (each instanceof TableToken) {
                appendTableToken(result, (TableToken) each, count, sqlTokens);
            } else if (each instanceof IndexToken) {
                appendIndexToken(result, (IndexToken) each, count, sqlTokens);
            } else if (each instanceof ItemsToken) {
                appendItemsToken(result, (ItemsToken) each, count, sqlTokens);
            } else if (each instanceof RowCountToken) {
                appendLimitRowCount(result, (RowCountToken) each, count, sqlTokens, isRewriteLimit);
            } else if (each instanceof OffsetToken) {
                appendLimitOffsetToken(result, (OffsetToken) each, count, sqlTokens, isRewriteLimit);
            } else if (each instanceof OrderByToken) {
                appendOrderByToken(result, count, sqlTokens);
            }
            count++;
        }
        return result;
    }

SQL 改写以 SQLToken 为间隔,顺序改写。

顺序:调用sortByBeginPosition()将 SQLToken 按照 beginPosition 升序。
间隔:遍历 SQLToken,逐个拼接。

SQLBuilder,SQL 构建器。下文会大量用到,我们看下实现代码。

public final class SQLBuilder {
    
    // 段集合
    private final List segments;
    // 当前段
    private StringBuilder currentSegment;
    
    /**
     * Constructs a empty SQL builder.
     */
    public SQLBuilder() {
        segments = new LinkedList<>();
        currentSegment = new StringBuilder();
        segments.add(currentSegment);
    }
    
    /**
     * 追加字面量.
     *
     * @param literals literals for SQL
     */
    public void appendLiterals(final String literals) {
        currentSegment.append(literals);
    }
    
    /**
     * 追加表占位符.
     *
     * @param tableName table name
     */
    public void appendTable(final String tableName) {
        // 添加 TableToken
        segments.add(new TableToken(tableName));
        // 新建当前段
        currentSegment = new StringBuilder();
        segments.add(currentSegment);
    }
    
    /**
     * Convert to SQL string.
     *
     * @param tableTokens table tokens
     * @return SQL string
     */
    public String toSQL(final Map tableTokens) {
        ...省略代码,【SQL生成】处分享
    }
    
    @RequiredArgsConstructor
    private class TableToken {
        // 表名
        private final String tableName;
        
        @Override
        public String toString() {
            return tableName;
        }
    }

}

现在我们来逐个分析每种 SQLToken 的拼接实现。

3.1 TableToken

调用appendTableToken()方法拼接。

// SQLRewriteEngine.java
private void appendTableToken(final SQLBuilder sqlBuilder, final TableToken tableToken, final int count, final List sqlTokens) {
        // 添加 TableToken
        sqlBuilder.appendTable(tableToken.getTableName().toLowerCase());
        int beginPosition = tableToken.getBeginPosition() + tableToken.getOriginalLiterals().length();
        // 拼接当前 SQLToken 到下一个 SQLToken 之间的 SQL串
        appendRest(sqlBuilder, count, sqlTokens, beginPosition);
    }
    
// SQLBuilder.java
private void appendRest(final SQLBuilder sqlBuilder, final int count, final List sqlTokens, final int beginPosition) {
        // 获取下一个 SQLToken 的起始位置
        int endPosition = sqlTokens.size() - 1 == count ? originalSQL.length() : sqlTokens.get(count + 1).getBeginPosition();
        sqlBuilder.appendLiterals(originalSQL.substring(beginPosition, endPosition));
    }

例如 SQL 为 SELECT * FROM ts_redeem_plan where id = 1;其返回结果为:

数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写_第2张图片

3.2 ItemsToken

调用appendItemsToken()方法拼接。

// SQLRewriteEngine.java
private void appendItemsToken(final SQLBuilder sqlBuilder, final ItemsToken itemsToken, final int count, final List sqlTokens) {
        // 拼接 ItemsToken
        for (String item : itemsToken.getItems()) {
            sqlBuilder.appendLiterals(", ");
            sqlBuilder.appendLiterals(SQLUtil.getOriginalValue(item, databaseType));
        }
        int beginPosition = itemsToken.getBeginPosition();
        // 拼接当前 SQLToken 到下一个 SQLToken 之间的 SQL串
        appendRest(sqlBuilder, count, sqlTokens, beginPosition);
    }

  • 第一种情况,AVG 查询列,SQL 为 SELECT AVG(repay_amount) FROM ts_redeem_plan 时返回结果:


    数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写_第3张图片
  • 第二种情况,ORDER BY 字段不在查询列,SQL 为 SELECT repay_amount FROM ts_redeem_plan order by id 时返回结果:


    数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写_第4张图片
  • 第三种情况,GROUP BY 字段不在查询列,类似第二种情况,就不举例子列。

3.3 OffsetToken

调用appendLimitOffsetToken()方法拼接。

// SQLRewriteEngine.java
 private void appendLimitOffsetToken(final SQLBuilder sqlBuilder, final OffsetToken offsetToken, final int count, final List sqlTokens, final boolean isRewrite) {
        // 拼接 OffsetToken
        sqlBuilder.appendLiterals(isRewrite ? "0" : String.valueOf(offsetToken.getOffset()));
        int beginPosition = offsetToken.getBeginPosition() + String.valueOf(offsetToken.getOffset()).length();
        // 拼接当前 SQLToken 到下一个 SQLToken 之间的 SQL串
        appendRest(sqlBuilder, count, sqlTokens, beginPosition);
    }
  • 当分页跨分片时,需要每个分片都查询后在内存中进行聚合。此时 isRewrite=true。为什么是 "0" 开始呢?每个分片在 [0, rowCount) 的记录可能属于实际分页结果,因而查询每个分片需要从 0 开始。

  • 当分页单分片时,则无需重写,该分片执行的结果即是最终结果。

3.4 RowCountToken

调用appendLimitRowCount()方法拼接。

private void appendLimitRowCount(final SQLBuilder sqlBuilder, final RowCountToken rowCountToken, final int count, final List sqlTokens, final boolean isRewrite) {
        SelectStatement selectStatement = (SelectStatement) sqlStatement;
        Limit limit = selectStatement.getLimit();
        if (!isRewrite) {
            // 为单分片
            sqlBuilder.appendLiterals(String.valueOf(rowCountToken.getRowCount()));
        } else if ((!selectStatement.getGroupByItems().isEmpty() || !selectStatement.getAggregationSelectItems().isEmpty()) && !selectStatement.isSameGroupByAndOrderByItems()) {

            // 需要加载全部数据的情况
            sqlBuilder.appendLiterals(String.valueOf(Integer.MAX_VALUE));
        } else {
            // 多分片
            sqlBuilder.appendLiterals(String.valueOf(limit.isNeedRewriteRowCount() ? rowCountToken.getRowCount() + limit.getOffsetValue() : rowCountToken.getRowCount()));
        }
        int beginPosition = rowCountToken.getBeginPosition() + String.valueOf(rowCountToken.getRowCount()).length();
        // 拼接当前 SQLToken 到下一个 SQLToken 之间的 SQL串
        appendRest(sqlBuilder, count, sqlTokens, beginPosition);
    }

需要加载全部数据的情况:

  1. !selectStatement.getGroupByItems().isEmpty() 跨分片分组需要在内存计算,可能需要全部加载。如果不全部加载,部分结果被分页条件错误结果,会导致结果不正确。

  2. !selectStatement.getAggregationSelectItems().isEmpty()) 跨分片聚合列需要在内存计算,可能需要全部加载。如果不全部加载,部分结果被分页条件错误结果,会导致结果不正确。

  3. GROUP BY 和 ORDER BY 排序不一致(包括字段,排序类型)。如果一致,各分片已经排序完成,无需内存中排序。(该条件必须满足,上面两个条件满足一个即可

如果无需加载全部数据的多分片情况,rowCount 需要加上 Limit中的 offset 值。因为在处理OffsetToken的时候,会把 offset 置为 0,为了保证能够加载到原来的的数据,rowCount 需要加上 offset。

3.4.1 分页补充

OffsetToken、RowCountToken 只有在分页对应位置为非占位符 ? 才存在。当对应位置是占位符时,会对分页条件对应的预编译 SQL 占位符参数进行重写,整体逻辑和 OffsetToken、RowCountToken 是一致的。

核心代码为ParsingSQLRouter#processLimit:

    private void processLimit(final List parameters, final SelectStatement selectStatement, final boolean isSingleRouting) {
        if (isSingleRouting) {
            // 单分片
            selectStatement.setLimit(null);
            return;
        }
        // 是否需要加载全部数据
        boolean isNeedFetchAll = (!selectStatement.getGroupByItems().isEmpty() || !selectStatement.getAggregationSelectItems().isEmpty()) && !selectStatement.isSameGroupByAndOrderByItems();
        // 填充改写分页参数.
        selectStatement.getLimit().processParameters(parameters, isNeedFetchAll);
    }

    // Limit.java
    public void processParameters(final List parameters, final boolean isFetchAll) {
        fill(parameters);
        rewrite(parameters, isFetchAll);
    }

    private void fill(final List parameters) {
        int offset = 0;
        if (null != this.offset) {
            offset = -1 == this.offset.getIndex() ? getOffsetValue() : NumberUtil.roundHalfUp(parameters.get(this.offset.getIndex()));
            this.offset.setValue(offset);
        }
        int rowCount = 0;
        if (null != this.rowCount) {
            rowCount = -1 == this.rowCount.getIndex() ? getRowCountValue() : NumberUtil.roundHalfUp(parameters.get(this.rowCount.getIndex()));
            this.rowCount.setValue(rowCount);
        }
        if (offset < 0 || rowCount < 0) {
            throw new SQLParsingException("LIMIT offset and row count can not be a negative value.");
        }
    }
    
    private void rewrite(final List parameters, final boolean isFetchAll) {
        int rewriteOffset = 0;
        int rewriteRowCount;
        // 重写
        if (isFetchAll) {
            rewriteRowCount = Integer.MAX_VALUE;
        } else if (isNeedRewriteRowCount()) {
            rewriteRowCount = null == rowCount ? -1 : getOffsetValue() + rowCount.getValue();
        } else {
            rewriteRowCount = rowCount.getValue();
        }
        // 参数设置
        if (null != offset && offset.getIndex() > -1) {
            parameters.set(offset.getIndex(), rewriteOffset);
        }
        if (null != rowCount && rowCount.getIndex() > -1) {
            parameters.set(rowCount.getIndex(), rewriteRowCount);
        }
    }

3.5 OrderByToken

调用appendOrderByToken()方法拼接。数据库里,当无 ORDER BY 条件,而有 GROUP BY 条件时候,会使用 GROUP BY条件将结果排序:

// SQLRewriteEngine.java
private void appendOrderByToken(final SQLBuilder sqlBuilder, final int count, final List sqlTokens) {
        SelectStatement selectStatement = (SelectStatement) sqlStatement;
        StringBuilder orderByLiterals = new StringBuilder();
        // 拼接 OrderByToken
        orderByLiterals.append(" ").append(DefaultKeyword.ORDER).append(" ").append(DefaultKeyword.BY).append(" ");
        int i = 0;
        for (OrderItem each : selectStatement.getOrderByItems()) {
            String columnLabel = SQLUtil.getOriginalValue(each.getColumnLabel(), databaseType);
            if (0 == i) {
                orderByLiterals.append(columnLabel).append(" ").append(each.getType().name());
            } else {
                orderByLiterals.append(",").append(columnLabel).append(" ").append(each.getType().name());
            }
            i++;
        }
        orderByLiterals.append(" ");
        sqlBuilder.appendLiterals(orderByLiterals.toString());
        int beginPosition = ((SelectStatement) sqlStatement).getGroupByLastPosition();
        // 拼接当前 SQLToken 到下一个 SQLToken 之间的 SQL串
        appendRest(sqlBuilder, count, sqlTokens, beginPosition);
    }

当 SQL 为 select id from ts_redeem_plan group by id 返回结果:


数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写_第5张图片

也就是说,select id from ts_redeem_plan group by id 等价于 select id from ts_redeem_plan group by id ORDER BY id ASC;
select id from ts_redeem_plan group by id DESC 等价于 select id from ts_redeem_plan group by id ORDER BY id DESC。

3.6 GeneratedKeyToken

GeneratedKeyToken,和其它 SQLToken 不同,它是在 SQL解析完就进行处理,而其他 SQLToken 是在路由完成之后处理的。

// ParsingSQLRouter.java
public SQLStatement parse(final String logicSQL, final int parametersSize) {
        SQLParsingEngine parsingEngine = new SQLParsingEngine(databaseType, logicSQL, shardingRule);
        SQLStatement result = parsingEngine.parse();
        if (result instanceof InsertStatement) {
            // 处理 GenerateKeyToken
            ((InsertStatement) result).appendGenerateKeyToken(shardingRule, parametersSize);
        }
        return result;
    }

// InsertStatement.java
public void appendGenerateKeyToken(final ShardingRule shardingRule, final int parametersSize) {
        if (null != generatedKey) {
            // SQL 里有主键列
            return;
        }
        Optional tableRule = shardingRule.tryFindTableRule(getTables().getSingleTableName());
        if (!tableRule.isPresent()) {
            return;
        }
        Optional generatedKeysToken = findGeneratedKeyToken();
        if (!generatedKeysToken.isPresent()) {
            // 没有生成 GeneratedKeyToken
            return;
        }
        ItemsToken valuesToken = new ItemsToken(generatedKeysToken.get().getBeginPosition());
        if (0 == parametersSize) {
            // 占位符参数数量 = 0 时,直接生成分布式主键
            appendGenerateKeyToken(shardingRule, tableRule.get(), valuesToken);
        } else {
            // 占位符参数数量 > 0 时,生成自增列的占位符,保持有占位符
            appendGenerateKeyToken(shardingRule, tableRule.get(), valuesToken, parametersSize);
        }
        // 移除 generatedKeysToken
        getSqlTokens().remove(generatedKeysToken.get());
        //  新增 ItemsToken
        getSqlTokens().add(valuesToken);
    }

根据占位符参数数量不同,调用的appendGenerateKeyToken() 是不同的:

  • 占位符参数数量 = 0 时,直接生成分布式主键,保持无占位符的做法。
// InsertStatement.java
private void appendGenerateKeyToken(final ShardingRule shardingRule, final TableRule tableRule, final ItemsToken valuesToken) {
        // 生成分布式主键,默认为 snowflake 算法
        Number generatedKey = shardingRule.generateKey(tableRule.getLogicTable());
        // 添加到 ItemsToken
        valuesToken.getItems().add(generatedKey.toString());
        // 增加 Condition,用于路由
        getConditions().add(new Condition(new Column(tableRule.getGenerateKeyColumn(), tableRule.getLogicTable()), new SQLNumberExpression(generatedKey)), shardingRule);
        // 生成 GeneratedKey
        this.generatedKey = new GeneratedKey(tableRule.getLogicTable(), -1, generatedKey);
    }
  • 占位符参数数量 > 0 时,生成自增列的占位符,保持有占位符的做法。
    // InsertStatement.java
    private void appendGenerateKeyToken(final ShardingRule shardingRule, final TableRule tableRule, final ItemsToken valuesToken, final int parametersSize) {
       // 生成占位符
       valuesToken.getItems().add("?");
       // 增加 Condition,用于路由
       getConditions().add(new Condition(new Column(tableRule.getGenerateKeyColumn(), tableRule.getLogicTable()), new SQLPlaceholderExpression(parametersSize)), shardingRule);
       // 生成 GeneratedKey,parametersSize 作为其 index
       generatedKey = new GeneratedKey(tableRule.getGenerateKeyColumn(), parametersSize, null);
    }

因为 GenerateKeyToken 已经处理完,所以移除,避免 SQLRewriteEngine#rewrite()二次改写。另外,通过 ItemsToken 补充自增列。

生成 GeneratedKey 会在ParsingSQLRouter#route进一步处理。

// ParsingSQLRouter.java
public SQLRouteResult route(final String logicSQL, final List parameters, final SQLStatement sqlStatement) {
   final Context context = MetricsContext.start("Route SQL");
   SQLRouteResult result = new SQLRouteResult(sqlStatement);
   // 处理 插入SQL 主键字段
   if (sqlStatement instanceof InsertStatement && null != ((InsertStatement) sqlStatement).getGeneratedKey()) {
       processGeneratedKey(parameters, (InsertStatement) sqlStatement, result);
   }
   // ... 省略部分代码
}

private void processGeneratedKey(final List parameters, final InsertStatement insertStatement, final SQLRouteResult sqlRouteResult) {
        GeneratedKey generatedKey = insertStatement.getGeneratedKey();
        if (parameters.isEmpty()) {
            // 已有主键,无占位符,INSERT INTO t_order(order_id, user_id) VALUES (1, 100);
            sqlRouteResult.getGeneratedKeys().add(generatedKey.getValue());
        } else if (parameters.size() == generatedKey.getIndex()) {
            // 主键字段不存在存在,INSERT INTO t_order(user_id) VALUES(?);
            Number key = shardingRule.generateKey(insertStatement.getTables().getSingleTableName());
            parameters.add(key);
            setGeneratedKeys(sqlRouteResult, key);
        } else if (-1 != generatedKey.getIndex()) {
            // 主键字段存在,INSERT INTO t_order(order_id, user_id) VALUES(?, ?);
            setGeneratedKeys(sqlRouteResult, (Number) parameters.get(generatedKey.getIndex()));
        }
    }

parameters.size()==generatedKey.getIndex() 处对应 appendGenerateKeyToken() 的占位符参数数量 > 0 情况,此时会根据客户端配置的主键生成算法生成分布式主键,同样默认算法是雪花算法。该处是不是可以考虑把生成分布式主键挪到 appendGenerateKeyToken(),这样更加统一一些。

4. SQL 生成

SQL 路由完后,会生成各数据分片的执行 SQL。

// ParsingSQLRouter.java
public SQLRouteResult route(final String logicSQL, final List parameters, final SQLStatement sqlStatement) {
        SQLRouteResult result = new SQLRouteResult(sqlStatement);
        // 省略部分代码... 处理 插入SQL 主键字段
        // 路由
        RoutingResult routingResult = route(parameters, sqlStatement);
        // SQL重写引擎
        SQLRewriteEngine rewriteEngine = new SQLRewriteEngine(shardingRule, logicSQL, databaseType, sqlStatement);
        boolean isSingleRouting = routingResult.isSingleRouting();
        // 省略部分代码... 处理分页
        // SQL 重写
        SQLBuilder sqlBuilder = rewriteEngine.rewrite(!isSingleRouting);
        // 生成 ExecutionUnit
        if (routingResult instanceof CartesianRoutingResult) {
            for (CartesianDataSource cartesianDataSource : ((CartesianRoutingResult) routingResult).getRoutingDataSources()) {
                for (CartesianTableReference cartesianTableReference : cartesianDataSource.getRoutingTableReferences()) {
                    // 生成 SQL
                    result.getExecutionUnits().add(new SQLExecutionUnit(cartesianDataSource.getDataSource(), rewriteEngine.generateSQL(cartesianTableReference, sqlBuilder)));
                }
            }
        } else {
            for (TableUnit each : routingResult.getTableUnits().getTableUnits()) {
                // 生成 SQL
                result.getExecutionUnits().add(new SQLExecutionUnit(each.getDataSourceName(), rewriteEngine.generateSQL(each, sqlBuilder)));
            }
        }
        if (showSQL) {
            SQLLogger.logSQL(logicSQL, sqlStatement, result.getExecutionUnits(), parameters);
        }
        return result;
    }

调用RewriteEngine#generateSQL()生成执行 SQL。对于笛卡尔积路由结果和简单路由结果传递的参数略有不同:
前者使用 CartesianDataSource ( CartesianTableReference ),后者使用路由表单元 ( TableUnit )。

但是处理上大体是一致的:

  1. 获得 SQL 相关逻辑表对应的真实表映射;
  2. 根据映射改写 SQL 相关逻辑表为真实表。
    // SQLRewriteEngine.java
    /**
    * 生成SQL语句.
    * @param tableUnit 路由表单元
    * @param sqlBuilder SQL构建器
    * @return SQL语句
    */
    public String generateSQL(final TableUnit tableUnit, final SQLBuilder sqlBuilder) {
       return sqlBuilder.toSQL(getTableTokens(tableUnit));
    }  

    /**
    * 生成SQL语句.
    * @param cartesianTableReference 笛卡尔积路由表单元
    * @param sqlBuilder SQL构建器
    * @return SQL语句
    */
    public String generateSQL(final CartesianTableReference cartesianTableReference, final SQLBuilder sqlBuilder) {
       return sqlBuilder.toSQL(getTableTokens(cartesianTableReference));
    }

    // SQLBuilder.java
    /**
    * 生成SQL语句.
    * @param tableTokens 占位符集合(逻辑表与真实表映射)
    * @return SQL语句
    */
    public String toSQL(final Map tableTokens) {
       StringBuilder result = new StringBuilder();
       for (Object each : segments) {
           if (each instanceof TableToken && tableTokens.containsKey(((TableToken) each).tableName)) {
               result.append(tableTokens.get(((TableToken) each).tableName));
           } else {
               result.append(each);
           }
       }
       return result.toString();
    }

下面我们以笛卡尔积路由结果获得 SQL 相关逻辑表对应的真实表映射为例子(简单路由结果基本类似而且简单)。

    // SQLRewriteEngine.java
    /**
    * 获得笛卡尔积表路由组里的路由表单元逻辑表 和 对应真实表的映射
    * 以及与其互为BindingTable关系的逻辑表 和 对应真实表的映射(逻辑表需要在 SQL 中存在)
    * @param cartesianTableReference 笛卡尔积表路由组
    * @return 集合
    */
    private Map getTableTokens(final CartesianTableReference cartesianTableReference) {
       Map tableTokens = new HashMap<>();
       for (TableUnit each : cartesianTableReference.getTableUnits()) {
           // 将笛卡尔积表路由组里的路由逻辑表和对应真实表做映射
           tableTokens.put(each.getLogicTableName(), each.getActualTableName());
           // 查找 BindingTableRule
           Optional bindingTableRule = shardingRule.findBindingTableRule(each.getLogicTableName());
           if (bindingTableRule.isPresent()) {
              // 将与其互为 BindingTable 关系的逻辑表和对应真实表做映射
               tableTokens.putAll(getBindingTableTokens(each, bindingTableRule.get()));
           }
       }
       return tableTokens;
    }

    /**
    * 获得 BindingTable 关系的逻辑表对应的真实表映射(逻辑表需要在 SQL 中存在)
    * @param tableUnit 路由单元
    * @param bindingTableRule Binding表规则配置对象
    * @return 映射
    */
    private Map getBindingTableTokens(final TableUnit tableUnit, final BindingTableRule bindingTableRule) {
       Map result = new HashMap<>();
       for (String eachTable : sqlStatement.getTables().getTableNames()) {
           if (!eachTable.equalsIgnoreCase(tableUnit.getLogicTableName()) && bindingTableRule.hasLogicTable(eachTable)) {
               result.put(eachTable, bindingTableRule.getBindingActualTable(tableUnit.getDataSourceName(), eachTable, tableUnit.getActualTableName()));
           }
       }
       return result;
    }

笛卡尔积表路由组( CartesianTableReference )包含多个路由表单元( TableUnit ),每个路由表单元需要遍历。

路由表单元本身包含逻辑表和真实表,直接添加到映射即可。

互为 BindingTable 关系的表只计算一次路由分片,因此未计算的真实表需要以其对应的已计算的真实表去查找,即 bindingTableRule.getBindingActualTable(tableUnit.getDataSourceName(),eachTable,tableUnit.getActualTableName())处逻辑。

    // BindingTableRule.java
    /**
    * 根据其他 Binding 表真实表名称获取相应的真实 Binding 表名称.
    * 
    * @param dataSource 数据源名称
    * @param logicTable 逻辑表名称
    * @param otherActualTable 其他真实Binding表名称
    * @return 真实Binding表名称
    */
    public String getBindingActualTable(final String dataSource, final String logicTable, final String otherActualTable) {
       // 计算 otherActualTable 在其 TableRule 的 actualTable 是第几个
       int index = -1;
       for (TableRule each : tableRules) {
           if (each.isDynamic()) {
               throw new UnsupportedOperationException("Dynamic table cannot support Binding table.");
           }
           index = each.findActualTableIndex(dataSource, otherActualTable);
           if (-1 != index) {
               break;
           }
       }
       Preconditions.checkState(-1 != index, String.format("Actual table [%s].[%s] is not in table config", dataSource, otherActualTable));
       // 计算 logicTable 在其 TableRule 的 第index 的 真实表
       for (TableRule each : tableRules) {
           if (each.getLogicTable().equalsIgnoreCase(logicTable)) {
               return each.getActualTables().get(index).getTableName();
           }
       }
       throw new IllegalStateException(String.format("Cannot find binding actual table, data source: %s, logic table: %s, other actual table: %s", dataSource, logicTable, otherActualTable));
    }

也就是说当 actualTable 为 db.order_01 时,调用
getBindingActualTable方法,就会推算出其 BindingTable 关系的另一张表的 actualTable 为 db.order_item_01。

因为互为 BindingTable 的表,配置 TableRule 时,有如下需要遵守的规则:

  • 分片策略与算法相同
  • 数据源配置对象相同
  • 真实表数量相同

5. 结语

SQL 改写完成,也就意味着ParsingSQLRouter#route(logicSQL, parameters, sqlStatement)方法执行完毕了,其路由结果封装在SQLRouteResult中:

public final class SQLRouteResult {
    
    private final SQLStatement sqlStatement;
    
    private final Set executionUnits = new LinkedHashSet<>();
    
    private final List generatedKeys = new LinkedList<>();
}

SQLExecutionUnit就是我们 SQL 改写之后的执行单元,下一篇文章我们将继续探讨 SQL 执行流程,来处理这些执行单元,尽请关注~

你可能感兴趣的:(数据库中间件 Sharding-JDBC 源码分析 —— SQL 改写)