返回顶部
首页 > 资讯 > 数据库 >用Mybatis手写一个分表插件
  • 236
分享到

用Mybatis手写一个分表插件

2024-04-02 19:04:59 236人浏览 安东尼
摘要

这篇文章主要介绍“用mybatis手写一个分表插件”,在日常操作中,相信很多人在用Mybatis手写一个分表插件问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用Mybatis

这篇文章主要介绍“用mybatis手写一个分表插件”,在日常操作中,相信很多人在用Mybatis手写一个分表插件问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用Mybatis手写一个分表插件”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

背景

事情是酱紫的,阿星的上级leader负责记录信息的业务,每日预估数据量是15万左右,所以引入sharding-jdbc做分表。

上级leader完成业务的开发后,走了一波自测,git push后,就忙其他的事情去了。

项目框架SpringBoot+Mybaits

出问题了

阿星负责的业务也开发完了,熟练的git pull,准备自测,单元测试run一下,上个厕所回来收工,就是这么自信。

回来后,看下控制台,人都傻了,一片红,内心不禁感叹“如果这是股票基金该多好”。

出了问题就要解决,随着排查深入,我的眉头一皱发现事情并不简单,怎么以前的一些代码都报错了?

随着排查深入,最后跟到了Mybatis源码,发现罪魁祸首是sharding-jdbc引起的,因为数据源是sharding-jdbc的,导致后续执行sql的是ShardingPreparedStatement。

这就意味着,sharding-jdbc影响项目的所有业务表,因为最终数据库交互都由ShardingPreparedStatement去做了,历史的一些sql语句因为sql函数或者其他写法,使得ShardingPreparedStatement无法处理而出现异常。

关键代码如下

用Mybatis手写一个分表插件

发现问题后,阿星马上就反馈给leader了。

用Mybatis手写一个分表插件

唉,本来还想摸鱼的,看来摸鱼的时间是没了,还多了一项任务。

分析

竟然交给阿星来做了,就撸起袖子开干吧,先看看分表功能的需求

  • 支持自定义分表策略

  • 能控制影响范围

  • 通用性

分表会提前建立好,所以不需要考虑表不存在的问题,核心逻辑实现,通过分表策略得到分表名,再把分表名动态替换到sql。

用Mybatis手写一个分表插件

分表策略

为了支持分表策略,我们需要先定义分表策略抽象接口,定义如下

 public interface ITableShardStrategy {            String generateTableName(String tableNamePrefix,Object value);           default void verificationTableNamePrefix(String tableNamePrefix){         if (StrUtil.isBlank(tableNamePrefix)) {             throw new RuntimeException("tableNamePrefix is null");         }     } }

generateTableName函数的任务就是生成分表名,入参有tableNamePrefix、value,tableNamePrefix为分表前缀,value作为生成分表名的逻辑参数。

verificationTableNamePrefix函数验证tableNamePrefix必填,提供给实现类使用。

为了方便理解,下面是id取模策略代码,取模两张表

 @Component public class TableShardStrategyId implements ITableShardStrategy {     @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             throw new RuntimeException("value is null");         }         long id = Long.parseLong(value.toString());         //此处可以缓存优化         return tableNamePrefix + "_" + (id % 2);     } }

传入进来的value是id值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影响范围

分表策略已经抽象出来,下面要考虑控制影响范围,我们都知道Mybatis规范中每个Mapper类对应一张业务主体表,Mapper类的函数对应业务主体表的相关sql。

阿星想着,可以给Mapper类打上注解,代表该Mpaaer类对应的业务主体表有分表需求,从规范来说Mapper类的每个函数对应的主体表都是正确的,但是有些同学可能不会按规范来写。

假设Mpaaer类对应的是B表,Mpaaer类的某个函数写着A表的sql,甚至是历史遗留问题,所以注解不仅仅可以打在Mapper类上,同时还可以打在Mapper类的任意一个函数上,并且保证小粒度覆盖粗粒度。

