mysql mybatis分表查询_mybatis 自动分表
参考:相关源码已上传至我的github欢迎转载,转载请注明出处,尊重作者劳动成果:https://www.cnblogs.com/li-mzx/p/9963312.html前言小弟才疏学浅,可能很多问题也没有考虑到,权当抛砖引玉,希望各位大神指点项目背景:希望做一个功能,能在sql操作数据库时,根据某个字段的值,或者说某种策略, 自动操作对应的表比如user表user_oa,其中useri...
参考:
相关源码已上传至我的 github
欢迎转载,转载请注明出处,尊重作者劳动成果:https://www.cnblogs.com/li-mzx/p/9963312.html
前言
小弟才疏学浅,可能很多问题也没有考虑到,权当抛砖引玉,希望各位大神指点
项目背景:
希望做一个功能,能在sql操作数据库时,根据某个字段的值,或者说某种策略, 自动操作对应的表
比如 user表
user_oa,其中userid 为 oa000001、oa000002、oa123456
user_bz,其中userid 为 bz000002
user_sr, 其中userid 为 sr654321
根据业务人员所使用的系统,将user表细分为3个
分表规则为业务人员所注册的系统,比如上面的, sr oa bz
当dao层操作数据库时,系统自动根据userid 或指明分表名,自动去操作对应的表,即1个查询,对应多个数据库相同结构的表
实现思路
1、在需要分表的实体类中, 实现接口,提供分表所需要的分表策略,否则需要在dao的操作数据库方法中,加入表名参数
2、在需要分表的Dao接口中,添加注解,声明一个需要分表的操作,供拦截器拦截
3、定义拦截器,注册到mybatis中,在mybatis使用sql语句操作数据库之前,拦截添加了注解的dao方法,修改sql语句,将其中的表名,全部添加 从参数中或实体类中取得的表名后缀
代码环境
IntelliJ IDEA 2018.2.5 +jdk1.8.0 +Spring Boot 1.5.17 +MySql 5.7 + MyBatis 1.3.2 + Druid 1.1.3
代码
maven依赖:
UTF-8
UTF-8
1.8
1.1.3
2.7.0
com.alibaba
fastjson
1.2.45
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
org.springframework.boot
spring-boot-devtools
true
mysql
mysql-connector-java
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
com.alibaba
druid-spring-boot-starter
${druid.version}
io.springfox
springfox-swagger2
${swagger.version}
io.springfox
springfox-swagger-ui
${swagger.version}
com.github.jsqlparser
jsqlparser
1.1
maven 依赖
application.yml
server:
port: 8021
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
druid:
url: jdbc:mysql://localhost:3306/local?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: limz
password: 123456
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
proxy-filters:
list:
ref: logFilter
#开启debug模式,用于打印sql
logging:
level:
com.limz.mysql.dsmysql.Dao: debug
application.yml
声明一个接口,提供获取表名后缀的方法
/**
* 需要分表的实体类,必须实现的接口
*/
public interface ShardEntity {
/**
* 需要分表的类,需要实现此方法, 提供分表后缀名的获取
* @return
*/
String getShardName();
}
实体类实现此接口
@Datapublic class User implementsSerializable, ShardEntity {privateString userId;
@NotNull(message= "用户名不能为空")privateString userName;privateString msg;private Listtelephones;
//提供获取后缀名的方法 此处为userid 的前两位,代表所在的系统publicString getShardName(){return userId != null ? userId.substring(0,2) : null;
}
}
声明一个注解,加此注解的dao表示需要分表
/*** 需要分表的 Dao 添加此注解,标记为需要分表*/@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)public @interfaceTableShard {//默认分表, 为false时, 此注解无效
boolean split() default true;
}
Dao层接口添加此注解,并在参数中传递shardName或ShardEntity实现类的对象
/*** 需要分区的dao 需要加上 @TableShard 注解*/@TableShardpublic interfaceUserDao{
@Insert({"insert into user(userId, userName, msg) values(#{userId}, #{userName}, #{msg})"})
@Options(keyProperty= "userId",keyColumn = "userId")voidsave(User user);/*** 需要分区的方法参数中, 必须存在 @Param("shardName") 的参数, 或者 存在实体类参数 实现了 ShardEntity 接口 如下面的 User
*@paramuser
*@paramshardName
*@return
*/@Select("")
@Results({
@Result(property= "userId",column = "userId"),
@Result(property= "userName",column = "userName"),
@Result(property= "msg",column = "msg"),
@Result(property= "telephones", javaType = List.class, column = "{userId=userId, shardName=shardName}", many = @Many(select = "com.limz.mysql.dsmysql.Dao.TelephoneDao.findTelephoneByUserId"))
})
List query(@Param("user") User user, @Param("shardName") String shardName);
}
此处副表也同样分表
@Datapublic class Telephone implementsSerializable, ShardEntity{privateLong id;privateString userId;privateString telephone;publicString getShardName(){return userId != null ? userId.substring(0,2) : null;
}
}
Telephone
@TableShardpublic interfaceTelephoneDao{
@Insert("insert into telephone (userId, telephone) values(#{userId},#{telephone})")voidsave(Telephone t);
@Select("select * from telephone where userId = #{userId}")
List findTelephoneByUserId(@Param("shardName") String shardName, String userId);
@Select("select * from telephone where id = #{id}")
Telephone get(Telephone t);
}
TelephoneDao
核心功能,声明一个拦截器,注册到Mybatis中, 拦截sql语句,
/*** 分表查询 拦截器 核心功能*/@Intercepts({@Signature(type= StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})public class TableSegInterceptor implementsInterceptor {private Logger logger = LoggerFactory.getLogger(this.getClass());//SQL解析工厂
private final SqlParserFactory parserFactory = newJSqlParserFactory();//sql语句存储字段
private finalField boundSqlField;publicTableSegInterceptor() {try{
boundSqlField= BoundSql.class.getDeclaredField("sql");
boundSqlField.setAccessible(true);
}catch(Exception e) {throw newRuntimeException(e);
}
}
@Overridepublic Object intercept(Invocation invocation) throwsThrowable {if (invocation.getTarget() instanceofExecutor) {returninvocation.proceed();
}
System.out.println("进入拦截器:====================");
StatementHandler statementHandler=(StatementHandler) invocation.getTarget();
MetaObject mo= MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, newDefaultReflectorFactory());
MappedStatement mappedStatement= (MappedStatement) mo.getValue("delegate.mappedStatement");//解析出MappedStatement的ID 从中获取Dao类信息
String id =mappedStatement.getId();
String clzName= id.substring(0,id.lastIndexOf("."));
Class> clzObj =Class.forName(clzName);//是否添加 @TableShard注解
TableShard ts = clzObj.getAnnotation(TableShard.class);if (ts != null &&ts.split()){//进行SQL解析,如果未找到表名,则跳过
BoundSql boundSql =statementHandler.getBoundSql();
SqlParser sqlParser=parserFactory.createParser(boundSql.getSql());
List
}//获取分表后缀名
String shardName = null;
Object v2= mo.getValue("delegate.boundSql.parameterObject");if (v2 instanceofMap){
Map pm=(Map) v2;//一定先从参数中查询,是否有 @Param("shardName") 的参数, 如果有,当做分表后缀,//如果没有, 将遍历参数, 找到实现了ShardEntity接口的参数
shardName = (String) pm.get("shardName");if (shardName == null){
Collection values=pm.values();for(Object o : values) {if (o instanceofShardEntity){
ShardEntity se=(ShardEntity) o;
shardName=se.getShardName();break;
}
}
}//如果只有一个参数,为实体类,则直接从中获取属性
}else{if (v2 instanceofShardEntity) {
ShardEntity se=(ShardEntity) v2;
shardName=se.getShardName();
}
}//如果参数中 未包含 shardName 相关参数, 则抛出异常
if (shardName == null)throw new ShardException("shardName must be not empty!");//设置实际的表名
for (int index = 0; index < tables.size(); index++) {
Table table=tables.get(index);//替换所有表名,为表名添加后缀
String targetName = table.getName() + "_" +shardName;
logger.info("Sharding table, {}-->{}", table, targetName);
table.setName(targetName);
}//修改实际的SQL
String targetSQL =sqlParser.toSQL();
boundSqlField.set(boundSql, targetSQL);
}returninvocation.proceed();
}
@OverridepublicObject plugin(Object target) {return Plugin.wrap(target, this);
}
@Overridepublic voidsetProperties(Properties properties) {
}
其中解析sql用的工具位jsqlparser 具体代码见我的github
然后将拦截器注册到mybatis中
@BeanpublicInterceptor getInterceptor(){
Interceptor interceptor= newTableSegInterceptor();returninterceptor;
}
OK 试一下
可以看到,根据userid 前两位, 自动将表名更改
扩展:
如果需要别的分表策略,只需要在实现ShardEntity时,将返回分表名后缀的方法换一种实现,比如根据创建时间,或者根据区域等
拦截器中返回结果处,可以扩展为, 如果不存在shardName 则获取所有叫 user_* 的表,查询所有表结果然后 union 拼接,只不过这样会使效率降低
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)