MyBatis自定义插件实现按日期分表功能

Posted 敲代码的小小酥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis自定义插件实现按日期分表功能相关的知识,希望对你有一定的参考价值。

一、项目背景

在项目中,某个业务数据,每天都产生几百万条数据,所以选择对这个表按日期分表,每天的数据,insert进当天的表中。起初的解决方案有两种:
1.insert语句动态定义表名,进行数据的存入操作。
2.使用mycat中间件进行数据负载操作。
因为项目中大数据量的业务不多,只有个别的数据量大,且也还没有达到分库的体量,只是进行分表,所以使用mycat解决方案有点儿小题大做,所以最开始使用的是方案1进行的操作。

二、缺点

当项目运行一段时间后,发现另一个业务数据,每天产生的数据量也很大,也需要按日期进行分表,于是,需要在这个模块的insert语句中,把表名改成动态的。所以,这种方式的缺点就是复用性很差,当再有业务数据需要分表时,还需要重新写一遍代码。

三、解决

学习了mybatis的插件原理后,发现这个问题,可以自定义mybatis插件进行解决。
思路:
首先,需要自定义一个注解,用来标识需要进行分表的实体类。然后,在mybatis执行sql的时候,根据注解标识,把有注解的类,动态修改其sql语句,改成当天日期的表名。

下面,我们来看一下具体实现:
首先,自定义注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PluginCS 
    String tablename();//表名前缀,在这个参数的基础上动态加当天的日期,作为当天的表名

接下来,就该分析,自定义插件,是要加强MyBatis的哪个组件了。因为要加强的是insert方法,所以,只考虑Executor组件和StatementHandler组件。因为只有这两个组件,涉及到了插入的方法。
Executor组件是一个总管的角色,其最终调用的是StatementHandler组件,执行的插入操作,那么这俩到底加强谁呢,我们先研究明白其执行流程,再做决定。
通过debug,追其源码:
代码走到ReuseExecutor的doUpate方法,

 @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException 
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.update(stmt);
  

重点看prepareStatement方法:

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException 
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    if (hasStatementFor(sql)) 
      stmt = getStatement(sql);
      applyTransactionTimeout(stmt);
     else 
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);
    
    handler.parameterize(stmt);
    return stmt;
  

可以看到,在prepareStatement,从BoundSql中获取到sql,然后传给了Statement对象。
然后看ReuseExecutor的doUpate方法的handler.update(stmt)方法:

 public int update(Statement statement) throws SQLException 
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  

这是PreparedStatementHandler的update实现方法,可以看到,这个方法里,就是单纯的执行JDBC的statement操作了,也就是说,在这里,sql已经定格了。
所以,现在的思路是,在StatementHandler的update方法里,修改Statement的sql语句。经过研究发现,Statement是一个接口,其实现类有很多,想获取到其sql熟悉,很难,放弃了这种想法。
那么只能在调用update方法之前,修改sql了。上面源码分析到,先是调用了prepareStatement方法,生成了Statement对象,然后在update方法里执行的Statement方法。所以,我们可以在prepareStatement方法中,来修改sql。通过研究发现,

stmt = handler.prepare(connection, transaction.getTimeout());

处生成了statement对象,且prepare方法也是StatementHandler的方法,所以,定位到对prepare方法进行加强。
所以,现在的思路是加强prepare方法,然后修改BoundSql中的sql,添加动态表名。代码如下:

@Component
@Intercepts(
        @Signature(type = StatementHandler.class, method = "prepare",
                args = Connection.class, Integer.class)
     )
public class MyPlugin implements Interceptor 
    @Override
    public Object intercept(Invocation invocation) throws Throwable 
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql bsinstance = statementHandler.getBoundSql();
        Object param=bsinstance.getParameterObject();
        if(param==null)
            return invocation.proceed();
        
        PluginCS annotation = param.getClass().getAnnotation(PluginCS.class);//分表自定义注解
        if(annotation!=null)//需要路由
            String tablename=annotation.tablename();
            //获取sql
         //   BoundSql boundSql = statement.getBoundSql(param);
            Field field = getField(bsinstance, "sql");
           String sql= field.get(bsinstance).toString();
           if(!sql.contains("insert"))//只对insert语句进行处理
               return invocation.proceed();
           
           sql =sql.replace(tablename,"_cs");//模拟分表,动态添加表名
           field.set(bsinstance,sql);
        
        return invocation.proceed();//修改完sql语句后,执行原方法
    

    
    @Override
    public Object plugin(Object target) 
        return Plugin.wrap(target,this);
    

    @Override
    public void setProperties(Properties properties) 

    
private Field getField(Object o, String name) 
        Field field = ReflectionUtils.findField(o.getClass(), name);
        ReflectionUtils.makeAccessible(field);
        return field;
    
    

然后,进行插件的注册,在mybatis配置文件中,注册插件:

<plugins>
		<plugin interceptor="xxx.xxx.plugin.MyPlugin"/>
	</plugins>

至此,分表插件开发完成,可以在需要分表的实体类中,加上自定义注解,就可以实现自动分表功能了。

以上是关于MyBatis自定义插件实现按日期分表功能的主要内容,如果未能解决你的问题,请参考以下文章

mybatis-plus小技能: 分表策略(按年分表和按月分表)

MyBatis自定义插件机制分析(源码级剖析)

.netcore分库分表的问题

springboot~mybatis-plus的DynamicTableNameInnerInterceptor实现分表

Mybatis自定义拦截器与插件开发

Mybatis -- MyBatis核心配置文件深入: typeHandlers标签(自定义类型转换器)plugins标签(插件标签:扩展mybatis功能 分页助手)