更新时间:2022-03-23 10:38:05 来源:极悦 浏览2742次
基于业务来看,想要按月分表,因此数据库表里增加了一个string类型字段 account_month 来记录月份,分表字段就使用account_month。
分表表名:表名_年月 例如明细表:ebs_date_detail_201607。
分表是一月一张表,分表的建立就是默认建立了12个分表,如果超出了,后续再手工添加吧。也可以写个脚本每月底创建下一个月的表,但是觉得没啥必要。就算哪天忘记添加了,代码逻辑的异常处理流程里面也能够保证我的数据不丢失,启动一下异常数据处理也就妥妥的了。
在sql语言里面会要求带上分表字段,通过分表字段计算得到分表的表名,然后替换掉原来的sql,直接将数据路由到指定的分表就行了。
听起来好像很简单的样子,那么就这么出发吧。
分表开始之前的问题:
Mybatis如何找到我们新增的拦截服务。
自定义的拦截服务应该在什么时间拦截查询动作。即什么时间截断Mybatis执行流。
自定义的拦截服务应该拦截什么样的对象。不能拦截什么样的对象。
自定义的拦截服务拦截的对象应该具有什么动作才能被拦截。
自定义的拦截服务如何获取上下文中传入的参数信息。
如何把简单查询,神不知鬼不觉的,无侵入性的替换为分表查询语句。
最后,拦截器应该如何交还被截断的Mybatis执行流。
带着这些问题,我们来看看我们自定义的拦截服务是如何实现的。
(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>
(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)应该拦截什么样的对象
并不是所有的表都进行了分表,也不是所有的表都需要拦截处理。所以我们要根据某些配置来确定哪些需要被处理。
这里主要使用注解的方式,设置了对应的参数。
@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 {}
(4)如何获取上下文中传入的参数
首先,如何拿到执行前已经组装好的语句。分两种情况来说,查询和更新。
不说话先看图:
新增数据的时候,我们从boundSql里面的additionalParameters 里面能轻松拿到注解上面 shardBy="accountMonth"所对应的参数值。然后根据参数来生成分表语句,一切顺利。
如此简单,觉得自己好机智。开心的去码后面的代码了,等到单测的时候执行查询,然后就报错啦。只能Debug看看。
没有想到,都是mybatis的动态sql,结果参数方式竟然不同,想来也只能自己去取参数了。参数在哪里?看图
具体的就看后面实现代码吧,反正就是通过两种方式取到我们要的分表字段的参数值,这样才能求得分表表名。
(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);
}
(6)交还被截断的Mybatis执行流
把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis了
// 传递给下一个拦截器处理
return invocation.proceed();
(1)配置文件
见本文: 3.1 Mybatis如何找到我们新增的拦截服务 -- mybatis-config.xml
(2)分表配置注解
分表注解定义、mapper注解配置、DAO注解配置
见本文: 3.3 应该拦截什么样的对象
(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-Plus视频教程,里面的课程内容细致全面,有更丰富的知识等着大家去学习,希望对大家能够有所帮助哦。
0基础 0学费 15天面授
Java就业班有基础 直达就业
业余时间 高薪转行
Java在职加薪班工作1~3年,加薪神器
工作3~5年,晋升架构
提交申请后,顾问老师会电话与您沟通安排学习