MybatisPlus实现数据库分表查询

在程序的运行中,随着时间的推移,势必会发生数据库中的表中的数据暴涨,尤其是一些接口日志表或者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提供了以下一系列的解析器的功能
MybatisPlus实现数据库分表查询_第1张图片
图中的红框就是动态替换表名的解析器,看看其中对于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 tableNameHandlerMap中,然后构建一个DynamicTableNameParser,将map定义进去,在将DynamicTableNameParser加到 PaginationInterceptor的变量sqlParserList几何中即可。
三、具体的实现代码
关键的逻辑就是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;
    }

}

你可能感兴趣的:(java,mybatis)