阿星这里自定义分表注解,代码如下

 @Target(value = {ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TableShard {      // 表前缀名     String tableNamePrefix();      //值     String value() default "";      //是否是字段名,如果是需要解析请求参数改字段名的值(默认否)     boolean fieldFlag() default false;      // 对应的分表策略类     Class<? extends ITableShardStrategy> shardStrategy();   }

注解的作用范围是类、接口、函数,运行时生效。

tableNamePrefix与shardStrategy属性都好理解,表前缀名和分表策略,剩下的value与fieldFlag要怎么理解,分表策略分两类,第一类依赖表中某个字段值,第二类则不依赖。

根据企业id取模,属于第一类,此处的value设置企业id入参字段名,fieldFlag为true,意味着,会去解析获取企业id字段名对应的值。

根据日期分表,属于第二类,直接在分表策略实现类里面写就行了,不依赖表字段值,value与fieldFlag无需填写,当然你value也可以设置时间格式,具体看分表策略实现类的逻辑。

通用性

抽象分表策略与分表注解都搞定了,最后一步就是根据分表注解信息,去执行分表策略得到分表名,再把分表名动态替换到sql中,同时具有通用性。

Mybatis框架中,有拦截器机制做扩展,我们只需要拦截StatementHandler#prepare函数,即StatementHandle创建Statement之前,先把sql里面的表名动态替换成分表名。

Mybatis分表拦截器流程图如下

用Mybatis手写一个分表插件

Mybatis分表拦截器代码如下,有点长哈,主流程看intercept函数就好了。

 @Intercepts({         @Signature(                 type = StatementHandler.class,                 method = "prepare",                 args = {Connection.class, Integer.class}         ) }) public class TableShardInterceptor implements Interceptor {      private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();      @Override     public Object intercept(Invocation invocation) throws Throwable {          // MetaObject是mybatis里面提供的一个工具类,类似反射的效果         MetaObject metaObject = getMetaObject(invocation);         BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");         MappedStatement mappedStatement = (MappedStatement)                 metaObject.getValue("delegate.mappedStatement");          //获取Mapper执行方法         Method method = invocation.getMethod();          //获取分表注解         TableShard tableShard = getTableShard(method,mappedStatement);          // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑         if (tableShard == null) {             return invocation.proceed();         }          //获取值         String value = tableShard.value();         //value是否字段名,如果是,需要解析请求参数字段名的值         boolean fieldFlag = tableShard.fieldFlag();          if (fieldFlag) {             //获取请求参数             Object parameterObject = boundSql.getParameterObject();              if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap类型逻辑处理                  MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;                 //根据字段名获取参数值                 Object valueObject = parameterMap.get(value);                 if (valueObject == null) {                     throw new RuntimeException(String.fORMat("入参字段%s无匹配", value));                 }                 //替换sql                 replaceSql(tableShard, valueObject, metaObject, boundSql);              } else { //单参数逻辑                  //如果是基础类型抛出异常                 if (isBaseType(parameterObject)) {                     throw new RuntimeException("单参数非法,请使用@Param注解");                 }                  if (parameterObject instanceof Map){                     Map<String,Object>  parameterMap =  (Map<String,Object>)parameterObject;                     Object valueObject = parameterMap.get(value);                     //替换sql                     replaceSql(tableShard, valueObject, metaObject, boundSql);                 } else {                     //非基础类型对象                     Class<?> parameterObjectClass = parameterObject.getClass();                     Field declaredField = parameterObjectClass.getDeclaredField(value);                     declaredField.setAccessible(true);                     Object valueObject = declaredField.get(parameterObject);                     //替换sql                     replaceSql(tableShard, valueObject, metaObject, boundSql);                 }             }          } else {//无需处理parameterField             //替换sql             replaceSql(tableShard, value, metaObject, boundSql);         }         //执行下一个插件逻辑         return invocation.proceed();     }       @Override     public Object plugin(Object target) {         // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数         if (target instanceof StatementHandler) {             return Plugin.wrap(target, this);         } else {             return target;         }     }            private boolean isBaseType(Object object) {         if (object.getClass().isPrimitive()                 || object instanceof String                 || object instanceof Integer                 || object instanceof Double                 || object instanceof Float                 || object instanceof Long                 || object instanceof Boolean                 || object instanceof Byte                 || object instanceof Short) {             return true;         } else {             return false;         }     }           private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) {         String tableNamePrefix = tableShard.tableNamePrefix();         //获取策略class         Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();         //从spring ioc容器获取策略类          ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);         //生成分表名         String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);         // 获取sql         String sql = boundSql.getSql();         // 完成表名替换         metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName));     }           private MetaObject getMetaObject(Invocation invocation) {         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();         // MetaObject是mybatis里面提供的一个工具类,类似反射的效果         MetaObject metaObject = MetaObject.forObject(statementHandler,                 SystemMetaObject.DEFAULT_OBJECT_FACTORY,                 SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,                 defaultReflectorFactory         );          return metaObject;     }           private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException {         String id = mappedStatement.getId();         //获取Class         final String className = id.substring(0, id.lastIndexOf("."));         //分表注解         TableShard tableShard = null;         //获取Mapper执行方法的TableShard注解         tableShard = method.getAnnotation(TableShard.class);         //如果方法没有设置注解,从Mapper接口上面获取TableShard注解         if (tableShard == null) {             // 获取TableShard注解             tableShard = Class.forName(className).getAnnotation(TableShard.class);         }         return tableShard;     }  }

到了这里,其实分表功能就已经完成了,我们只需要把分表策略抽象接口、分表注解、分表拦截器抽成一个通用jar包,需要使用的项目引入这个jar,然后注册分表拦截器,自己根据业务需求实现分表策略,在给对应的Mpaaer加上分表注解就好了。

用Mybatis手写一个分表插件

实践跑起来

这里阿星单独写了一套demo,场景是有两个分表策略,表也提前建立好了

  • 根据id分表

    • tb_log_id_0

    • tb_log_id_1

  • 根据日期分表

    • tb_log_date_202105

    • tb_log_date_202106

预警:后面都是代码实操环节,请各位读者大大耐心看完(非Java开发除外)。

TableShardStrategy定义

 @Component public class TableShardStrategyDate implements ITableShardStrategy {      private static final String DATE_PATTERN = "yyyyMM";      @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN);         } else {             return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString());         }     } }    **  * @Author 程序猿阿星  * @Description 分表策略id  * @Date 2021/5/9  */ @Component public class TableShardStrategyId implements ITableShardStrategy {     @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             throw new RuntimeException("value is null");         }         long id = Long.parseLong(value.toString());         //可以加入本地缓存优化         return tableNamePrefix + "_" + (id % 2);     } }

Mapper定义

Mapper接口

 @TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class) public interface LogDateMapper {           List<LogDate> queryList();           void  save(LogDate logDate);  }   -------------------------------------------------------------------------------------------------    @TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class) public interface LogIdMapper {           LogId queryOne(@Param("id") long id);           void save(LogId logId);   }

