基于mybatis拦截器分表实现
1、拦截器简介
MyBatis提供了一种插件(plugin)的功能,但其实这是拦截器功能。基于这个拦截器我们可以选择在这些被拦截的方法执行前后加上某些逻辑或者在执行这些被拦截的方法时执行自己的逻辑。
这点跟spring的拦截器是基本一致的。它的设计初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。
拦截器的使用中,分页插件应该是使用得最多的了。分表的实现也差不多类似。
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语法构建的处理
我们看到了可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。
这4各方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。undefine
3、Interceptor接口
了解到了拦截器能够拦截的方法调用,就需要看看拦截接口是如何实现的了。
package org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); } |
接口中一共定义有三个方法,intercept、plugin 、setProperties。
- intercept方法就是要进行拦截的时候要执行的方法。
- setProperties方法是用于在Mybatis配置文件中指定一些属性的。
- plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。
分表实现
1、大体思路
基于业务来看,我想要 按月 分表,因此数据库表里增加了一个string类型字段 account_month 来记录月份,分表字段就使用account_month。
分表表名:表名_年月 例如明细表:ebs_date_detail_201607。
分表是一月一张表,分表的建立就是默认建立了12个分表,如果超出了,后续再手工添加吧。也可以写个脚本每月底创建下一个月的表,但是我觉得没啥必要。就算哪天忘记添加了,代码逻辑的异常处理流程里面也能够保证我的数据不丢失,启动一下异常数据处理也就妥妥的了。
在sql语句里面会要求带上分表字段,通过分表字段计算得到分表的表名,然后替换掉原来的sql,直接将数据路由到指定的分表就行了。
听起来好像很简单的样子,那么就这么出发吧。
2、问题目录
分表开始之前的问题:
- Mybatis如何找到我们新增的拦截服务。
- 自定义的拦截服务应该在什么时间拦截查询动作。即什么时间截断Mybatis执行流。
- 自定义的拦截服务应该拦截什么样的对象。不能拦截什么样的对象。
- 自定义的拦截服务拦截的对象应该具有什么动作才能被拦截。
- 自定义的拦截服务如何获取上下文中传入的参数信息。
- 如何把简单查询,神不知鬼不觉的,无侵入性的替换为分表查询语句。
- 最后,拦截器应该如何交还被截断的Mybatis执行流。
带着这些问题,我们来看看我们自定义的拦截服务是如何实现的。
3、逐步实现
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>
3.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.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 {}
3.4 如何获取上下文中传入的参数
关于这个问题,我觉得我有很大的发言权,真的是摸着石头过来的。
天真的以为,只要拿到要执行之前已经组装好的语句,然后用我的分表表名替换一下原表名就可以了。当然其实也差不多就是这样子的。不过实际情况还是有点坎坷的。
首先,如何拿到执行前已经组装好的语句。分两种情况来说,查询和更新。
不说话先看图:
新增数据的时候,我们从boundSql里面的additionalParameters 里面能轻松拿到注解上面 shardBy="accountMonth"所对应的参数值。然后根据参数来生成分表语句,一切顺利。
如此简单,觉得自己好机智。开心的去码后面的代码了,等到单测的时候执行查询,然后就报错啦。只能Debug看看。
没有想到,都是mybatis的动态sql,结果参数方式竟然不同,想来也只能自己去取参数了。参数在哪里?看图
具体的就看后面实现代码吧,反正就是通过两种方式取到我们要的分表字段的参数值,这样才能求得分表表名。
3.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);
}
3.6 交还被截断的Mybatis执行流
把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis了
// 传递给下一个拦截器处理
return invocation.proceed();
4 实现源码
4.1 配置文件
见本文: 3.1 Mybatis如何找到我们新增的拦截服务 -- mybatis-config.xml
4.2 分表配置注解
分表注解定义、mapper注解配置、DAO注解配置
见本文: 3.3 应该拦截什么样的对象
4.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拦截器实现
数据权限管理中心 由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手 需求场景 第一种场景:行级数据处理 原sql: select ...
- 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表
利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...
- 基于Spring和Mybatis拦截器实现数据库操作读写分离
首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...
- Mybatis拦截器介绍
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...
- Mybatis拦截器介绍及分页插件
1.1 目录 1.1 目录 1.2 前言 1.3 Interceptor接口 1.4 注册拦截器 1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页 1.2 前言 拦截器的一 ...
- Mybatis拦截器实现原理深度分析
1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...
- Mybatis拦截器 mysql load data local 内存流处理
Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...
- Mybatis拦截器实现分页
本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...
- 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离
前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...
随机推荐
- 【LeetCode】随机化算法 random(共6题)
[384]Shuffle an Array(2019年3月12日) Shuffle a set of numbers without duplicates. 实现一个类,里面有两个 api,struc ...
- sass-RGB颜色函数-RGB()颜色函数
在 Sass 的官方文档中,列出了 Sass 的颜色函数清单,从大的方面主要分为 RGB , HSL 和 Opacity 三大函数,当然其还包括一些其他的颜色函数,比如说 adjust-color 和 ...
- HttpRunnerManager安装部署(centos7)
一.安装python3环境 参考 二.安装依赖环境 根据根目录requirements.txt文件安装依赖,可以使用pip安装 #pip3 install -r requirements.txt 会遇 ...
- JVM内存组成
JVM的内存区域模型 1.方法区 也称永久代.非堆. 用于存储虚拟机加载的类信息.常量.静态变量,是各个线程共享的内存区域. 默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize和 ...
- Linux中检查内存使用情况的命令
Linux操作系统包含大量工具,所有这些工具都可以帮助您管理系统.从简单的文件和目录工具到非常复杂的安全命令,在Linux上没有太多不能做的事情.而且,虽然普通桌面用户可能不需要在命令行熟悉这些工具, ...
- tmux使用——2019年11月20日16:40:15
1.tmux 命令行的典型使用方式是,打开一个终端窗口(terminal window,以下简称"窗口"),在里面输入命令.用户与计算机的这种临时的交互,称为一次"会话& ...
- Prometheus指标采集常用配置
一.node-exporter配置textfile收集器 textfile收集器作用: 运行暴露自定义指标.例如,需要在某个被监控节点上添加一个地理位置的指标. node-exporter会自动启动t ...
- SQL Server 创建表
SQL Server 创建表 我们在上一节中完成了数据库的创建,在本节,我们要往这个新的数据库中加入点数据,要想将数据添加到数据库,我们就必须在数据库中添加一个表,接下来来看看具体的操作. 我们的数据 ...
- iis7反向代理
很多站长通常在Linux系统下使用nginx作为前端server,通过反向代理间接访问其他webserver.那么如果用户安装的是Windows系统的话,又改如何实现反向代理的设置呢?搜索引擎大全 下 ...
- Java和Tomcat的关系 Java如何发布web服务
https://blog.csdn.net/qq_31301961/article/details/80732669 除了Tomcat还有WebLogic 大型分布式的 如何部署映射 Tomcat使用 ...