Mybatis Plus 3.X版本的insert填充自增id的IdType.ID_WORKER策略源码分析

总结/朱季谦

某天同事突然问我,你知道Mybatis Plus的insert方法,插入数据后自增id是如何自增的吗?

我愣了一下,脑海里只想到,当在POJO类的id设置一个自增策略后,例如@TableId(value = "id",type = IdType.ID_WORKER)的注解策略时,就能实现在每次数据插入数据库时,实现id的自增,例如以下形式——

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "用户对象")
@TableName("user_info")
public class UserInfo {
    @ApiModelProperty(value = "用户ID", name = "id")
    @TableId(value = "id",type = IdType.ID_WORKER)
    private Integer id;
    @ApiModelProperty(value = "用户姓名", name = "userName")
    private String userName;
    @ApiModelProperty(value = "用户年龄", name = "age")
    private int age;
}

但是,说实话,我一直都没能理解,这个注解策略实现id自增的底层原理究竟是怎样的?

带着这样的疑惑,我开始研究了一番Mybatis Plus的insert自增id的策略源码,并将其写成了本文。

先来看一下Mybatis Plus生成id的自增策略,可以通过枚举IdType设置以下数种策略——

@Getter
public enum IdType {
    /**
     * 数据库ID自增
     */
    AUTO(0),
    /**
     * 该类型为未设置主键类型
     */
    NONE(1),
    /**
     * 用户输入ID
     * 该类型可以通过自己注册自动填充插件进行填充
     */
    INPUT(2),

    /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
    /**
     * 全局唯一ID (idWorker)
     */
    ID_WORKER(3),
    /**
     * 全局唯一ID (UUID)
     */
    UUID(4),
    /**
     * 字符串全局唯一ID (idWorker 的字符串表示)
     */
    ID_WORKER_STR(5);
    
    ......
}

每个字段都有各自含义,说明如下:

  1. AUTO(0): 用于数据库ID自增的策略,主要用于数据库表的主键,在插入数据时,数据库会自动为新插入的记录分配一个唯一递增ID。
  2. NONE(1): 表示未设置主键类型,存在某些情况下不需要主键,或者主键由其他方式生成。
  3. INPUT(2): 表示用户输入ID,允许用户自行指定ID值,例如前端传过来的对象id=1,就会根据该自行定义的id=1当作ID值;
  4. ID_WORKER(3): 表示全局唯一ID,使用的是idWorker算法生成的ID,这是一种雪花算法的改进。
  5. UUID(4): 表示全局唯一ID,使用的是UUID(Universally Unique Identifier)算法。
  6. ID_WORKER_STR(5): 表示字符串形式的全局唯一ID,这是idWorker生成的ID的字符串表示形式,便于在需要字符串ID的场景下使用。

接下来,让我们跟着源码看一下,究竟是如何基于这些ID策略做id自增的,本文主要以ID_WORKER(3)策略id来追踪。

先从插入insert方法开始。

基于前文创建的UserInfo类,我们写一个test的方法,用于追踪insert方法——

@Test
public void test(){
    UserInfo userInfo = new UserInfo();
    userInfo.setUserName("用户名");
    userInfo.setAge(1);
    userInfoMapper.insert(userInfo);
}

可以看到,此时的id=0,还没有任何值——

image

执行到insert的时候,底层会执行一个动态代理,最终通过动态代理,执行DefaultSqlSession类的insert方法,可以看到,insert方法里,最终调用的是一个update方法。

image

在mybatis中,无论是新增insert或者更新update,其底层都是统一调用DefaultSqlSession的update方法——