Mapper.xml

<?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.xing.shard.mapper.LogDateMapper">          //对应LogDateMapper#queryList函数     <select id="queryList" resultType="com.xing.shard.entity.LogDate">         select         id as id,         comment as comment,         create_date as createDate         from         tb_log_date     </select>          //对应LogDateMapper#save函数     <insert id="save" >         insert into tb_log_date(id, comment,create_date)         values (#{id}, #{comment},#{createDate})     </insert> </mapper>  -------------------------------------------------------------------------------------------------  <?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.xing.shard.mapper.LogIdMapper">          //对应LogIdMapper#queryOne函数     <select id="queryOne" resultType="com.xing.shard.entity.LogId">         select         id as id,         comment as comment,         create_date as createDate         from         tb_log_id         where         id = #{id}     </select>          //对应save函数     <insert id="save" >         insert into tb_log_id(id, comment,create_date)         values (#{id}, #{comment},#{createDate})     </insert>  </mapper>

执行下单元测试

日期分表单元测试执行

@Test   void test() {       LogDate logDate = new LogDate();       logDate.setId(snowflake.nextId());       logDate.setComment("测试内容");       logDate.setCreateDate(new Date());       //插入       logDateMapper.save(logDate);       //查询       List<LogDate> logDates = logDateMapper.queryList();       System.out.println(JSONUtil.tojsonPrettyStr(logDates));   }

输出结果

用Mybatis手写一个分表插件

id分表单元测试执行

@Test void test() {     LogId logId = new LogId();     long id = snowflake.nextId();     logId.setId(id);     logId.setComment("测试");     logId.setCreateDate(new Date());     //插入     logIdMapper.save(logId);     //查询     LogId logIdObject = logIdMapper.queryOne(id);     System.out.println(JSONUtil.toJsonPrettyStr(logIdObject)); }

输出结果

用Mybatis手写一个分表插件

小结一下

本文可以当做对Mybatis进阶的使用教程,通过Mybatis拦截器实现分表的功能,满足基本的业务需求,虽然比较简陋,但是Mybatis这种扩展机制与设计值得学习思考。

到此,关于“用Mybatis手写一个分表插件”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

您可能感兴趣的文档:

--结束END--

本文标题: 用Mybatis手写一个分表插件

本文链接: https://lsjlt.com/news/56097.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 用Mybatis手写一个分表插件
    这篇文章主要介绍“用Mybatis手写一个分表插件”,在日常操作中,相信很多人在用Mybatis手写一个分表插件问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用Mybatis...
    99+
    2024-04-02
  • Mybatis实现分表插件
    背景 事情是酱紫的,阿星的上级leader负责记录信息的业务,每日预估数据量是15万左右,所以引入sharding-jdbc做分表。 上级leader完成业务的开发后,走了一波自测,...
    99+
    2024-04-02
  • TypeScript手写一个简单的eslint插件实例
    目录引言前置知识第一个eslint规则:no-console本地测试本地查看效果no-console规则添加功能:排除用户指定的文件发布npm包引言 看到参考链接1以后,觉得用TS...
    99+
    2023-02-06
    TypeScript eslint插件 TypeScript eslint
  • mybatis插件怎么实现自定义改写表名
    这篇文章主要介绍“mybatis插件怎么实现自定义改写表名”,在日常操作中,相信很多人在mybatis插件怎么实现自定义改写表名问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”mybatis插件怎么实现自定义改...
    99+
    2023-06-30
  • 手写mybatis完整sql插件问题及实现思路
    问题产生 我们在使用mybatis的过程中,如果开启了mysql的日志功能的话,会在控制台打印一些sql的信息,但是日志中的sql语句,是没有拼接参数的,也就是说,是不可以直接放到数...
    99+
    2024-04-02
  • MyBatis分页插件PageHelper如何使用
    本篇内容介绍了“MyBatis分页插件PageHelper如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!MyBatis使⽤PageH...
    99+
    2023-07-05
  • 【JavaWeb】手写一个Servlet+JSP+JavaBean分页
    ✅✅作者主页:🔗孙不坚1208的博客 🔥🔥精选专栏:🔗JavaWeb从入门到精通(持续更新中) 📋📋...
    99+
    2023-08-31
    java tomcat 分页 jsp servlet
  • React手写一个手风琴组件示例
    目录知识点结构分析AccordionItem子组件Accordion容器组件知识点 emotion语法react语法css语法typescript类型语法 结构分析 根据上图,我们来...
    99+
    2024-04-02
  • mybatis插件实现自定义改写表名实例代码
    代码如下: @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatem...
    99+
    2024-04-02
  • bootstrap用哪个表格插件
    本篇内容介绍了“bootstrap用哪个表格插件”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成! ...
    99+
    2024-04-02
  • java编程自写一款JavaScript超实用表格插件
    目录使用效果文档说明第一步第二步第三步源码使用效果 文档说明 第一步 从tableFactory获图标Form对象。 或者: 第二步 加载参数,传入json对象 表格效果: ...
    99+
    2024-04-02
  • 手把手教你写一个uniapp通用页面组件
    目录前言需求开发初始化页面数据实现状态栏与底部配置页面使用总结前言 做移动端项目时为了兼容各种手机型号的界面,最好有一个统一的页面组件对样式做统一处理,例如:判断是否显示状态栏,是否...
    99+
    2022-12-15
    uniapp 组件开发 uniapp组件使用 uniapp通用页面组件
  • Flutter笔记:手写并发布一个人机滑动验证码插件
    Flutter笔记 手写一个人机滑块验证码 作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 :291148484@163.co...
    99+
    2023-10-19
    Flutter Dart 验证码 滑块验证 插件
  • 如何使用vue写一个翻页的时间插件
    本文小编为大家详细介绍“如何使用vue写一个翻页的时间插件”,内容详细,步骤清晰,细节处理妥当,希望这篇“如何使用vue写一个翻页的时间插件”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。代码<templat...
    99+
    2023-07-05
  • mybatis分页插件pageHelper的使用示例
    小编给大家分享一下mybatis分页插件pageHelper的使用示例,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!工作的框架spring springmvc mybatis3首先使用分页...
    99+
    2024-04-02
  • 【解决】Mybatis-plus分页插件一对多分页查询问题
    项目场景: 一对多分页查询,查询主表的同时将子表数据查出 问题描述 Mybatis-plus分页插件多表连查寻时数量不对 总条数有8条 实际只查询出了7条 结果丢失了一条数据,并且还会导致主数据...
    99+
    2023-09-23
    mybatis java mysql
  • Mybatis-plus配置分页插件返回统一结果集
    目录一、MyBatisPlusConfig中配置分页插件1. 分页实现的原理二、统一结果集1. 创建返回码定义类2. 创建结果集类三、编写分页接口1. 先编写查询类2. servic...
    99+
    2024-04-02
  • Mybatis第三方PageHelper分页插件怎么用
    这篇文章将为大家详细讲解有关Mybatis第三方PageHelper分页插件怎么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。用法此时commentAnalyses为Page对象(PageHelper插...
    99+
    2023-06-29
  • MyBatis分页插件PageHelper的使用与原理
    目录MyBatis使⽤PageHelper1.limit分⻚2.PageHelper插件MyBatis使⽤PageHelper 1.limit分⻚ (1)概念: ①页码:pageNu...
    99+
    2023-02-24
    MyBatis分页插件PageHelper MyBatis分页插件 MyBatis PageHelper
  • MyBatis-Plus分页插件的配置与使用
    MyBatis-Plus分页插件的配置与使用 1. 分页插件的配置1.1 MyBatis-Plus依赖配置1.2 MyBatis-Plus分页插件配置 2. 分页插件的使用2.1 理论分析2.1 代码实现 1. 分页插件...
    99+
    2023-08-17
    mybatis java mysql spring boot sql
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作