Mybatis plus 动态表名插件开发
开发背景:表进行数据归档时,结构一致,但调用的时候又不想重复复制相关的代码逻辑,所以开发了个动态修改表名的插件。虽然说高版本的 mybatis plus 提供了同样的插件,但是需要升级版本。高版本的 mybatis plus 改动太大,升级的话有很大的风险,所以就自己开发了一个插件。
插件
mybatis 插件
代码语言:text复制import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.rookie.mybatis.study.entity.DynamicTableInfo;
import com.rookie.mybatis.study.entity.DynamicTableName;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
/**
* @author rookie
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicTableInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 只处理增删改查语句
if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
&& SqlCommandType.INSERT != mappedStatement.getSqlCommandType()
&& SqlCommandType.DELETE != mappedStatement.getSqlCommandType()
&& SqlCommandType.UPDATE != mappedStatement.getSqlCommandType()) {
return invocation.proceed();
}
// 针对定义了rowBounds,做为mapper接口方法的参数
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
Object paramObj = boundSql.getParameterObject();
DynamicTableInfo info = extractInfo(paramObj);
Map<String, String> tableMap = TablePluginContainer.TABLE_NAME_THREAD_LOCAL.get();
// 如果参数或者 ThreadLocal 中带有需要替换的表名
boolean replaceProcess = (info == null || ObjectUtils.isEmpty(info.getTableNames())) && tableMap == null;
if (replaceProcess) {
return invocation.proceed();
}
Map<String, String> tableNameMap = null;
// 以 ThreadLocal 中的数据优先
if (tableMap != null) {
tableNameMap = tableMap;
} else {
tableNameMap = info.getTableNames().stream().collect(Collectors.toMap(DynamicTableName::getCurrentTable,
DynamicTableName::getItemTable, (v1, v2) -> v1));
}
String originalSql = boundSql.getSql();
Statement statement = CCJSqlParserUtil.parse(originalSql);
// 替换相应表名
originalSql = alterTableName(mappedStatement, statement, tableNameMap);
metaObject.setValue("delegate.boundSql.sql", originalSql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
private static Table alterTableName(Table table, Map<String, String> tableNameMap) {
String tableName = tableNameMap.get(table.getName());
if (StringUtils.isNotBlank(tableName)) {
table.setName(tableName);
}
return table;
}
private static DynamicTableInfo extractInfo(Object paramObj) {
// 判断参数里是否有DynamicTableInfo对象
DynamicTableInfo info = null;
if (paramObj instanceof DynamicTableInfo) {
info = (DynamicTableInfo) paramObj;
} else if (paramObj instanceof Map) {
for (Object arg : ((Map<?, ?>) paramObj).values()) {
if (arg instanceof DynamicTableInfo) {
info = (DynamicTableInfo) arg;
break;
}
}
}
return info;
}
private static String alterTableName(MappedStatement mappedStatement, Statement statement, Map<String, String> tableNameMap) {
List<Table> tableList = new ArrayList<>();
String originalSql = null;
if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {
Insert insertStatement = (Insert) statement;
Table table = alterTableName(insertStatement.getTable(), tableNameMap);
insertStatement.setTable(table);
originalSql = insertStatement.toString();
} else if (mappedStatement.getSqlCommandType() == SqlCommandType.DELETE) {
Delete deleteStatement = (Delete) statement;
Table table = alterTableName(deleteStatement.getTable(), tableNameMap);
deleteStatement.setTable(table);
originalSql = deleteStatement.toString();
} else if (mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) {
Update updateStatement = (Update) statement;
for (Table p : updateStatement.getTables()) {
Table table = alterTableName(p, tableNameMap);
tableList.add(table);
}
updateStatement.setTables(tableList);
originalSql = updateStatement.toString();
} else if (mappedStatement.getSqlCommandType() == SqlCommandType.SELECT) {
Select selectStatement = (Select) statement;
PlainSelect plainSelect = (PlainSelect) selectStatement.getSelectBody();
Table fromItem = (Table) plainSelect.getFromItem();
Table table = alterTableName(fromItem, tableNameMap);
plainSelect.setFromItem(table);
List<Join> joinList = new ArrayList<>();
if (!ObjectUtils.isEmpty(plainSelect.getJoins())) {
for (Join p : plainSelect.getJoins()) {
if (p.getRightItem() != null) {
Table tableInner = alterTableName((Table) p.getRightItem(), tableNameMap);
p.setRightItem(tableInner);
joinList.add(p);
}
}
plainSelect.setJoins(joinList);
}
originalSql = plainSelect.toString();
}
return originalSql;
}
}
因为 mybatis plus 重写了一些类,只要将相关的插件放到 Spring 当中就会自动加载插件。所以这里注册一下 Bean 就行。
代码语言:typescript复制@Configuration
public class MybatisPlusConfig {
@Bean
public DynamicTableInterceptor dynamicTableInterceptor(){
return new DynamicTableInterceptor();
}
}
其他类
代码语言:swift复制public class TablePluginContainer {
/**
* 需要更改表名的缓存,请调用后手动清除
* 当前表,需要替换的表名
*/
public static ThreadLocal<Map<String,String>> TABLE_NAME_THREAD_LOCAL = new ThreadLocal<>();
}
@Data
public class DynamicTableInfo {
private List<DynamicTableName> tableNames;
}
@Data
@AllArgsConstructor
public class DynamicTableName {
/**
* 当前表
*/
private String currentTable;
/**
* 需要替换的表
*/
private String itemTable;
}
使用方法
这里提供了两种方式注入。
第一种是像分页插件一样在 mapper 注入相应的实体类就可以了。
代码语言:text复制void selectTest(DynamicTableInfo dynamicTableInfo);
第二种是在逻辑代码的前后添加,考虑到 plus 是动态生成 sql,还是 threadLocal 方便点。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!