专注Java教育14年 全国咨询/投诉热线:400-8080-105
动力节点LOGO图
始于2009,口口相传的Java黄埔军校
首页 hot资讯 基于MyBatis分表的实现

基于MyBatis分表的实现

更新时间:2022-03-23 10:38:05 来源:动力节点 浏览2552次

1.大体思路

基于业务来看,想要按月分表,因此数据库表里增加了一个string类型字段 account_month 来记录月份,分表字段就使用account_month。

分表表名:表名_年月 例如明细表:ebs_date_detail_201607。

分表是一月一张表,分表的建立就是默认建立了12个分表,如果超出了,后续再手工添加吧。也可以写个脚本每月底创建下一个月的表,但是觉得没啥必要。就算哪天忘记添加了,代码逻辑的异常处理流程里面也能够保证我的数据不丢失,启动一下异常数据处理也就妥妥的了。

sql语言里面会要求带上分表字段,通过分表字段计算得到分表的表名,然后替换掉原来的sql,直接将数据路由到指定的分表就行了。

听起来好像很简单的样子,那么就这么出发吧。

2.问题目录

分表开始之前的问题:

Mybatis如何找到我们新增的拦截服务。

自定义的拦截服务应该在什么时间拦截查询动作。即什么时间截断Mybatis执行流。

自定义的拦截服务应该拦截什么样的对象。不能拦截什么样的对象。

自定义的拦截服务拦截的对象应该具有什么动作才能被拦截。

自定义的拦截服务如何获取上下文中传入的参数信息。

如何把简单查询,神不知鬼不觉的,无侵入性的替换为分表查询语句。

最后,拦截器应该如何交还被截断的Mybatis执行流。

带着这些问题,我们来看看我们自定义的拦截服务是如何实现的。

3.逐步实现

(1)Mybatis如何找到我们新增的拦截服务

对于拦截器Mybatis为我们提供了一个Interceptor接口,前面有提到,通过实现该接口就可以定义我们自己的拦截器。自定义的拦截器需要交给Mybatis管理,这样才能使得Mybatis的执行与拦截器的执行结合在一起,即,拦截器需要注册到mybatis-config配置文件中。

通过在Mybatis配置文件中plugins元素下的plugin元素来进行。一个plugin对应着一个拦截器,在plugin元素下面我们可以指定若干个property子元素。Mybatis在注册定义的拦截器时会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。

配置文件:mybatis-config.xml

<configuration>
    <plugins>
        <plugin interceptor="com.selicoco.sango.common.database.paginator.interceptor.ShardTableInterceptor">
        </plugin>
    </plugins>
</configuration>

(2)什么时间截断Mybatis执行流

Mybatis允许我们能够进行切入的点:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

ParameterHandler (getParameterObject, setParameters)

ResultSetHandler (handleResultSets, handleOutputParameters)

StatementHandler (prepare, parameterize, batch, update, query)

因为我是想要通过替换原来SQL中的表名来实现分表,包括查询,新增,删除等操作,所以拦截的合理时机选在StatementHandler中prepare。

执行流在PreparedStatementHandler.instantiateStatement()方法中 return connection.prepareStatement(sql); 最终真正的执行了语句。

所以拦截器的注解内容:

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }) 

(3)应该拦截什么样的对象

并不是所有的表都进行了分表,也不是所有的表都需要拦截处理。所以我们要根据某些配置来确定哪些需要被处理。

这里主要使用注解的方式,设置了对应的参数。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface TableSeg {
    //表名
    public String tableName();
    // 分表方式,取模,如%5:表示取5余数,
    // 按时间,如MONTH:表示按月分表
    // 如果不设置,直接根据shardBy值分表
    public String shardType();
    //根据什么字段分表 ,多个字段用数学表达表示,如a+b   a-b
    public String shardBy();
    // 根据什么字段分表,多个字段用数学表达表示,如a+b   a-b
    public String shardByTable();
}

注解完成后,在mapper上去配置。如果是自定义的查询语句和返回,没有对应的mapper文件,那么在对应的dao 上进行配置就可以了。

@TableSeg(tableName="ebs_date_detail",shardType="MONTH",shardBy="accountMonth",shardByTable="account_month")
public interface EbsDataDetailMapper {}
@Repository
@TableSeg(tableName="ebs_date_detail",shardType="MONTH",shardBy="accountMonth",shardByTable="account_month")
public class EbsDataDetailDao {}

(4)如何获取上下文中传入的参数

