工作中需要用到MyBatis进行分表操作,简单记录实现过程
MyBatis Interceptor
MyBatis 允许你在已映射语句执行过程中的某一点使用插件来拦截,包括
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)[拦截执行器的方法]
ParameterHandler (getParameterObject, setParameters)[拦截参数的处理]
ResultSetHandler (handleResultSets, handleOutputParameters)[拦截结果集的处理]
StatementHandler (prepare, parameterize, batch, update, query)[拦截Sql语法构建的处理]
Interceptor接口
public interface Interceptor {
//进行拦截的时候要执行的方法
Object intercept(Invocation invocation) throws Throwable;
//决定是否要进行拦截进而决定要返回一个什么样的目标对象
Object plugin(Object target);
//Mybatis配置文件中指定一些属性
void setProperties(Properties properties);
}
@Intercepts
@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。
@Intercepts( {
@Signature(method = "query", type = Executor.class, args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }),
@Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
利用Interceptor实现自定义分表原理
利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,Sql语句生成的时机是在Statement之前的,利用Interceptor可以在Sql语句成为Statement之前对Sql语句进行修改。
Mybatis中Statement语句是通过RoutingStatementHandler对象的 prepare方法生成的。使用Interceptor对StatementHandler的prepare方法进行拦截即可获得原始Sql。
获取原始Sql后,可以解析出根表名,再通过一系列的策略获取到实际的分表表名,对其原始Sql进行表名替换。
示例
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class CustomerIdShardInterceptor implements Interceptor {
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 保存会话信息
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler,
DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
// 获取原sql
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
//修改原有Sql数据
String modifiedSql = modifySql(boundSql);
//将修改后的Sql设置到metaStatementHandler中
metaStatementHandler.setValue("delegate.boundSql.sql", modifiedSql);
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
//对应类型进行封装
if (o instanceof StatementHandler) {
return Plugin.wrap(o, this);
} else {
return o;
}
}
@Override
public void setProperties(Properties properties) {
}
private String modifySql(BoundSql boundSql) {
String targetSql = boundSql.getSql().trim().toLowerCase();
String tableName = getTableName(targetSql);
// 对targetSql进行表名修改
...
return targetSql;
}
/**
* 根据sql获取表名
*
* @param sql
* @return
*/
private String getTableName(String sql) {
String[] sqls = sql.split("\\s+");
switch (sqls[0]) {
case "select": {
// select tableName
for (int i = 0; i < sqls.length; i++) {
if (sqls[i].equals("from")) {
return sqls[i + 1];
}
}
break;
}
case "update": {
// update tableName
return sqls[1];
}
case "insert": {
// insert into tableName
return sqls[2];
}
case "delete": {
// delete tableName
return sqls[1];
}
}
return StringUtils.EMPTY;
}
}
关于分表策略
取模
- 可以维护一个根symbols表用来存储插入表的自增的id代表固定业务id,使用id取模进行分表(存储分表table_suffix = table_(id%i),i的大小取决于数据量的大小),由此策略可分table_0~table_i-1个分表进行数据存储。
- 插入时根据symbols表的自增业务id进行取模对对应表进行插入。
- 查询时从symbols对查询业务id进行取模,决定去哪个分表查询