@Override
public int update(String statement, Object parameter) {
  try {
    dirty = true;
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.update(ms, wrapCollection(parameter));
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

执行到executor.update(ms, wrapCollection(parameter))方法时,会跳转到BaseExecutor的update方法里——

image

这里的BaseExecutor是mybatis的核心组件,它是Executor 接口的一个具体实现,提供了实际数据的增删改查操作功能。在 MyBatis 中,基于BaseExecutor扩展了以下三种基本执行器类:

  1. SimpleExecutor:这是最简单的执行器类型,它对每个数据库CURD操作都创建一个新的 Statement 对象。如果应用程序执行大量的数据库操作,这种类型的执行器可能会产生大量的开销,因为它不支持 Statement 重用。
  2. ReuseExecutor:这种执行器类型会尝试重用 Statement 对象。它在处理多个数据库操作时,会尝试使用相同的 Statement 对象,从而减少创建 Statement 对象的次数,提高性能。
  3. BatchExecutor:这种执行器类型用于批量操作,它会在内部缓存所有的更新操作,然后在适当的时候一次性执行它们,适合批量插入或更新操作的场景,可以显著提高性能。

除了这三种基本的执行器类型,MyBatis 还提供了其他一些执行器,这里暂时不展开讨论。

在本文中,执行到doUpdate(ms, parameter)时,会默认跳转到SimpleExecutor执行器的doUpdate方法里。注意我标注出来的这两行代码,自动填充插入ID策略的逻辑,就是在这两行代码当中——

image

先来看第一行代码,从类名就可以看出,这里创建里一个实现StatementHandler接口的对象,这个StatementHandler接口专门用来处理SQL语句的接口。从这里就可以看出,通过创建这个对象,可以专门用来处理SQL相关语句操作,例如,对参数的设置,更具体一点,可以对参数id进行自定义设置等功能。

实现StatementHandler接口有很多类,那么,具体需要创建哪个对象呢?

跟着代码一定进入到RoutingStatementHandler类的RoutingStatementHandler方法当中,可以看到,这里有一个switch,debug到这一步,最终创建的是一个PreparedStatementHandler对象——

image

进入到PreparedStatementHandler方法当中,可以看到会通过super调用创建其父类的构造器方法——

public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}

从super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql)方法进去,到父类的BaseStatementHandler里,这里面有一行很关键的代码 this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql),这是一个MyBatis内部的接口或实现类的实例,用于处理SQL的参数映射和传递。

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  this.configuration = mappedStatement.getConfiguration();
  this.executor = executor;
  this.mappedStatement = mappedStatement;
  this.rowBounds = rowBounds;

  this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  this.objectFactory = configuration.getObjectFactory();

  if (boundSql == null) { // issue #435, get the key before calculating the statement
    generateKeys(parameterObject);
    boundSql = mappedStatement.getBoundSql(parameterObject);
  }

  this.boundSql = boundSql;

  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

进入到configuration.newParameterHandler(mappedStatement, parameterObject, boundSql)代码里,可以看到这里通过createParameterHandler方法创建一个实现ParameterHandler接口的对象,至于这个对象是什么,可以接着往下去。

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

最终来到MybatisXMLLanguageDriver类的createParameterHandler方法,可以看到,创建的这个实现ParameterHandler接口的对象,是这个MybatisDefaultParameterHandler。

public class MybatisXMLLanguageDriver extends XMLLanguageDriver {

    @Override
    public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject,
                                                   BoundSql boundSql) {
        /* 使用自定义 ParameterHandler */
        return new MybatisDefaultParameterHandler(mappedStatement, parameterObject, boundSql);
    }
}

继续跟进去,可以看到构造方法里,有一个processBatch(mappedStatement, parameterObject)方法,我们要找的填充自增id的IdType.ID_WORKER策略实现,其实就在这个processBatch方法里。

public MybatisDefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    super(mappedStatement, processBatch(mappedStatement, parameterObject), boundSql);
    this.mappedStatement = mappedStatement;
    this.configuration = mappedStatement.getConfiguration();
    this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
    this.parameterObject = parameterObject;
    this.boundSql = boundSql;
}

至于processBatch(mappedStatement, parameterObject)中的两个参数分别是什么,debug就知道了,mappedStatement是一个存储执行语句相关的Statement对象,而parameterObject则是需要插入数据库的对象数据,此时id仍然是默认0,相当还没有值。

image

继续往下debug,因为是insert语句,故而会进入到ms.getSqlCommandType() == SqlCommandType.INSERT方法里,将isFill赋值true,isInsert赋值true,这两个分别表示是否需要填充以及是否插入。由此可见,它将会执行if (isFill) {}里的逻辑——

image

在if(isFill)方法当中,最重要的是populateKeys(metaObjectHandler, tableInfo, ms, parameterObject, isInsert);这个方法,这个方法就是根据不同的id策略,去生成不同的id值,然后填充到id字段里,最终插入到数据库当中。而我们要找的最终方法,正是在这里面——

