Spring Boot Mybatis实现分表
Posted tuacy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot Mybatis实现分表相关的知识,希望对你有一定的参考价值。
开发过程中可能会碰到分表的场景,数据库的数据量相当大的时候可能需要按天分表或者按月分表啥的(分表策略)。接下来就教大家用最简单的方式实现这一需求。
咱们接下来主要实现以下两个大功能:
- 自动建表,当表不存在的时候自动把表创建出来。
- 自动分表,根据操作数据库的某个参数进行分表。
自动建表,自动分表核心思想在Mybatis拦截器的使用。强烈建议大家先去了解下Mybatis拦截器的使用(之前也写过一遍关于Mybatis拦截器的使用的文章,有兴趣的可以看下 https://blog.csdn.net/wuyuxing24/article/details/89343951 )。
根据实际情况我们做如下规定:
- 每个需要分表的表都有一个基础表名。比如按月分表之后的表名为“XXX-201909”,那么我们认为"XXX"就是基础表名。所有的sql语句里面还是用基础表名,我们会在自定义Mybatis拦截器里面找到基础表名替换成分表表名。
- 分表的依据来源于操作数据库的参数当中的一个。我们会通过参数注解(TableShardParam)来标识哪个操作作为分表依据。
- 每个分表需要自己指定分表策略(ITableNameStrategy),针对每个分表我们需要自己去实现自己的分表策略,自己实现ITableNameStrategy接口。
一 自动建表准备
我们考虑到大部分分表的情况下,都希望在代码里面能够自动建表。操作表之前判断表是否存在,如果表不存在则自动帮我们把表建出来。
关于自动建表,结合实际情况,我们认为建表是和每个表对应的实体类绑定在一起的。所以我们会有一个建表相关的TableCreate注解,TableCreate注解是添加在每个表对应的实体类上的。TableCreate注解的元数据会告诉我们当前实体类对应表的基础表名,已经去哪里找到相关的建表语句。
TableCreate注解需要添加在表对应的实体类上
/**
* @name: TableCreate
* @author: tuacy.
* @date: 2019/8/29.
* @version: 1.0
* @Description: TableCreate注解用于告诉我们怎么找到建表语句(如果表不存在的情况下, 我们程序里面自己去建表)
* <p>
* tableName -- 基础表名
* autoCreateTableMapperClass -- mapper class对应的名字
* autoCreateTableMapperMethodName -- mapper class 里面对应的方法
* <p>
* 最终我们会去mapper class里面找到对应的对应的方法,最终拿到建表语句
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TableCreate
/**
* table的基础表名
*/
String tableName();
/**
* Mapper类,不能为空
*/
Class<?> autoCreateTableMapperClass();
/**
* Mapper文件里面的函数名字(创建表对应的函数)
*/
String autoCreateTableMapperMethodName();
为了方便Mybatis拦截器里面自动建表的使用,每个表对应的建表信息我们用TableCreateConfig类做一个简单的分装。TableCreateConfig会告诉我们基础表名,以及我们需要的建表语句在哪个Mapper类哪个方法里面。
/**
* @name: TableCreateConfig
* @author: tuacy.
* @date: 2019/8/29.
* @version: 1.0
* @Description: 自动建表相关的一些配置信息
* 在拦截器里面我们会根据autoCreateTableMapperClass类的autoCreateTableMapperMethodName方法找到建表语句
*/
@Data
@Accessors(chain = true)
public class TableCreateConfig
/**
* 表名
*/
private String tableName;
/**
* 自动建表Mapper类
*/
private Class<?> autoCreateTableMapperClass;
/**
* 自动建表Mapper中的方法
*/
private String autoCreateTableMapperMethodName;
在Spring Boot启动的时候,我们会表实体类对应的包下面读取所有添加了TableCreate注解的相关信息,把读取的信息封装到TableCreateConfig类里面,并且保存在单例类TableCreateManager里面。这一部分内容大家可以看下我给出的源码里面TableCreateScan,TableCreateScanRegister类里面逻辑。
简单总结下关于自动建表我们做了那些准备工作。我们会在Spring Boot启动的过程中去读取所有添加了TableCreate注解的实体类。把读取到的信息保存在单例类TableCreateManager里面。单例TableCreateManager里面会维护一个Map:key就是每个需要建表的基础表名,value则是建表相关的信息。建表相关的信息会和Mapper里面的某个方法关联起来。具体可以看下下面Mybatis拦截器的具体实现。
二 自动分表准备
分表,我们需要两个东西:分表策略、分表依据。
2.1 分表策略
分表策略,我们定义一个分表接口,让每个分表去实现直接的分表策略。分表策略我们给两个参数,一个是基础表名,一个是分表依据。
/**
* @name: ITableNameStrategy
* @author: tuacy.
* @date: 2019/8/13.
* @version: 1.0
* @Description: 分表对应的策略
*/
public interface ITableNameStrategy
/**
* 表名字
*
* @param oldTableName 表基本名字
* @param dependFieldValue 根据该字段确定表名(Mapper方法的某个参数对应的值)
* @return 表名
*/
String tableName(String oldTableName, String dependFieldValue);
分表策略的配置,我们把他们放在操作数据库的方法上。在TablePrepare注解里面指定。TablePrepare注解也用于标识是否进入我们自定义的Mybatis拦截器里面去。
/**
* @name: TablePrepare
* @author: tuacy.
* @date: 2019/8/29.
* @version: 1.0
* @Description:
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TablePrepare
/**
* 启用自动建表,当表不存在的时候,是否创建表
*/
boolean enableAutoCreateTable() default true;
/**
* 启用分表
*/
boolean enableTableShard() default false;
/**
* 指定表,如果设置该值,则只会处理指定的表,没有则会处理sql中的所有表
* 如果自己设置了基础表的名字,那么我们处理建表和分表的时候只会处理这些指定的表.
* 如果没有设置基础表的时候,我们会自动去sql语句里面解析出所有的表名.做相应的建表和分表的逻辑
*/
String[] appointTable() default ;
/**
* 表名策略,通过某种规则得到表名
*/
Class<? extends ITableNameStrategy> strategy() default TableNameStrategyVoid.class;
2.2 分表依据
结合实际情况,我们认为分表的依据都是来源于操作数据的某个参数(也可能是某个参数的某个字段)。那这里就有问题了,操作数据库有的时候有多个参数,哪个参数作为分表依据呢。我们定义一个参数注解TableShardParam。哪个参数添加了该注解,我们就认为这个参数是分表依据(目前只支持一个参数作为依据)。我们会在我们自定义的Mybatis拦截器里面找到添加了TableShardParam注解的参数对应的值。
为了应对多种情况。TableShardParam支持以下几种情况(这部分具体的实现,需要仔细看下下面自定义Mybatis拦截器里面这部分的具体实现)。大家可以根据自己的实际情况做相应的修改。
- TableShardParam添加在java基础类型上,比如int,long等,我们会把基础类型转换为String,最终传递给分表策略。
- TableShardParam添加在对象类型上,我们可以找到对象的某个属性(反射)对应的值,最终传递给分表策略。
- TableShardParam添加在List上,我们会找到List对象的一个元素,如果List里面的元素是java基础类型,直接获取到第一个元素对应的值,如果List里面的元素是对象,则获取到对象某个属性对应的值。在最终把他们传递给分表策略。
/**
* @name: TableShardParam
* @author: tuacy.
* @date: 2019/8/30.
* @version: 1.0
* @Description: 添加在参数上的注解, 一定要配置mybatis 的Param注解使用
* <p>
* 我们是这样考虑的,分表核心在于确定表的名字,表的名字怎么来,肯定是通过某个参数来获取到.
* 所以,这里我们设计TableShardParam注解,用于添加在参数上,让我们方便的获取到通过那个参数来获取表名
* 1. int insertItem(@TableShardParam(dependFieldName = "recTime") @Param("item") AccHour item);
* -- 分表依据对应AccHour对象recTime属性对应的值
* 2. int insertList(@TableShardParam(dependFieldName = "recTime") @Param("list") List<AccHour> list);
* -- 分表依据对应list的第一个对象recTime属性对应的值
* 3. List<AccHour> selectLIst(@TableShardParam() @Param("startTime") Long startTIme, @Param("endTime") Long endTime);
* -- 分表依据对应endTime对应的值
*/
@Documented
@Inherited
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShardParam
@AliasFor("dependFieldName")
String value() default "";
/**
* dependFieldName取到我们需要的获取表名的依据
*/
@AliasFor("value")
String dependFieldName() default "";
三 自定义Mybatis拦截器
关于自定义Mybatis拦截器的具体实现,我这里就直接贴代码了。肯定有些地方是还没有考虑到的,大家需要根据自己的需求做修改,重点关注以下几个部分:
- 拦截器里面我们是怎么拿到相应的建表语句的。
- 拦截器里面我们是怎么去执行建表语句的。
- 拦截器里面我们是怎么拿到分表依据的,里面考虑了多种情况。
- 每个sql语句,我们是怎么解析出表名的。怎么把我们把我们分表表名替换进去的。
/**
* @name: TableShardInterceptor
* @author: tuacy.
* @date: 2019/8/13.
* @version: 1.0
* @Description: 自动建表 + 分表 拦截器的实现
*/
@Intercepts(
@Signature(
type = StatementHandler.class,
method = "prepare",
args = Connection.class, Integer.class
)
)
public class TableShardInterceptor implements Interceptor
/**
* sql语句里面去获取表名的依据(主要,全部是小写的)
* 说白了就是哪些字符串后面会跟上表名
*/
private final static String[] SQL_TABLE_NAME_FLAG_PREFIX = "from", "join", "update", "insert into";
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
@Override
public Object intercept(Invocation invocation) throws Throwable
if (!(invocation.getTarget() instanceof RoutingStatementHandler))
return invocation.proceed();
try
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
// MetaObject是mybatis里面提供的一个工具类,类似反射的效果
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");//获取sql语句
String originSql = boundSql.getSql();
if (StringUtils.isEmpty(originSql))
return invocation.proceed();
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
// 判断方法上是否添加了 TableShardAnnotation 注解,因为只有添加了TableShard注解的方法我们才会去做分表处理
TablePrepare tablePrepare = getTableShardAnnotation(mappedStatement);
// 没有加@TablePrepare注解则不填家我们自定义的逻辑
if (tablePrepare == null)
return invocation.proceed();
boolean enableAutoCreateTable = tablePrepare.enableAutoCreateTable(); // 表不存在的是哈,事发创建
boolean enableTableShard = tablePrepare.enableTableShard(); // 事发进行分表逻辑处理
// 自动建表和分表是否开启,都没有则退出往下走
if (!enableAutoCreateTable && !enableTableShard)
invocation.proceed();
// 获取到需要处理的表名
String[] appointTable = tablePrepare.appointTable();
if (appointTable.length == 0)
List<String> tableNameList = getTableNamesFromSql(originSql);
if (tableNameList == null || tableNameList.isEmpty())
return invocation.proceed();
else
// 去掉前后空格和/n
tableNameList = tableNameList.stream().map(item ->
if (item == null)
return null;
return item.trim().replaceAll("[\\r\\n]", "");
).collect(Collectors.toList());
appointTable = new String[tableNameList.size()];
tableNameList.toArray(appointTable);
// 获取分表表名处理策略
Class<? extends ITableNameStrategy> strategyClass = tablePrepare.strategy();
ITableNameStrategy tableStrategy = null;
if (!strategyClass.equals(TableNameStrategyVoid.class))
tableStrategy = strategyClass.newInstance();
// 分表处理的时候,我们一般是依赖参数里面的某个值来进行的.这里用于获取到参数对应的值.
String dependValue = getDependFieldValue(tablePrepare, metaStatementHandler, mappedStatement);
// 自动建表处理逻辑(表不存在的时候,我们会建表)
if (tablePrepare.enableAutoCreateTable())
SqlSessionTemplate template = SpringContextHolder.getBean(SqlSessionTemplate.class);
for (String tableName : appointTable)
TableCreateConfig classConfig = TableCreateManager.INSTANCE.getClassConfig(tableName);
if (classConfig == null)
// 没有找到建表语句则跳过
continue;
String createSqlMethodPath = classConfig.getAutoCreateTableMapperClass().getName() + "." + classConfig.getAutoCreateTableMapperMethodName();
String sql = template.getConfiguration().getMappedStatement(createSqlMethodPath).getBoundSql("delegate.boundSql").getSql();
if (StringUtils.isEmpty(sql))
// 建表sql为空时不理,直接跳过
continue;
if (!StringUtils.isEmpty(dependValue) && strategyClass != TableNameStrategyVoid.class)
sql = sql.replace(tableName, tableStrategy.tableName(tableName, dependValue));
Connection conn = (Connection) invocation.getArgs()[0];
boolean preAutoCommitState = conn.getAutoCommit();
conn.setAutoCommit(false);//将自动提交关闭
try (PreparedStatement countStmt = conn.prepareStatement(sql))
// 把新语句设置回去
metaStatementHandler.setValue("delegate.boundSql.sql", sql);
countStmt.execute();
conn.commit();//执行完后,手动提交事务
// System.out.println(isSuccess);
catch (Exception e)
e.printStackTrace();
finally
conn.setAutoCommit(preAutoCommitState);//在把自动提交打开
// 分表处理逻辑
if (strategyClass != TableNameStrategyVoid.class)
if (tablePrepare.enableTableShard())
String updateSql = originSql;
for (String tableName : appointTable)
// 策略处理表名
String newTableName = tableStrategy.tableName(tableName, dependValue);
updateSql = updateSql.replaceAll(tableName, newTableName);
// 把新语句设置回去,替换表名
metaStatementHandler.setValue("delegate.boundSql.sql", updateSql);
else
// fix 启用了自动建表,但是没有启用分表的时候,sql被替换成建表的sql。没有设置回来的问题
metaStatementHandler.setValue("delegate.boundSql.sql", originSql);
catch (Exception ignored)
// ignore 任何一个地方有异常都去执行原始操作 -- invocation.proceed()
return invocation.proceed();
/**
* 从参数里面找到指定对象指定字段对应的值
*/
private String getDependFieldValue(TablePrepare tablePrepare, MetaObject metaStatementHandler, MappedStatement mappedStatement) throws Exception
// 以上情况下不满足则走@TableShardParam机制
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
Method[] methods = Class.forName(className).getMethods();
Method method = null;
for (Method me : methods)
if (me.getName().equals(methodName) && me.isAnnotationPresent(tablePrepare.annotationType()))
method = me;
if (method == null)
return null;
Parameter[] parameters = method.getParameters();
if (parameters.length == 0)
return null;
int flag = 0;
Parameter parameter = null;
for (Parameter p : parameters)
// TableShardParam和Param需要同时添加
if (p.getAnnotation(TableShardParam.class) != null && p.getAnnotation(Param.class) != null)
parameter = p;
flag++;
// 参数没有注解则退出
if (flag == 0)
return null;
// 多个则抛异常
if (flag > 1)
throw new RuntimeException("存在多个指定@TableShardParam的参数,无法处理");
String tableSharedFieldParamKey = parameter.getAnnotation(Param.class).value();
TableShardParam annotation = parameter.getAnnotation(TableShardParam.class);
Class<?> parameterType = parameter.getType(); // 参数的类型
String dependFieldName = StringUtils.isEmpty(annotation.value()) ? annotation.dependFieldName() : annotation.value();
if (isPrimitive(parameterType) || StringUtils.isEmpty(dependFieldName))
return getPrimitiveParamFieldValue(metaStatementHandler, tableSharedFieldParamKey);
else
return getParamObjectFiledValue(metaStatementHandler, tableSharedFieldParamKey, dependFieldName);
/**
* 判断是否是基础类型 9大基础类型及其包装类
*
* @return 是否是基础类型, long, int, Long 等等
*/
private boolean isPrimitive(Class<?> clazz)
if (clazz.isPrimitive())
return true;
try
if (((Class) clazz.getField("TYPE").get(null)).isPrimitive())
return true;
catch (Exception e)
return false;
return clazz.equals(String.class);
/**
* 解析sql获取到sql里面所有的表名
*
* @param sql sql
* @return 表名列表
*/
private List<String> getTableNamesFromSql(String sql)
// 对sql语句进行拆分 -- 以','、'\\n'、'\\t'作为分隔符
List<String> splitterList = Lists.newArrayList(Splitter.on(new CharMatcher()
@Override
public boolean matches(char c)
return Character.isWhitespace(c) || c == '\\n' || c == '\\t';
).omitEmptyStrings().trimResults().split(sql))
.stream()
.filter(s -> !s.equals(","))
.filter(s -> !s.equals("?"))
.filter(s -> !s.equals("?,"))
.filter(s -> !s.equals("("))
.filter(s -> !s.equals(")"))
.filter(s -> !s.equals("="))
.collect(Collectors.toList());
List<String> tableNameList = Lists.newArrayList();
for (String item : SQL_TABLE_NAME_FLAG_PREFIX)
tableNameList.addAll(getTableName(splitterList, Lists.newArrayList(Splitter.on(' ').split(item))));
return tableNameList;
/**
* 获取表名
*/
private List<String> getTableName(List<String> splitterList, List<String> list)
List<String> retList = Lists.newArrayList();
if (list == null || list.isEmpty() || splitterList == null || splitterList.isEmpty() || splitterList.size() <= list.size())
return retList;
for (int index = 0; index < splitterList.size(); index = index + list.size())
if (index < splitterList.size() - list.size())
boolean match = true;
for (int innerIndex = 0; innerIndex < list.size(); innerIndex++)
if (!splitterList.get(index + innerIndex).toLowerCase().equals(list.get(innerIndex).toLowerCase()))
match = false;
break;
if (match)
if ("update".toLowerCase().equals(list.get(0).toLowerCase()))
// ON DUPLICATE KEY UPDATE 需要过滤出来
if (index < 3 || !(splitterList.get(index - 1).toLowerCase().equals("key".toLowerCase()) &&
splitterList.get(index - 2).toLowerCase().equals("DUPLICATE".toLowerCase()) &&
splitterList.get(index - 3).toLowerCase().equals("ON".toLowerCase())))
retList.add(splitterList.get(index + list.size()));
else
retList.add(splitterList.get(index + list.size()));
return retList;
/**
* 获取方法上的TableShard注解
*
* @param mappedStatement MappedStatement
* @return TableShard注解
*/
private TablePrepare getTableShardAnnotation(MappedStatement mappedStatement)
TablePrepare tablePrepare = null;
try
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
final Method[] method = Class.forName(className).getMethods();
for (Method me : method)
if (me.getName().equals(methodName) && me.isAnnotationPresent(TablePrepare.class))
tablePrepare = me.getAnnotation(TablePrepare.class);
break;
catch (Exception ex)
ex.printStackTrace();
return tablePrepare;
/**
* 从参数里面找到指定对象指定字段对应的值--基础类型
*/
private String getPrimitiveParamFieldValue(MetaObject metaStatementHandler, String fieldParamKey)
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
Object parameterObject = boundSql.getParameterObject();
if (parameterObject == null)
return null;
Object filterFiledObject = ((MapperMethod.ParamMap) parameterObject).get(fieldParamKey);
if (filterFiledObject == null)
return null;
Object dependObject = recursiveGetEffectiveObject(filterFiledObject);
return dependObject == null ? null : dependObject.toString();
/**
* 获取参数里面的对象
*/
private Object recursiveGetEffectiveObject(Object srcObject)
if (!(srcObject instanceof List))
return srcObject;
Object listItemObject = ((List) srcObject).get(0);
while (listItemObject instanceof List)
listItemObject = ((List) listItemObject).get(0);
return listItemObject;
/**
* 从参数里面找到指定对象指定字段对应的值--对象
* 如该参数是List.指定对象为第一个元素
*/
private String getParamObjectFiledValue(MetaObject metaStatementHandler, String fieldParamKey, String dependFieldName)
BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
Object parameterObject = boundSql.getParameterObject();
Object filterFiledObject = ((MapperMethod.ParamMap) parameterObject).get(fieldParamKey);
if (filterFiledObject == null)
return null;
Object dependObject = recursiveGetEffectiveObject(filterFiledObject);
try
return ReflectUtil.getFieldValue(dependObject, dependFieldName);
catch (Exception ignored)
return null;
@Override
public Object plugin(Object target)
// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
return (target instanceof RoutingStatementHandler) ? Plugin.wrap(target, this) : target;
@Override
public void setProperties(Properties properties)
四 怎么使用
我们用一个简单实例来教大家怎么使用我们实现的分表功能。基础表名StatisAccHour,
4.1 建表语句
和我们平常使用Mybatis一样的,一个Mapper接口和一个Mapper xml。
public interface CreateTableMapper
int createAccHour();
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tuacy.tableshard.mapper.CreateTableMapper">
<!-- acc 小时表, 一个小时一张表 -->
<update id="createAccHour">
CREATE TABLE IF NOT EXISTS `StatisAccHour` (
`recTime` bigint(20) NOT NULL,
`ptId` int(11) NOT NULL,
`value` double DEFAULT NULL,
PRIMARY KEY (`RecTime`,`PtId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
</update>
</mapper>
4.2 表对应实体类
"StatisAccHour"基础表对应的实体类,三个字段和表里面的字段一一对应。同时我们添加了TableCreate注解在实体了上,指定了基础表名“StatisAccHour”,建表语句在CreateTableMapper类的createAccHour方法里面。
@TableCreate(
tableName = "StatisAccHour",
autoCreateTableMapperClass = CreateTableMapper.class,
autoCreateTableMapperMethodName = "createAccHour"
)
@Getter // lombok 注解,不用手动去写get set方法
@Setter
public class AccHour
/**
* 针对recTime做一个简单说明,
* 比如当前时间是 2019年08月31日00时31分46秒141微妙
* 则我们在数据库里面存20190831003146141
*/
private Long recTime;
private Long ptId;
private Double value;
4.3 分表策略
基础表名和分表依据字段的前十个字符组成分表对应的表名。
/**
* 分表方案 按照年月日时分表
*/
public class SuffixYYYYMMDDHHNameStrategy implements ITableNameStrategy
private static final int SUFFIX_LENGTH = 10; // yyyymmddhh
@Override
public String tableName(String baseTableName, String dependFieldValue)
return baseTableName + dependFieldValue.substring(0, SUFFIX_LENGTH);
4.4 数据库操作
注意TablePrepare注解的添加,每个sql里面的表名还是用的基础表名。最终会在自定义拦截器里面替换。
/**
* AccHour 每个小时一张表(多数据源,我们有三个数据源,我们假设该表放在statis数据源下面)
*/
public interface AccHourMapper
/**
* 往数据库里面插入一条记录
*/
@TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
int insertItem(@TableShardParam(dependFieldName = "recTime") @Param("item") AccHour item);
/**
* 往数据库里面插入多条
*/
@TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
int insertList(@TableShardParam(dependFieldName = "recTime") @Param("list") List<AccHour> list);
/**
* 往数据库里面插入多条
*/
@TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
AccHour selectItem(@TableShardParam(dependFieldName = "recTime") @Param("recvTime") Long recvTime, @Param("pkId") Long pkId);
/**
* 查询指定时间范围内的列表
*
* @param startTIme 开始时间
* @param endTime 解释时间
*/
@TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
List<AccHour> selectLIst(@TableShardParam() @Param("startTime") Long startTIme, @Param("endTime") Long endTime);
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tuacy.tableshard.mapper.AccHourMapper">
<!-- 基础表名 StatisAccHour -->
<!-- 往数据库里面插入一条记录 -->
<insert id="insertItem">
insert into StatisAccHour (
`recTime`,
`ptId`,
`value`
) value (
#item.recTime,
#item.ptId,
#item.value
)
</insert>
<!-- 批量插入多条记录 -->
<insert id="insertList">
insert into StatisAccHour (
`recTime`,
`ptId`,
`value`
) values
<foreach collection="list" item="item" separator=",">
(
#item.recTime,
#item.ptId,
#item.value
)
</foreach>
</insert>
<!-- 查询一条记录 -->
<select id="selectItem" resultType="com.tuacy.tableshard.entity.model.AccHour">
select
`recTime` as recTime,
`ptId` as ptId,
`value` as value
from StatisAccHour
where recTime = #recvTime and ptId = #pkId
</select>
<!-- 查询一条记录 -->
<select id="selectLIst" resultType="com.tuacy.tableshard.entity.model.AccHour">
select
`recTime` as recTime,
`ptId` as ptId,
`value` as value
from StatisAccHour
where recTime >= $startTime and recTime <![CDATA[<=]]> $endTime
</select>
</mapper>
4.5 DAO使用
特别要注意,在Dao层我们需要自己保证每一次操作的数据库都是属于同一个分表的。比如插入一批数据的时候,我们需要自己对不同分表的数据做分批次处理。保存每个调用mapper插入的时候都是属于同一个分表的数据。具体可以看看下面insertList()方法的具体实现。
@Repository
public class AccHourDao extends BaseDao implements IAccHourDao
/**
* 基础表名
*/
private static final String BASE_TABLE_NAME = "StatisAccHour";
/**
* 分表策略
*/
private static final ITableNameStrategy STRATEGY = new SuffixYYYYMMDDHHNameStrategy();
private AccHourMapper accHourMapper;
@Autowired
public void setAccHourMapper(AccHourMapper accHourMapper)
this.accHourMapper = accHourMapper;
/**
* DataSourceAnnotation 用于指定数据源,放到统计数据库里面
*/
@Override
@DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
@Transactional(rollbackFor = Exception.class)
public int insertItem(AccHour item)
return accHourMapper.insertItem(item);
@Override
@DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
@Transactional(rollbackFor = Exception.class)
public int insertList(List<AccHour> list)
if (list == null || list.isEmpty())
return 0;
// 首先,我们不能保证list所有的数据都是一张表的,所以我们先得对数据分类,按表来分类
Map<String, List<AccHour>> groupingByTable = list.stream().collect(Collectors.groupingBy(
item -> STRATEGY.tableName(BASE_TABLE_NAME, item.getRecTime().toString()),
(Supplier<Map<String, List<AccHour>>>) HashMap::new,
Collectors.toList()));
// 遍历存储(上面的代码我们已经保存了每个Map.)
int sucessCount = 0;
for (List<AccHour> mapValueItem : groupingByTable.values())
sucessCount += accHourMapper.insertList(mapValueItem);
return sucessCount;
@Override
@DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
@Transactional(rollbackFor = Exception.class)
public AccHour selectItem(Long recvTime, Long ptId)
return accHourMapper.selectItem(recvTime, ptId);
/**
* 查询指定时间范围的数据
* 针对time做一个简单说明,
* 比如当前时间是 2019年08月31日00时31分46秒141微妙
* 则我们在数据库里面存20190831003146141
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 所有查询到的记录
*/
@Override
@DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
@Transactional(rollbackFor = Exception.class)
public List<AccHour> selectList(Long startTime, Long endTime)
// long类型是20190831003146141的形式转换为2019年08月31日00时31分46秒141微妙对应的LocalDateTime
LocalDateTime startTimeDate = DbDataTimeUtils.long2DateTime(startTime);
LocalDateTime endTimeDate = DbDataTimeUtils.long2DateTime(endTime);
if (startTimeDate.isAfter(endTimeDate))
return null;
// 数据库里面所有的表
List<String> allTableName = allTableName();
if (allTableName == null || allTableName.isEmpty())
return null;
// 全部转换成小写
allTableName = allTableName.stream().map(String::toLowerCase).collect(Collectors.toList());
List<TwoTuple<Long, Long>> singleTableConditionList = Lists.newArrayList();
// 我们已经确定了当前是按小时分表的,表名类似于 StatisAccHour2019083122 的形式,先确定指定的时间范围里面有多少张表
while (startTimeDate.isBefore(endTimeDate) || startTimeDate.equals(endTimeDate))
String tableName = STRATEGY.tableName(BASE_TABLE_NAME, String.valueOf(DbDataTimeUtils.dateTime2Long(startTimeDate)));
if (allTableName.contains(tableName.toLowerCase()))
// 有这个表存在
Long singleTableStartTime = DbDataTimeUtils.dateTime2Long(startTimeDate);
if (singleTableStartTime < startTime)
singleTableStartTime = startTime;
singleTableConditionList.add(new TwoTuple<>(singleTableStartTime, endTime));
startTimeDate = startTimeDate.plusHours(1);
if (singleTableConditionList.isEmpty())
return null;
List<AccHour> retList = Lists.newArrayList();
for (TwoTuple<Long, Long> item : singleTableConditionList)
retList.addAll(accHourMapper.selectLIst(item.getFirst(), item.getSecond()));
return retList;
关于Spring Boot Mybatis实现分表功能,整个的实现逻辑就这么多。估计上面很多地方我们也没讲明白,可能有些地方认为简单就没讲。所以这里面把整个实现源码的链接地址给出来 https://github.com/tuacy/java-study/tree/master/tableshard 推荐大家去看下具体源码是怎么实现的,有什么疑问可以留言。
以上是关于Spring Boot Mybatis实现分表的主要内容,如果未能解决你的问题,请参考以下文章
spring boot:用shardingjdbc实现多数据源的分库分表(shardingsphere 4.1.1/spring boot 2.3.1)
mybatis-plus 动态数据源读写分离 + shardingJDBC分库分表