在介绍路由引擎的最后一篇文章《ShardingSphere源码解析之路由引擎(七)》中,我们在BaseShardingEngine的shard方法中看到了ShardingSphere中另一个重要的概念,即SQL改写(Rewrite)。SQL改写在分库分表框架中通常位于路由之后,也是整个SQL执行流程中的重要的一个环节,因为开发人员是面向逻辑库与逻辑表书写的SQL,并不能够直接在真实的数据库中执行,SQL改写用于将逻辑SQL改写为在真实数据库中可以正确执行的SQL。
让我们来看一下BaseShardingEngine中用于执行改写逻辑的rewriteAndConvert方法:
private Collection<RouteUnit> rewriteAndConvert(final String sql, final List<Object> parameters, final SQLRouteResult sqlRouteResult) {
//构建SQLRewriteContext
SQLRewriteContext sqlRewriteContext = new SQLRewriteContext(metaData.getRelationMetas(), sqlRouteResult.getSqlStatementContext(), sql, parameters);
//构建ShardingSQLRewriteContextDecorator对SQLRewriteContext进行装饰
new ShardingSQLRewriteContextDecorator(shardingRule, sqlRouteResult).decorate(sqlRewriteContext);
//判断是否是数据脱敏列
boolean isQueryWithCipherColumn = shardingProperties.<Boolean>getValue(ShardingPropertiesConstant.QUERY_WITH_CIPHER_COLUMN);
//构建EncryptSQLRewriteContextDecorator对SQLRewriteContext进行装饰
new EncryptSQLRewriteContextDecorator(shardingRule.getEncryptRule(), isQueryWithCipherColumn).decorate(sqlRewriteContext);
//生成SQLTokens
sqlRewriteContext.generateSQLTokens();
Collection<RouteUnit> result = new LinkedHashSet<>();
for (RoutingUnit each : sqlRouteResult.getRoutingResult().getRoutingUnits()) {
//构建ShardingSQLRewriteEngine
ShardingSQLRewriteEngine sqlRewriteEngine = new ShardingSQLRewriteEngine(shardingRule, sqlRouteResult.getShardingConditions(), each);
//执行改写
SQLRewriteResult sqlRewriteResult = sqlRewriteEngine.rewrite(sqlRewriteContext);
//保存改写结果
result.add(new RouteUnit(each.getDataSourceName(), new SQLUnit(sqlRewriteResult.getSql(), sqlRewriteResult.getParameters())));
}
return result;
}
这段代码虽然内容不多,但确完整描述了实现SQL改写的整体流程。这里面涉及到的核心类也很多,值得我们花几篇文章对其进行详细展开。在此之前,我们还是给出相关核心类的整体结构,如下图所示:
针对上图,我们发现在改写引擎中,SQLRewriteContext是一个非常重要的类,SQLRewriteEngine、SQLRewriteContextDecorator等核心接口都与它有依赖关系。从命名上讲,SQLRewriteContext是一个上下文对象,可以想象肯定保存着与SQL改写相关的很多数据信息,让我们来看一下它的变量定义,如下所示:
private final RelationMetas relationMetas;
private final SQLStatementContext sqlStatementContext;
private final String sql;
private final List<Object> parameters;
private final List<SQLToken> sqlTokens = new LinkedList<>();
private final ParameterBuilder parameterBuilder;
@Getter(AccessLevel.NONE)
private final SQLTokenGenerators sqlTokenGenerators = new SQLTokenGenerators();
在这里,我们看到了前面已经介绍过的SQLStatementContext,也看到了新的SQLToken和SQLTokenGenerators。随着内容的演进,这些对象都会逐一进行介绍。这里我们先明确SQLRewriteContext中保存着用于SQL改写的相关信息,而且这些信息的构建过程会根据不同的应用场景而有所不同。
我们先来看一下这里的SQLToken对象,该对象在改写引擎中重要性很高,SQLRewriteEngine正是基于SQLToken实现了SQL改写。SQLToken定义如下所示:
@RequiredArgsConstructor
@Getter
public abstract class SQLToken implements Comparable<SQLToken> {
private final int startIndex;
@Override
public final int compareTo(final SQLToken sqlToken) {
return startIndex - sqlToken.getStartIndex();
}
}
可以看到SQLToken实际上是一个抽象类,在ShardingSphere中,存在了一大批SQLToken的子类,其类层结构如下所示:
这些SQLToken多数跟SQL改写相关(包名中包含rewrite),而有些在改写的基础上还与后面要讲到的数据脱敏功能相关(包名中还包含着encrypt),数据脱敏也是ShardingSphere提供的一项非常实用的功能,我们后面会有专题进行介绍。同时,这里的部分SQLToken位于shardingsphere-rewrite-engine工程中,而有些则位于sharding-core-rewrite工程中,这点也需要注意。
结合SQL改写的常见场景,上图中的部分SQLToken的含义我们可以从字面意思上直接进行理解。
例如,对于INSERT语句而言,如果使用数据库自增主键,是无需写入主键字段的。但数据库的自增主键是无法满足分布式场景下的主键唯一性,因此ShardingSphere提供了分布式自增主键的生成策略,并且可以通过补列,让使用方无需改动现有代码,即可将分布式自增主键透明的替换数据库现有的自增主键。关于ShardingSphere中的分布式主键的介绍可以回顾《ShardingSphere源码解析之分布式主键》中的内容。举例说明,假设表t_order的主键是order_id,原始的SQL为:
INSERT INTO t_order (`field1`, `field2`) VALUES (10, 1);
可以看到,上述SQL中并未包含自增主键,需要数据库自行填充。ShardingSphere配置自增主键后,SQL将改写为:
INSERT INTO t_order (`field1`, `field2`, order_id) VALUES (10, 1, xxxxx);
改写后的SQL将在INSERT语句的最后部分增加主键列名称以及自动生成的自增主键值。上述SQL中的xxxxx表示自动生成的自增主键值。
从命名上看,GeneratedKeyInsertColumnToken对应上述的自动主键填充的场景,这实际上属于常见的一种SQL改写策略,即补列。GeneratedKeyInsertColumnToken的实现如下所示:
public final class GeneratedKeyInsertColumnToken extends SQLToken implements Attachable {
private final String column;
public GeneratedKeyInsertColumnToken(final int startIndex, final String column) {
super(startIndex);
this.column = column;
}
@Override
public String toString() {
return String.format(", %s", column);
}
}
可以看到这里多了一个column变量用于指定主键的所在列。我们再来跟踪GeneratedKeyInsertColumnToken的构造函数调用情况,发现在GeneratedKeyInsertColumnTokenGenerator中创建了GeneratedKeyInsertColumnToken。顾名思义,GeneratedKeyInsertColumnTokenGenerator是一种TokenGenerator,专门负责生成具体的Token。TokenGenerator接口定义如下:
public interface SQLTokenGenerator {
//判断是否要生成SQLToken
boolean isGenerateSQLToken(SQLStatementContext sqlStatementContext);
}
该接口还有两个子接口,分别是负责生成单个SQLToken的OptionalSQLTokenGenerator和负责生成批量SQLToken的CollectionSQLTokenGenerator,如下所示:
public interface OptionalSQLTokenGenerator extends SQLTokenGenerator {
//生成单个SQLToken
SQLToken generateSQLToken(SQLStatementContext sqlStatementContext);
}
public interface CollectionSQLTokenGenerator extends SQLTokenGenerator {
//生成批量SQLToken
Collection<? extends SQLToken> generateSQLTokens(SQLStatementContext sqlStatementContext);
}
在ShardingSphere,和SQLToken一样,TokenGenerator的类层结构也比较复杂,类层结构如下所示:
对于GeneratedKeyInsertColumnTokenGenerator而言,它还有一个抽象的基类,即上图中的BaseGeneratedKeyTokenGenerator,其实现如下所示:
public abstract class BaseGeneratedKeyTokenGenerator implements OptionalSQLTokenGenerator, SQLRouteResultAware {
private SQLRouteResult sqlRouteResult;
@Override
public final boolean isGenerateSQLToken(final SQLStatementContext sqlStatementContext) {
return sqlStatementContext instanceof InsertSQLStatementContext && sqlRouteResult.getGeneratedKey().isPresent()
&& sqlRouteResult.getGeneratedKey().get().isGenerated() && isGenerateSQLToken((InsertStatement) sqlStatementContext.getSqlStatement());
}
protected abstract boolean isGenerateSQLToken(InsertStatement insertStatement);
@Override
public final SQLToken generateSQLToken(final SQLStatementContext sqlStatementContext) {
Preconditions.checkState(sqlRouteResult.getGeneratedKey().isPresent());
return generateSQLToken(sqlStatementContext, sqlRouteResult.getGeneratedKey().get());
}
protected abstract SQLToken generateSQLToken(SQLStatementContext sqlStatementContext, GeneratedKey generatedKey);
}
这个抽象类留下了两个模板方法isGenerateSQLToken和generateSQLToken交由子类进行实现,在GeneratedKeyInsertColumnTokenGenerator中,这两个方法的实现如下所示:
public final class GeneratedKeyInsertColumnTokenGenerator extends BaseGeneratedKeyTokenGenerator {
@Override
protected boolean isGenerateSQLToken(final InsertStatement insertStatement) {
Optional<InsertColumnsSegment> sqlSegment = insertStatement.findSQLSegment(InsertColumnsSegment.class);
return sqlSegment.isPresent() && !sqlSegment.get().getColumns().isEmpty();
}
@Override
protected GeneratedKeyInsertColumnToken generateSQLToken(final SQLStatementContext sqlStatementContext, final GeneratedKey generatedKey) {
Optional<InsertColumnsSegment> sqlSegment = sqlStatementContext.getSqlStatement().findSQLSegment(InsertColumnsSegment.class);
Preconditions.checkState(sqlSegment.isPresent());
return new GeneratedKeyInsertColumnToken(sqlSegment.get().getStopIndex(), generatedKey.getColumnName());
}
}
我们看到在上述generateSQLToken方法中,通过利用在SQL解析引擎中获取的InsertColumnsSegment以及从用于生成分布式主键的GeneratedKey中获取对应的主键列,我们就可以构建一个GeneratedKeyInsertColumnToken。
关于SQLToken的基本概念以及它的其中一个实现类GeneratedKeyInsertColumnToken就介绍到这里,关于其他SQLToken实现类我们不会全部展开,部分会在后面的内容中进行穿插介绍。
作为总结,从今天起,我们正式进入到ShardingSphere中关于改写引擎部分的讲解,本文先从整体结构上给出了改写引擎部分的核心类,然后重点分析了改写引擎的上下文对象SQLRewriteContext中与SQLToken相关的内容。
更多内容可以关注我的公众号:程序员向架构师转型。