在程序的运行中,随着时间的推移,势必会发生数据库中的表中的数据暴涨,尤其是一些接口日志表或者PV记录表,面对数据的越来越多,在处理时就会增加处理难度,直到数据库难以承受。这时就面临着对表格的拆分,将该表格按照不同的维度(时间,类型等),满足开发需求的同时,拆分成多个表。而一个表变成了多个表,代表着原来的操作数据库的映射体系已经无法操作这些表了,如何对这些表进行增删改查呢?难道要为每一个表建立一个Mapper映射的类吗?当然不可能,这时需要运用mybatis拦截器的方式实现动态的更换表名,这些完全可以在自定义mybatis拦截器去实现,但是在mybatisPlus提供了一个简单封装,简化了开发(包括动态表名的解析,还有其他的功能),简单谈一谈。
简单的说就是可以实现用interface_communication_log 表的Mapper类,动态的查询这个表的分表,比如根据时间的分表,查interface_communication_log_202006,interface_communication_log_202007等表。
一、mybatis拦截器
动态的表名替换依靠拦截器实现,简单说一下拦截器。
mybatis拦截器就是mybatis支持在映射语句执行的过程中进行拦截,通过动态代理的方式,改变映射语句的执行逻辑,当然这种拦截并不是任意的,mybatis支持在以下的节点进行拦截
1.Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 拦截执行器的方法;
2.ParameterHandler (getParameterObject, setParameters) 拦截参数的处理;
3.ResultSetHandler (handleResultSets, handleOutputParameters) 拦截结果集的处理;
4.StatementHandler (prepare, parameterize, batch, update, query) 拦截Sql语法构建的处理;
拦截器支持对上述接口中的方法(括号内是接口的方法)进行拦截,这四个接口组成了sql映射语句的执行流程。
自定义的拦截器都需要实现Interceptor接口
public interface Interceptor {
//是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。
Object intercept(Invocation invocation) throws Throwable;
//就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this)来实现返回代理类还是目标类,来决定是否进行拦截。
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//用于设置额外的参数,参数配置在拦截器的Properties节点里。
default void setProperties(Properties properties) {
}
拦截器的格式
//上面注解需要声明拦截的接口type,拦截的是接口中的哪个方法mothed,方法的参数args
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {
Connection.class, Integer.class})})
public class MyMybatisInterceptor implements Interceptor {
}
而对于分表表格进行增删改查简单说就是,在构建sql的时候,动态的用分表后的表名替换原来表名,从而改变操作表格的过程。
二、mybatisPlus具体的实现过程
在Mybatis是没有实现拦截器的,而MybatisPlus中定义了一些拦截器,其中PaginationInterceptor这个拦截器,是一个分页插件,它就是对sql构建的接口进行了拦截,在其中实现了对sql的解析重构。
/**
* 分页拦截器
*
* @author hubin
* @since 2016-01-23
*/
@Setter
@Accessors(chain = true)
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {
Connection.class, Integer.class})})
public class PaginationInterceptor extends AbstractSqlParserHandler implements Interceptor {
}
这个拦截器接口实现了Interceptor 接口,在Intercept方法中实现了sql的解析
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// SQL 解析
this.sqlParser(metaObject);
进到sqlParser方法中看看解析过程
@Data
@Accessors(chain = true)
public abstract class AbstractSqlParserHandler {
//这个就是定义的解析器的接口,只需要将定义的解析器是实现类加如集合中,就会进行解析
private List<ISqlParser> sqlParserList;
private ISqlParserFilter sqlParserFilter;
/**
* 拦截 SQL 解析执行
*/
protected void sqlParser(MetaObject metaObject) {
if (null != metaObject) {
Object originalObject = metaObject.getOriginalObject();
StatementHandler statementHandler = PluginUtils.realTarget(originalObject);
metaObject = SystemMetaObject.forObject(statementHandler);
if (null != this.sqlParserFilter && this.sqlParserFilter.doFilter(metaObject)) {
return;
}
// @SqlParser(filter = true) 跳过该方法解析
if (SqlParserHelper.getSqlParserInfo(metaObject)) {
return;
}
// SQL 解析
if (CollectionUtils.isNotEmpty(this.sqlParserList)) {
// 好像不用判断也行,为了保险起见,还是加上吧.
statementHandler = metaObject.hasGetter("delegate") ? (StatementHandler) metaObject.getValue("delegate") : statementHandler;
if (!(statementHandler instanceof CallableStatementHandler)) {
// 标记是否修改过 SQL
boolean sqlChangedFlag = false;
String originalSql = (String) metaObject.getValue(PluginUtils.DELEGATE_BOUNDSQL_SQL);
//这里就是解析器的解析过程,遍历了所有的解析器,依次对sql进行重构
for (ISqlParser sqlParser : this.sqlParserList) {
if (sqlParser.doFilter(metaObject, originalSql)) {
//接口中的parser方法实现了重构的过程
SqlInfo sqlInfo = sqlParser.parser(metaObject, originalSql);
if (null != sqlInfo) {
originalSql = sqlInfo.getSql();
sqlChangedFlag = true;
}
}
}
if (sqlChangedFlag) {
metaObject.setValue(PluginUtils.DELEGATE_BOUNDSQL_SQL, originalSql);
}
}
}
}
}
}
看一下sql解析器的实现都有哪些,mybatisPlus提供了以下一系列的解析器的功能
图中的红框就是动态替换表名的解析器,看看其中对于parse方法的实现逻辑
/**
* 动态表名 SQL 解析器
*
* @author jobob
* @since 2019-04-23
*/
@Data
@Accessors(chain = true)
public class DynamicTableNameParser implements ISqlParser {
//在这里定义了一个map,键是表名,值是一个函数式接口,其中的方法定义了替换的逻辑
private Map<String, ITableNameHandler> tableNameHandlerMap;
@Override
public SqlInfo parser(MetaObject metaObject, String sql) {
Assert.isFalse(CollectionUtils.isEmpty(tableNameHandlerMap), "tableNameHandlerMap is empty.");
if (allowProcess(metaObject)) {
//获取sql中的所有的表名
Collection<String> tables = new TableNameParser(sql).tables();
if (CollectionUtils.isNotEmpty(tables)) {
boolean sqlParsed = false;
String parsedSql = sql;
//遍历sql中的所有表名
for (final String table : tables) {
//通过表名拿到对应的替换逻辑
ITableNameHandler tableNameHandler = tableNameHandlerMap.get(table);
if (null != tableNameHandler) {
//执行替换的逻辑
parsedSql = tableNameHandler.process(metaObject, parsedSql, table);
sqlParsed = true;
}
}
if (sqlParsed) {
return SqlInfo.newInstance().setSql(parsedSql);
}
}
}
return null;
}
/**
* 判断是否允许执行
* 例如:逻辑删除只解析 delete , update 操作
*
* @param metaObject 元对象
* @return true
*/
public boolean allowProcess(MetaObject metaObject) {
return true;
}
}
综上所述,只需要配置表的映射关系,在ITableNameHandler的dynamicTableName方法中编写替换逻辑, 将映射关系加到 Map
三、具体的实现代码
关键的逻辑就是ITableNameHandler接口中的dynamicTableName,看一下这个函数式接口
/**
* 动态表名处理器
*
* @author jobob
* @since 2019-04-23
*/
public interface ITableNameHandler {
/**
* 表名 SQL 处理
*
* @param metaObject 元对象
* @param sql 当前执行 SQL
* @param tableName 表名
* @return
*/
default String process(MetaObject metaObject, String sql, String tableName) {
//通过dynamic
String dynamicTableName = dynamicTableName(metaObject, sql, tableName);
if (null != dynamicTableName && !dynamicTableName.equalsIgnoreCase(tableName)) {
return sql.replaceAll(tableName, dynamicTableName);
}
return sql;
}
/**
* 生成动态表名,无改变返回 NULL
*
* @param metaObject 元对象
* @param sql 当前执行 SQL
* @param tableName 表名
* @return String
*/
String dynamicTableName(MetaObject metaObject, String sql, String tableName);
}
只需要重写dynamicTableName方法,返回动态表名即可,下面示例写一下时间维度和业务维度分表
@Configuration
@MapperScan(basePackages = {
"com.ruius.chinamobile.*.mapper"})
public class MybatisPlusConfig {
@Resource
private SpringUtil springUtil;
/**
* 设置分页插件
* 设置方言
* @return PaginationInterceptor
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor page = new PaginationInterceptor();
//设置方言类型
page.setDialectType("mysql");
//动态表名
List<ISqlParser> sqlParserList = CollUtil.newArrayList();
DynamicTableNameParser dynamicTableNameParser = new DynamicTableNameParser();
Map<String, ITableNameHandler> tableNameHandlerMap = CollUtil.newHashMap();
//拿到当前时间得年月
String year = DateUtil.format(DateUtil.date(), "yyyyMM");
/**
* 按照时间逻辑分表
* @param metaObject 元对象
* @param sql 当前执行 SQL
* @param tableName 表名
* lambda表示式的三个参数
* @return 动态表名 为当前表名_当前年月
*/
tableNameHandlerMap.put("interface_communication_log", (m, s, tn) -> tn + "_" + year);
/**
* 按照业务逻辑分表
* @return 动态表名 为当前表名_业务名称
*/
tableNameHandlerMap.put("visit_pv_log", (m, s, tn) -> {
//通过元数据一步步拿到sql中业务类型字段
Object originalObject = m.getOriginalObject();
JSONObject originalObjectJSON = JSON.parseObject(JSON.toJSONString(originalObject));
JSONObject boundSql = originalObjectJSON.getJSONObject("boundSql");
JSONObject parameterObject = boundSql.getJSONObject("parameterObject");
//业务的分类字段
String businessType = (String) parameterObject.get("businessType");
if(StrUtil.isBlank(businessType)) {
JSONObject ew = parameterObject.getJSONObject("ew");
JSONArray normal = ew.getJSONObject("expression").getJSONArray("normal");
for (int i = 0; i < normal.size(); i++) {
if(normal.get(i) instanceof JSONObject) {
String sqlSegment = normal.getJSONObject(i).getString("sqlSegment");
if (normal.get(i) instanceof JSONObject && StrUtil.equals(sqlSegment, "business_type")) {
businessType = ew.getJSONObject("paramNameValuePairs").getString("MPGENVAL" + (i));
break;
}
}
}
}
if(StrUtil.isBlank(businessType)) {
//无业务类型返回原表
return tn;
} else {
//模糊查询去掉百分号
businessType = businessType.replaceAll("%", "");
//返回动态表名
return tn + "_" + project.getTableNameSuffix();
}
});
//将表名的映射集合加回到PaginationInterceptor对象
dynamicTableNameParser.setTableNameHandlerMap(tableNameHandlerMap);
sqlParserList.add(dynamicTableNameParser);
page.setSqlParserList(sqlParserList);
return page;
}
}