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 ); }}
- mybatis插件开发——分表插件
- mybatis generator 的分表插件
- Mybatis 分表插件shardbatis2.0使用
- MyBatis插件开发
- MyBatis初窥:插件开发
- mybatis插件开发
- Mybatis插件开发原理
- MyBatis插件开发
- Java Mybatis Plugin插件实现分表路由规则
- 【插件开发】—— 2 插件入门
- android插件开发——加载插件
- 【插件开发】—— 2 插件入门
- LUA插件——wireshark插件开发
- MyBatis 教程 - MyBatis插件(Plugins)开发
- mybatis框架(七)——插件
- Mybatis之分页插件——PageHelper
- mybatis插件
- Mybatis插件
- HDU3294 Girls' research(Manacher算法)
- 关于jquery mobile checkbox无法获取值一种尝试
- 求101-200以内的质数
- Spring框架入手学习(三)
- Java的位运算符详解实例-与(&)、非(~)、或(|)、异或(^)
- mybatis插件开发——分表插件
- 利用FFTW3生成图像频谱模板
- 理解SetCapture、ReleaseCapture、GetCapture(控制了消息发往哪个窗口,是理解消息的关键)
- 基于matlab/Simulink的参数辨识
- 使用redis实现一个购物车功能
- 接口回调
- 高精度模板
- 方向传感器
- HDU