protected static Object populateKeys(MetaObjectHandler metaObjectHandler, TableInfo tableInfo,
                                     MappedStatement ms, Object parameterObject, boolean isInsert) {
    if (null == tableInfo) {
        /* 不处理 */
        return parameterObject;
    }
    /* 自定义元对象填充控制器 */
    MetaObject metaObject = ms.getConfiguration().newMetaObject(parameterObject);
    // 填充主键
    if (isInsert && !StringUtils.isEmpty(tableInfo.getKeyProperty())
        && null != tableInfo.getIdType() && tableInfo.getIdType().getKey() >= 3) {
        Object idValue = metaObject.getValue(tableInfo.getKeyProperty());
        /* 自定义 ID */
        if (StringUtils.checkValNull(idValue)) {
            if (tableInfo.getIdType() == IdType.ID_WORKER) {
                metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId());
            } else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) {
                metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr());
            } else if (tableInfo.getIdType() == IdType.UUID) {
                metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID());
            }
        }
    }
    if (metaObjectHandler != null) {
        if (isInsert && metaObjectHandler.openInsertFill()) {
            // 插入填充
            metaObjectHandler.insertFill(metaObject);
        } else if (!isInsert) {
            // 更新填充
            metaObjectHandler.updateFill(metaObject);
        }
    }
    return metaObject.getOriginalObject();
}

例如,我们设置的id策略是这个 @TableId(value = "id",type = IdType.ID_WORKER),当代码执行到populateKeys方法里时,就会判断是否为 IdType.ID_WORKER策略,如果是,就会执行对应的生存id的方法。这里的IdWorker.getId()就是获取一个唯一ID,然后赋值给tableInfo.getKeyProperty(),这个tableInfo.getKeyProperty()正是user_info的对象id。

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/787637.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

展开说说:Android服务之实现AIDL跨应用通信

前面几篇总结了Service的使用和源码执行流程,这里再简单分析一下如果需要Service跨进程通信该怎样做。AIDL(Android Interface Definition Language)Android接口定义语言,用于实现 Android 两个进程之间进行进程间通信&#xff08…

计算机网络之WPAN 和 WLAN

上一篇文章内容:无线局域网 1.WPAN(无线个人区域网) WPAN 是以个人为中心来使用的无线个人区域网,它实际上就是一个低功率、小范围、低速率和低价格的电缆替代技术。 (1) 蓝牙系统(Bluetooth) &#…

新闻资讯整合平台:一站式满足企业信息需求

摘要: 面对信息爆炸的时代,企业如何在海量数据中快速获取有价值资讯,成为提升竞争力的关键。本文将探讨如何通过一站式新闻资讯整合平台,实现企业信息需求的全面满足,提升决策效率,同时介绍实用工具推荐&a…

开源数据科学平台Anaconda简介

开源数据科学平台Anaconda简介 零、时光宝盒 最近,某金融行业女性选择以跳楼的形式结束自己的生命,这件不幸的事情成了热门话题,各种猜测的都有,有些人评论的话真的很过分。我想起前段时间看到的,有个女学生跳江&#…

ISO/OSI七层模型