首先,如何拿到执行前已经组装好的语句。分两种情况来说,查询和更新。

不说话先看图:

新增数据的时候,我们从boundSql里面的additionalParameters 里面能轻松拿到注解上面 shardBy="accountMonth"所对应的参数值。然后根据参数来生成分表语句,一切顺利。

如此简单,觉得自己好机智。开心的去码后面的代码了,等到单测的时候执行查询,然后就报错啦。只能Debug看看。

没有想到,都是mybatis的动态sql,结果参数方式竟然不同,想来也只能自己去取参数了。参数在哪里?看图

具体的就看后面实现代码吧,反正就是通过两种方式取到我们要的分表字段的参数值,这样才能求得分表表名。

(5)真正实现分表查询语句

拦截器主要的作用是读取配置,根据配置的切分策略和字段,来切分表,然后替换原执行的SQL,从而实现自动切分。

        String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql);
        String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth);
        if (newSql != null) {
            logger.debug(tag, "分表后SQL =====>" + newSql);
            metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
        }

(6)交还被截断的Mybatis执行流

把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis了

        // 传递给下一个拦截器处理
        return invocation.proceed();

4.实现源码

(1)配置文件

见本文: 3.1 Mybatis如何找到我们新增的拦截服务 -- mybatis-config.xml

(2)分表配置注解

分表注解定义、mapper注解配置、DAO注解配置

见本文: 3.3 应该拦截什么样的对象

(3)分表实现

分表具体实现

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class ShardTableInterceptor implements Interceptor {
    private final static Logger logger = LoggerFactory.getLogger(ShardTableInterceptor.class);
    private static final String tag = ShardTableInterceptor.class.getName();
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        String sqlId = mappedStatement.getId();
        String className = sqlId.substring(0, sqlId.lastIndexOf("."));
        Class<?> classObj = Class.forName(className);
        TableSeg tableSeg = classObj.getAnnotation(TableSeg.class);
        if(null == tableSeg){
            //不需要分表,直接传递给下一个拦截器处理
            return invocation.proceed();
        } 
        //根据配置获取分表字段,生成分表SQL
        String accountMonth = genShardByValue(metaStatementHandler, mappedStatement ,tableSeg, boundSql);
        String newSql = boundSql.getSql().replace(tableSeg.tableName(), tableSeg.tableName() + "_" + accountMonth);
        if (newSql != null) {
            logger.debug(tag, "分表后SQL =====>" + newSql);
            metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
        }
        // 传递给下一个拦截器处理
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
    @Override
    public void setProperties(Properties properties) {
        logger.info("scribeDbNames:" + properties.getProperty("scribeDbNames"));
    }
    //根据配置获取分表的表名后缀
    private String genShardByValue(MetaObject metaStatementHandler,MappedStatement mappedStatement, TableSeg tableSeg, BoundSql boundSql) {
        String accountMonth = null;
        Map<String, Object> additionalParameters = (Map<String, Object>) metaStatementHandler.getValue("delegate.boundSql.additionalParameters");
        if (null != additionalParameters.get(tableSeg.shardBy())) {
            accountMonth = boundSql.getAdditionalParameter(tableSeg.shardBy()).toString();
        } else {
            Configuration configuration = mappedStatement.getConfiguration();
            String showSql = showSql(configuration,boundSql);
            accountMonth = getShardByValue(showSql,tableSeg);
        }
        return accountMonth;
    }
    //根据配置获取分表参数值
    public static String getShardByValue(String showSql,TableSeg tableSeg) {
        final String conditionWhere = "where";
        String accountMonth = null ;
        if(StringUtils.isBlank(showSql)){
            return null;
        }else{
            String[] sqlSplit = showSql.toLowerCase().split(conditionWhere);
            if(sqlSplit.length>1 && sqlSplit[1].contains(tableSeg.shardByTable())){
                accountMonth = sqlSplit[1].replace(" ","").split(tableSeg.shardByTable())[1].substring(2,8);
            }
        }
        return accountMonth;
    }
    //组装查询语句参数
    public static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }else{
            return null;
        }
        return sql;
    }
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }
}

以上就是关于“基于MyBatis分表的实现”介绍,大家如果想了解更多相关知识,可以关注一下动力节点的Mybatis-Plus视频教程,里面的课程内容细致全面,有更丰富的知识等着大家去学习,希望对大家能够有所帮助哦。

提交申请后,顾问老师会电话与您沟通安排学习

免费课程推荐 >>
技术文档推荐 >>