Mybatis 的 plugin 实现分表

176 阅读2分钟

数据库是 mysql,其中的浏览记录表数据量越来越大,导致按时间范围查询的时候非常慢,打算做按月分表,前端限制一次只能查询一个月内的数据,网上的例子很多,都是创建分表注解,标注了分表注解的 MapperDao 执行分表操作,但是我的项目很老,是直接用 xml 映射文件中的 namespace.id 来查询数据的,所以改用了 plugin 标签中的 property 来配置要按月分表的表名,且只提供了按月分表这一种策略。

Mybatis 的 plugin 是 Mybatis 的一个扩展点,它可以拦截一下接口:

  • Executor(update、query、flushStatements、commit、rollback、getTransaction、close、isClosed)
  • ParameterHandler(getParameterObject、setParameters)
  • ResultSetHandler(handleResultSets、handleOutputParameters)
  • StatementHandler(prepare、parameterize、batch、update、query)

而分表主要是修改需要分表的表名,而按月分表一般是在表名后边添加 _YYYYMM 的后缀,所以拦截 StatementHandler 接口中的 prepare 方法即可。

首先是 mybatis-config.xml 文件配置插件,假设要拦截的表名是 person,如果有多个以逗号分隔。

<plugins>
    <plugin interceptor="com.niuma.config.interceptors.SimpleInterceptor">
        <property name="tables" value="person"/>
    </plugin>
</plugins>

然后是拦截类,里面还有一个知识点是用到了 MetaObject,它是 Mybatis 提供的一个用来操作 Java 对象属性的工具类,可以用它来获取和设置属性值,因为我们拦截的是 StatementHandler,所以从 invocation 入参中获取 StatementHandler 实例,StatementHandler 中有一个 delegate 属性值,它也是 StatementHandler 接口的实现类,然后 delegate 中有一个 BoundSql 对象,它封装了要执行的 sql 语句,BoundSql 里面的 sql 就是要执行的 sql,但是 delegate 和 sql 属性并没有 getter 和 setter 方法,所以我们需要通过反射来获取 sql 的值,如果我们自己写反射来获取 sql 的话,代码类似于下面这样:

StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        
Class<? extends StatementHandler> statementHandlerClass = statementHandler.getClass();
Field declaredField = statementHandlerClass.getDeclaredField("delegate");
declaredField.setAccessible(true);
StatementHandler delegate = (StatementHandler) declaredField.get(statementHandler);
​
Class<?> baseStatementClass = delegate.getClass().getSuperclass();
Field boundSqlField = baseStatementClass.getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
BoundSql boundSql = (BoundSql) boundSqlField.get(delegate);
​
Class<? extends BoundSql> boundSqlClass = boundSql.getClass();
Field sqlField = boundSqlClass.getDeclaredField("sql");
sqlField.setAccessible(true);
String sql = (String) sqlField.get(boundSql);
System.out.println("sql: " + sql);

但是 MetaObject 类帮我们封装了这些样板代码,如下所示:

@Intercepts({
        @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
public class SimpleInterceptor implements Interceptor {
​
    private static final Logger log = LoggerFactory.getLogger(SimpleInterceptor.class);
    private static final String BOUNDSQL = "delegate.boundSql";
    private static final String BOUNDSQL_SQL = "delegate.boundSql.sql";
​
    private String[] oldTableNames = new String[0];
​
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        String oldSql = (String) metaStatementHandler.getValue(BOUNDSQL_SQL);
        log.info("oldSql: " + oldSql);
        String newSql = oldSql;
​
        // 需要分表
        if (oldTableNames.length != 0) {
            String yyyymm = LocalDate.now().format(DateTimeFormatter.ofPattern("YYYYMM"));
            for (int i = 0; i < oldTableNames.length ; i ++) {
                String oldTableName = oldTableNames[i];
                if (oldSql.contains(oldTableName)) {
                    String newTableName = oldTableName + "_" + yyyymm;
                    newSql = newSql.replace(oldTableName, newTableName);
                }
            }
        }
        log.info("newSql: " + newSql);
​
        metaStatementHandler.setValue(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) {
        String tablesStr = properties.getProperty("tables");
        String[] split = tablesStr.split(",");
        oldTableNames = split;
    }
​
}

参考: blog.csdn.net/cckevincyh/… mybatis.org/mybatis-3/z… www.cnblogs.com/selinamee/p… www.jianshu.com/p/d576cf56b… juejin.cn/post/684490…