參考:
https://blog.csdn.net/qq_37751454/article/details/81630100
https://blog.csdn.net/Dwade_mia/article/details/77371871
相關源碼已上傳至我的 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依賴:

<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <druid.version>1.1.3</druid.version> <swagger.version>2.7.0</swagger.version> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.45</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>1.1</version> </dependency>
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
聲明一個接口,提供獲取表名后綴的方法
/**
* 需要分表的實體類,必須實現的接口
*/
public interface ShardEntity {
/**
* 需要分表的類,需要實現此方法, 提供分表后綴名的獲取
* @return
*/
String getShardName();
}
實體類實現此接口
@Data public class User implements Serializable, ShardEntity { private String userId; @NotNull(message = "用戶名不能為空") private String userName; private String msg; private List<Telephone> telephones; //提供獲取后綴名的方法 此處為userid 的前兩位,代表所在的系統 public String getShardName(){ return userId != null ? userId.substring(0,2) : null; } }
聲明一個注解,加此注解的dao表示需要分表
/** * 需要分表的 Dao 添加此注解,標記為需要分表 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface TableShard { //默認分表, 為false時, 此注解無效 boolean split() default true; }
Dao層接口添加此注解,並在參數中傳遞shardName或ShardEntity實現類的對象
/** * 需要分區的dao 需要加上 @TableShard 注解 */ @TableShard public interface UserDao{ @Insert({"insert into user(userId, userName, msg) values(#{userId}, #{userName}, #{msg})"}) @Options(keyProperty = "userId",keyColumn = "userId") void save(User user); /** * 需要分區的方法參數中, 必須存在 @Param("shardName") 的參數, 或者 存在實體類參數 實現了 ShardEntity 接口 如下面的 User * @param user * @param shardName * @return */ @Select("<script> select *, #{shardName} as shardName from user where userId=#{user.userId} <if test=\"user.userName != null\"> and userName = #{user.userName}</if> </script>") @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<User> query(@Param("user") User user, @Param("shardName") String shardName); }
此處副表也同樣分表

@Data public class Telephone implements Serializable, ShardEntity{ private Long id; private String userId; private String telephone; public String getShardName(){ return userId != null ? userId.substring(0,2) : null; } }

@TableShard public interface TelephoneDao{ @Insert("insert into telephone (userId, telephone) values(#{userId},#{telephone})") void save(Telephone t); @Select("select * from telephone where userId = #{userId}") List<Telephone> findTelephoneByUserId(@Param("shardName") String shardName, String userId); @Select("select * from telephone where id = #{id}") Telephone get(Telephone t); }
核心功能,聲明一個攔截器,注冊到Mybatis中, 攔截sql語句,
/** * 分表查詢 攔截器 核心功能 */ @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class TableSegInterceptor implements Interceptor { private Logger logger = LoggerFactory.getLogger(this.getClass()); //SQL解析工廠 private final SqlParserFactory parserFactory = new JSqlParserFactory(); //sql語句存儲字段 private final Field boundSqlField; public TableSegInterceptor() { try { boundSqlField = BoundSql.class.getDeclaredField("sql"); boundSqlField.setAccessible(true); } catch (Exception e) { throw new RuntimeException(e); } } @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof Executor) { return invocation.proceed(); } System.out.println("進入攔截器:===================="); StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject mo = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); 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<Table> tables = sqlParser.getTables(); if (tables.isEmpty()) { return invocation.proceed(); } //獲取分表后綴名 String shardName = null; Object v2 = mo.getValue("delegate.boundSql.parameterObject"); if (v2 instanceof Map){ 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 instanceof ShardEntity){ ShardEntity se = (ShardEntity) o; shardName = se.getShardName(); break; } } } //如果只有一個參數,為實體類,則直接從中獲取屬性 }else { if (v2 instanceof ShardEntity) { 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); } return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { }
其中解析sql用的工具位jsqlparser 具體代碼見我的github
然后將攔截器注冊到mybatis中
@Bean public Interceptor getInterceptor(){ Interceptor interceptor = new TableSegInterceptor(); return interceptor; }
OK 試一下
可以看到,根據userid 前兩位, 自動將表名更改
擴展:
如果需要別的分表策略,只需要在實現ShardEntity時,將返回分表名后綴的方法換一種實現,比如根據創建時間,或者根據區域等
攔截器中返回結果處,可以擴展為, 如果不存在shardName 則獲取所有叫 user_* 的表,查詢所有表結果然后 union 拼接,只不過這樣會使效率降低