mybatis插件开发——分表插件

来源:互联网 发布:华工电路二端口ucd=0.1 编辑:程序博客网 时间:2024/05/22 10:39

相关源码已上传至我的github,对应的插件代码在src/main/java/net/dwade/plugins/mybatis目录
https://github.com/huangxfchn/dwade/tree/master/framework-plugins

项目背景

  项目中使用oracle数据库 + mybatis框架,由于数据量较大,需要使用日表。而我们又不希望对mybatis的mapper文件做较大的改动,比如在SQL中添加日表后续,通过变量符的方式操作日表,因为这样的话就不能使用mybatis预编译的SQL影响性能,而且将来如果使用分布式数据库的话,意味着将来还要改动mapper文件。虽然当当有sharding-jdbc框架,但是不支持oracle,因此,自己开发了简单的mybatis插件,通过sql改写的方式操作日表。

特性

  • 支持oracle、mysql
  • 支持pagehelper分页插件
  • 简单实用,出于项目实际情况考虑,该插件目前只支持编码的方式指定要操作的日表,不支持根据某个字段进行拆表

quick start

添加插件支持

  如果项目中用到了pagehelper分页插件,需要将该插件放到分表插件前面,因为mybatis对拦截器进行了处理,顺序靠后的的拦截器越先执行,下面是InterceptorChain中的pluginAll方法,返回的是最后被代理的对象

public Object pluginAll(Object target) {    for (Interceptor interceptor : interceptors) {      target = interceptor.plugin(target);    }    return target;}

  下面是mybatis的xml配置,添加了分表插件

     <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">        <property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />        <property name="jdbcUrl" value="jdbc:oracle:thin:@localhost:1521:dwade" />        <property name="user" value="******" />        <property name="password" value="******" />        <property name="minPoolSize" value="20" />        <property name="maxPoolSize" value="200" />        <property name="initialPoolSize" value="20" />        <property name="acquireIncrement" value="20" />        <property name="checkoutTimeout" value="10000" />        <property name="idleConnectionTestPeriod" value="600" />        <property name="maxIdleTime" value="600" />    </bean>    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">        <property name="dataSource" ref="dataSource" />        <property name="configuration" ref="mybatisConfig" />        <property name="plugins">            <array>                <!-- 具体参数请查看wiki: https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md -->                <bean class="com.github.pagehelper.PageInterceptor">                    <property name="properties">                        <value>                            helperDialect=oracle                            reasonable=true                            supportMethodsArguments=true                        </value>                    </property>                </bean>                <!-- 日表插件 -->                <bean id="tableSegInterceptor"                    class="net.dwade.plugins.mybatis.ShardingInterceptor">                </bean>            </array>        </property>        <property name="mapperLocations" value="classpath*:net/dwade/payment/dao/**/*Mapper.xml" />        <property name="typeAliasesPackage" value="net.dwade.payment.dao.*.model" />    </bean>    <bean id="mybatisConfig" class="org.apache.ibatis.session.Configuration">        <property name="logImpl" value="org.apache.ibatis.logging.log4j2.Log4j2Impl" />    </bean>    <bean id="paymentMapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">        <property name="basePackage" value="net.dwade.payment.dao" />        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />    </bean>

编码

  使用方式和分页插件相同,通过将日表条件绑定到ThreadLocal中,简单的调用ShardingHolder的api即可。在项目中,我们会在主键中体现日期,很多情况下是知道数据存放在哪张表里面的。
* 插入日表
假设我们需要将数据插入到T_USER_20170603这张表中,如下面的代码所示。

User user = new User();user.setUserId( "12341234" );user.setCreateTime( new Date() );user.setUserName( "15567899876" );user.setEmail( "xxx@163.com" );ShardingHolder.set( "20170603" );userDao.insert( user );
  • 日表查询
ShardingHolder.set( "20170603" );userDao.selectByPrimaryKey( "2017060312341234" );
  • 多表、分页关联查询
ShardingHolder.set( "20170601", "20170602" );PageHelper.startPage( xxx, xxx );userDao.selectXXX( param1, param2 );

注意事项

  • 多个表关联查询,需要按照表名在sql出现的顺序,依次设置,如果涉及到其中的某个表为全表,设为null即可,eg:ShardingHolder.set( “20170712”, null, “20170712” );
  • 为了保证分页条件的准确性,调用ShardingHolder的set方法之后必须紧接着调用dao方法,错误示例:
ShardingHolder.set( "20170601", "20170602" );// do something 1// do something 2payUserDao.selectXXX( "12341234" );

源码说明

该插件的原理非常简单,通过拦截StatementHalder接口,对sql进行解析、改写,mybatis使用改写的SQL执行,最终获得我们想要的结果。

mybatis拦截器基本原理

  mybatis允许我们对四大接口的方法进行拦截,所以要先了解Mybatis的四大接口对象Executor, StatementHandler, ResultSetHandler, ParameterHandler各自的作用,分别代表执行器,SQL语法处理、结果集处理、参数处理。关于更详细的介绍,请参考《mybatis插件原理》http://www.jianshu.com/p/7c7b8c2c985d