ISO:国际标准化/ OSI:开放系统互联 七层协议必背图 1.注意事项: 1.上三层是为用户服务的,下四层负责实际数据传输。 2.下四层的传输单位: 传输层; 数据段(报文) 网络层: 数据包(报…

git撤销/返回到某次提交(idea工具 + gitbush)

不多说废话,直接展示使用。 方法一:使用idea工具进行返回 准备某次过度提交 使用idea打开git log 找到要回去的版本 点击右键选到reset 模式选hard,强制回滚 这个时候本地代码已经回归你指定的版本了。 这个时候再进行强制推送&#xff0c…

读书笔记-Java并发编程的艺术-第4章(Java并发编程基础)-第3节(线程间通信)

文章目录 4.3 线程间通信4.3.1 volatile和synchronized 关键字4.3.2 等待/通知机制4.3.3 等待/通知的经典范式4.3.4 管道输入 / 输出流4.3.5 Thread.join()的使用4.3.6 ThreadLocal的使用 4.3 线程间通信 线程开始运行,拥有自己的栈空间,就如同一个脚本…

APP项目测试 之 APP性能测试

性能指标描述:一定是某种时间内某种条件执行某种操作,性能指标如何? 性能测试可以考虑和稳定性结合,monkey测试时使用性能监控工具监控性能数据。 例如: 2小时内持续刷新操作,性能如何? 持续运行8小时,性能如何? 常见…

【MySQL】详解

SQL语句的分类: 1.DDL(Data Definition Languages)语句: 数据定义语言 ,这些语句定义了不同的数据段,数据库,表,列,索引等数据库对象的定义。常用的语句关键字主要包括…

随笔(一)

1.即时通信软件原理(发展) 即时通信软件实现原理_即时通讯原理-CSDN博客 笔记: 2.泛洪算法: 算法介绍 | 泛洪算法(Flood fill Algorithm)-CSDN博客 漫水填充算法实现最常见有四邻域像素填充法&#xf…

Studio One直播声音怎么调 Studio One直播没有声音输出怎么办 studio one如何设置声音变好听

Studio One做为新生代音乐工作站,凭借更低的价格和完备的功能,获得了音乐人和直播行业工作者的青睐,尤其是对硬件声卡的适配支持更好,特别适合用来配合线上教学和电商带货。 一、Studio One直播声音怎么调 在Studio One进行直播时…

AdaBoost集成学习算法理论解读以及公式为什么这么设计?

本文致力于阐述AdaBoost基本步骤涉及的每一个公式和公式为什么这么设计。 AdaBoost集成学习算法基本上遵从Boosting集成学习思想,通过不断迭代更新训练样本集的样本权重分布获得一组性能互补的弱学习器,然后通过加权投票等方式将这些弱学习器集成起来得到…

P8306 【模板】字典树

题目描述 给定 n 个模式串 s1​,s2​,…,sn​ 和 q 次询问,每次询问给定一个文本串 ti​,请回答 s1​∼sn​ 中有多少个字符串 sj​ 满足 ti​ 是 sj​ 的前缀。 一个字符串 t 是 s 的前缀当且仅当从 s 的末尾删去若干个(可以为 0 个&#…

Scissor算法-从含有表型的bulkRNA数据中提取信息进而鉴别单细胞亚群

在做基础实验的时候,研究者都希望能够改变各种条件来进行对比分析,从而探索自己所感兴趣的方向。 在做数据分析的时候也是一样的,我们希望有一个数据集能够附加了很多临床信息/表型,然后二次分析者们就可以进一步挖掘。 然而现实…

【深度学习基础】MacOS PyCharm连接远程服务器

目录 一、需求描述二、建立与服务器的远程连接1. 新版Pycharm的界面有什么不同?2. 创建远程连接3. 建立本地项目与远程服务器项目之间的路径映射4.设置保存自动上传文件 三、设置解释器总结 写在前面,本人用的是Macbook Pro, M3 MAX处理器&am…

【Linux】多线程_2

文章目录 九、多线程2. 线程的控制 未完待续 九、多线程 2. 线程的控制 主线程退出 等同于 进程退出 等同于 所有线程都退出。为了避免主线程退出,但是新线程并没有执行完自己的任务的问题,主线程同样要跟进程一样等待新线程返回。 pthread_join 函数…

接口测试(3)

接口自动化 # 获取图片验证码import requestsresponse requests.get(url"http://kdtx-test.itheima.net/api/captchaImage")print(response.status_code) print(response.text) import requestsurl "http://kdtx-test.itheima.net/api/login" header_da…

ffmpeg滤镜-drawtext-命令行

使用 FFmpeg 在视频上添加文字可以通过 drawtext 滤镜来实现。这个滤镜允许你指定字体、大小、颜色、位置等。 基本用法 以下命令将 "Hello, World!" 添加到视频的顶部左侧: ffmpeg -i input.mp4 -vf "drawtexttextHello, World\!:fontcolorwhite…

使用redis进行短信登录验证(验证码打印在控制台)

使用redis进行短信登录验证 一、流程1. 总体流程图2. 流程文字讲解:3.代码3.1 UserServiceImpl:(难点)3.2 拦截器LoginInterceptor:3.3 拦截器配置类: 4 功能实现,成功存入redis (黑…

飞速(FS)10G光模块选择指南

飞速(FS)的10G SFP光模块专为万兆每秒(10 Gbps)的数据传输设计,满足多样化网络需求。该光模块支持多种传输距离,具备热插拔和数字诊断监控功能,全面适配200品牌,为客户提供更灵活的选…