核心代码

  该分表插件拦截了StatementHandler的prepare方法,用于对SQL进行改写,如Signature注解所示。其中,args代表方法的参数,因为只有指定了接口、方法名、参数,mybatis才能确定需要拦截哪个方法,值得一提的是,低版本的mybatis的StatementHandler接口中的prepare方法只有一个参数(如3.2.8版本只有一个参数,而我的项目里面用的是3.4.2),因此注解中args指定了Connection和Integer。此外,还拦截了Executor的query和update方法,主要的作用是为了支持pagehelper分页插件,因为在分页插件中,先是调用了Executor接口执行了一次count (1)的SQL语句,然后才是执行查询数据的SQL。这样一来,执行count (1)的SQL会调用我们拦截器的interceptor方法,如果不做额外的处理,分表条件便会清除,所以我们还拦截了Executor接口,并且在其执行完毕之后才清理ThreadLocal中的分表条件,如代码中的54行所示。
  对于SQL解析,我们使用的是开源的jsqlparse,简单的封装了下,只获取SQL中的表结构,具体请参考net.dwade.plugins.mybatis.parser.JSqlParserFactory.java

/*** mybatis分表拦截器,<em>如果同时和分页插件一起使用,需要配置在分页插件之后</em><br/>* <p>mybatis拦截器的执行顺序:Executor-->StatementHandler(ParameterHandler)-->ResultSetHandler</p>* <p>* 该分表插件拦截了Executor的query和update方法,Executor执行完毕之后将分表条件清除,* 否则会把全表操作误认为分表操作,此外,由于分页插件也拦截了Executor的query方法,因此和分页插件同时* 使用时需要将分页插件配置在该分表插件前面,因为InterceptorChain.pluginAll(Object target)返回的* 是最后一个拦截器的代理,因此会先执行最后一个拦截器的intercept方法* </p>* <p>为了避免对非日表的操作带来影响,该插件在Executor执行完毕的时候清除ThreadLocal中的分表条件。</p>* <strong>为什么不在获取分表条件之后就清理ThreadLocal中的分表条件?</strong>* 因为分页插件拦截的是Executor,并且自己创建了BoundSql进行调用,先是count操作,再是查询数据,如果拦截的是获取之后就清除,* 那么只会对count操作的分表起作用,对分页插件的数据查询操作是不会起作用的* @author huangxf* @date 2017年6月29日*/@Intercepts({     @Signature(type = StatementHandler.class, method = "prepare", args = { java.sql.Connection.class, Integer.class }),    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})public class ShardingInterceptor implements Interceptor {    private Logger logger = LoggerFactory.getLogger( this.getClass() );    private final SqlParserFactory parserFactory = new JSqlParserFactory();    private final Field boundSqlField;    public final String DEFAULT_SEPARATOR = "_";    /**    * 分表的连接符,T_ORDER_20160629,其中T_ORDER为逻辑表名,_代表separator    */    private String separator = DEFAULT_SEPARATOR;    public ShardingInterceptor() {        try {            boundSqlField = BoundSql.class.getDeclaredField("sql");            boundSqlField.setAccessible(true);        } catch (Exception e) {            throw new RuntimeException( e );        }    }    @Override    public Object intercept(Invocation invocation) throws Throwable {        //---------------------------------------------------------------        // 对于分页插件而言,它自己调用了count的SQL查询,最后还是会进入intercept方法,只不过        // invocation的target是StatementHandler了,而不再是Executor        //---------------------------------------------------------------        if ( invocation.getTarget() instanceof Executor ) {            try {                return invocation.proceed();            } finally {                ShardingHolder.remove();            }        }        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();        BoundSql boundSql = statementHandler.getBoundSql();        // 判断是否设置分表条件,ThreadLocal中的变量在commit或者rollback的时候清除        final String[] actualTables = ShardingHolder.get();        if ( ArrayUtils.isEmpty( actualTables ) ) {            return invocation.proceed();        }        // 进行SQL解析        SqlParser sqlParser = parserFactory.createParser( boundSql.getSql() );        List<Table> tables = sqlParser.getTables();        if ( tables.isEmpty() ) {            return invocation.proceed();        }        // 如果设置的表名数量和实际不一致,抛出SQL异常        if ( tables.size() != actualTables.length ) {            throw new SQLException( "Table sharding exception, tables in sql not equals to actual settings" );        }        // 设置实际的表名        for ( int index = 0; index < tables.size(); index++ ) {            if ( StringUtils.isEmpty( actualTables[ index ] ) ) {                continue;            }            Table table = tables.get( index );            String targetName = table.getName() + separator + actualTables[ index ];            logger.info( "Sharding table, {}-->{}", table, targetName );            table.setName( targetName );        }        // 修改实际的SQL        String targetSQL = sqlParser.toSQL();        boundSqlField.set( boundSql, targetSQL );        return invocation.proceed();    }    @Override    public Object plugin(Object target) {        return Plugin.wrap( target, this );    }    @Override    public void setProperties(Properties properties) {        this.separator = properties.getProperty( "separator", DEFAULT_SEPARATOR );    }}
原创粉丝点击