mybatis插入与批量插入返回ID的原理详解

背景

最近正在整理之前基于mybatis的半ORM框架。原本的框架底层类ORM操作是通过StringBuilder的append拼接的,这次打算用JsqlParser重写一遍,一来底层不会存在太多的文本拼接,二来基于其他开源包维护难度会小一些,最后还可以整理一下原有的冗余方法。

这两天整理insert相关的方法,在将对象插入数据库后,期望是要返回完整对象,并且包含实际的数据库id。

基础相关框架为:spring、mybatis、hikari。

底层调用方法

最底层的做法实际上很直白,就是利用mybatis执行最简单的sql语句,给上代码。

@Repository("baseDao")
public class BaseDao extends SqlSessionDaoSupport {

 private Logger logger = LoggerFactory.getLogger(this.getClass());

 /**
  * 最大的单次批量插入的数量
  */
 private static final int MAX_BATCH_SIZE = 10000;

 @Override
 @Autowired
 public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
  super.setSqlSessionFactory(sqlSessionFactory);
 }

 /**
  * 根据sql方法名称和对象插入数据库
  */
 public Object insert(String sqlName, Object obj) throws SQLException {
  return getSqlSession().insert(sqlName, obj); // 此处直接执行传入的xml中对应的sql id,以及参数

 }
}

单个对象插入

java代码

 /**
  * 简单插入实体对象
  *
  * @param entity 实体对象
  * @throws SQLException
  */
 public <T extends BaseEntity> T insertEntity(T entity) throws SQLException {
  Insert insert = new Insert();
  insert.setTable(new Table(entity.getClass().getSimpleName()));
  insert.setColumns(JsqlUtils.getColumnNameFromEntity(entity.getClass()));
  insert.setItemsList(JsqlUtils.getAllColumnValueFromEntity(entity,insert.getColumns()));

  Map<String, Object> param = new HashMap<>();
  param.put("baseSql", insert.toString());
  param.put("entity", entity);
  this.insert("BaseDao.insertEntity", param);

  return entity;
 }

xml代码

 <insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id">
 ${baseSql}
 </insert>

其他的就不多说了,这里针对如何返回已经入库的id给个说明。

在xml的 insert 标签中,设置 keyProperty 为 对应对象的id字段,和 insert(sqlName, obj) 这个方法中的 obj 是对应的。

这里一般有两种情况:

直接保存实体的对象作为参数传入(给伪代码示例)

SaveObject saveObject = new SaveObject(); // SaveObject中包含字段soid,作为自增id
saveObject.setName("my name");
saveObject.setNums(2);

getSqlSession().insert("saveObject.insert",saveObject);

这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样

 <insert id="insert" parameterType="SaveObject " useGeneratedKeys="true" keyProperty="soid">
 insert into save_object (`name`,nums) values (#{names},#{nums})
 </insert>

这里我们传入了SaveObject实体对象作为参数,所以我们的 keyProperty 就是parameter的id对应的字段,在这里就是 soid 。

多个对象,实体对象作为其中一个对象传入

    Map<String, Object> param = new HashMap<>();
    param.put("baseSql", insert.toString());
    param.put("entity", entity); // 此处对应实体作为map的第二个参数传入
    this.insert("BaseDao.insertEntity", param);
 <insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id">
 ${baseSql}
 </insert>

这里也是比较容易理解,当传入参数是Map时,我们的 keyProperty 对应方式就是先从Map中读出对应value,再指向 value中的id字段。

列表批量插入

批量插入数据有两种做法,一种是多次调用单个insert方法,这种效率较低就不说了。另外一种是 insert into table (cols) values (val1),(val2),(val3) 这样批量插入。

到mybatis中,也是分为两种

直接保存实体的对象作为参数传入(给伪代码示例)

SaveObject saveObject1 = new SaveObject(); // SaveObject中包含字段soid,作为自增id
saveObject1.setName("my name");
saveObject1.setNums(2);

SaveObject saveObject2 = new SaveObject(); // SaveObject中包含字段soid,作为自增id
saveObject2.setName("my name");
saveObject2.setNums(2);

List<SaveObject> saveObjects = new ArrayList<SaveObject>();
saveObjects.add(saveObjects1);
saveObjects.add(saveObjects2);

getSqlSession().insert("saveObject.insertList",saveObjects);

这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样

 <insert id="insertList" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="soid">
    insert into save_object (`name`,nums) values
    <foreach collection="list" index="index" item="saveObject" separator=",">
      (#{saveObject.numsnames}, #{saveObject.nums})
    </foreach>
 </insert>

多个对象,实体对象作为其中一个对象传入

本文的重点来了,我自己卡在这里很久,反复调试才摸清逻辑。接下来就顺着mybatis的思路来讲,只会讲id生成相关的,其他的流程就不多说了。

先看这个类:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator (很多代码我用...代替了,不是特别重要,放在还占地方)

 /**
  * 这个方法是在执行完插入语句之后处理的,两个关键参数
  * 1. MappedStatement ms 里面包含了我们的 keyProperty
  * 2. Object parameter 就是我们inser方法传入的参数
  */
 @Override
 public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
  processBatch(ms, stmt, parameter);
 }

 public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
  final String[] keyProperties = ms.getKeyProperties();
  if (keyProperties == null || keyProperties.length == 0) {
   return;
  }
  try (ResultSet rs = stmt.getGeneratedKeys()) {
   final Configuration configuration = ms.getConfiguration();
   if (rs.getMetaData().getColumnCount() >= keyProperties.length) {
    Object soleParam = getSoleParameter(parameter);
    if (soleParam != null) {
     assignKeysToParam(configuration, rs, keyProperties, soleParam);
    } else {
     assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter);
    }
   }
  } catch (Exception e) {
   ...
  }
 }

 protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties,
   Map<?, ?> paramMap) throws SQLException {
  // Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'.
  int firstDot = keyProperties[0].indexOf('.');
  if (firstDot == -1) {
   ...
  }
  String paramName = keyProperties[0].substring(0, firstDot);
  Object param;
  if (paramMap.containsKey(paramName)) {
   param = paramMap.get(paramName);
  } else {
   ...
  }
  ...
  assignKeysToParam(configuration, rs, modifiedKeyProperties, param);
 }

 private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties,
   Object param)
   throws SQLException {
  final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  final ResultSetMetaData rsmd = rs.getMetaData();
  // Wrap the parameter in Collection to normalize the logic.
  Collection<?> paramAsCollection = null;
  if (param instanceof Object[]) {
   paramAsCollection = Arrays.asList((Object[]) param);
  } else if (!(param instanceof Collection)) {
   paramAsCollection = Arrays.asList(param);
  } else {
   paramAsCollection = (Collection<?>) param;
  }
  TypeHandler<?>[] typeHandlers = null;
  for (Object obj : paramAsCollection) {
   if (!rs.next()) {
    break;
   }
   MetaObject metaParam = configuration.newMetaObject(obj);
   if (typeHandlers == null) {
    typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
   }
   populateKeys(rs, metaParam, keyProperties, typeHandlers);
  }
 }

利用这个代码先解释一下上一节 直接保存实体的对象作为参数传入 为什么id会被更新至实体内的soid字段。

上一节的是 keyProperty="soid"

我们来看19行的代码Object soleParam = getSoleParameter(parameter); ,当我们传入的对象是List的时候 soleParam != null,所以 直接执行 assignKeysToParam 方法。

注意64和65行

for (Object obj : paramAsCollection) {
if (!rs.next()) {

paramAsCollection 是将我们传入的转换为 Collection 类型,所以这里是循环我们的给定实体列表参数。

rs就是ResultSet,就是插入之后的结果集。 rs.next()就是指针指向下一条记录,所以实际上这里是同步循环,将结果集中的id直接设置到我们给的实体列表中

我们现在来看看多参数插入是会有什么问题。

Java方法:

  /**
   * 简单批量插入实体对象
   *
   * @param entitys
   * @throws SQLException
   */
  public List insertEntityList(List<? extends BaseEntity> entitys) throws SQLException {
    if (entitys == null || entitys.size() == 0) {
      return null;
    }

    Insert insert = new Insert();
    insert.setTable(new Table(entitys.get(0).getClass().getSimpleName()));
    insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass()));
    MultiExpressionList multiExpressionList = new MultiExpressionList();
    entitys.stream().map(e -> JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e -> multiExpressionList.addExpressionList(e));
    insert.setItemsList(multiExpressionList);

    Map<String, Object> param = new HashMap<>();
    param.put("baseSql", insert.toString());
    param.put("list", entitys);
    this.insert("BaseDao.insertEntityList", param);
    return entitys;
  }

Xml:

 <insert id="insertEntityList" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="id">
 ${baseSql}
 </insert>

会有什么问题??根据这样的xml,最后的结果是我们传入的map中会多一个key 叫 “id”,里面存的是一个插入的实体的id。

因为根据源码 Map并非 Collection 类型,所以会做为只有一个元素的数组传入,在刚才同步循环的地方就只会循环一次,把结果集中第一条数据的id放进map中,循环就结束了。

怎么解决呢??

解决的方法就在 assignKeysToOneOfParams 这个方法,方法名其实已经说了,将主键赋给其中一个参数,这里确实也是取了其中的一个参数进行赋值主键。所以我们只要能够跳转到这个方法就好。所以需要满足 getSoleParameter(parameter) == null ,点进代码看

private Object getSoleParameter(Object parameter) {
  if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) {
   return parameter;
  }
  Object soleParam = null;
  for (Object paramValue : ((Map<?, ?>) parameter).values()) {
   if (soleParam == null) {
    soleParam = paramValue;
   } else if (soleParam != paramValue) {
    soleParam = null;
    break;
   }
  }
  return soleParam;
 }

要返回null,条件是这样:

  • 参数是ParamMap或者 StrictMap
  • 参数大于两个,且第一个和后面任意一个不相等

所以解决方案出炉,很简单,只需要改动代码两个地方即可。

  /**
   * 简单批量插入实体对象
   *
   * @param entitys
   * @throws SQLException
   */
  public List insertEntityList(List<? extends BaseEntity> entitys) throws SQLException {
    if (entitys == null || entitys.size() == 0) {
      return null;
    }

    Insert insert = new Insert();
    insert.setTable(new Table(entitys.get(0).getClass().getSimpleName()));
    insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass()));
    MultiExpressionList multiExpressionList = new MultiExpressionList();
    entitys.stream().map(e -> JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e -> multiExpressionList.addExpressionList(e));
    insert.setItemsList(multiExpressionList);

    Map<String, Object> param = new MapperMethod.ParamMap<>(); // 这里替换为 MapperMethod.ParamMap 类型
    param.put("baseSql", insert.toString());
    param.put("list", entitys);
    this.insert("BaseDao.insertEntityList", param);
    return entitys;
  }

Xml:

 <insert id="insertEntityList" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="list.id"> <!-- 这里是map中的key.实体id -->
 ${baseSql}
 </insert>

完成

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Mybatis返回插入主键id的方法

    在mapper的xml文件中配置  useGeneratedKeys 以及 keyProperty 返回Id即可 <insert id="insertObject" useGeneratedKeys="true" keyProperty="id" parameterType="www.change.tm.model.Orders" > insert into orders <trim prefix="

  • MyBatis+MySQL 返回插入的主键ID的方法

    需求:使用MyBatis往MySQL数据库中插入一条记录后,需要返回该条记录的自增主键值. 方法:在mapper中指定keyProperty属性,示例如下: <insert id="insertAndGetId" useGeneratedKeys="true" keyProperty="userId" parameterType="com.chenzhou.mybatis.User"> insert into us

  • MyBatis insert操作插入数据之后返回插入记录的id

    MyBatis插入数据的时候,返回该记录的id <insert id="insert" keyProperty="id" useGeneratedKeys="true"
 parameterType="com.demo.domain.CountRateConfig">
 insert into query_rate_config (code,partner_type,search_count, booking_co

  • MyBatis在insert插入操作时返回主键ID的配置(推荐)

    很多时候,在向数据库插入数据时,需要保留插入数据的id,以便进行后续的update操作或者将id存入其他表作为外键. 但是,在默认情况下,insert操作返回的是一个int值,并且不是表示主键id,而是表示当前SQL语句影响的行数... 接下来,我们看看MyBatis如何在使用MySQL和Oracle做insert插入操作时将返回的id绑定到对象中. MySQL用法: <insert id="insert" parameterType="com.test.User&qu

  • MyBatis获取插入记录的自增长字段值(ID)

    第一步: 在Mybatis Mapper文件中添加属性"useGeneratedKeys"和"keyProperty",其中keyProperty是Java对象的属性名! <insert id="insert" parameterType="Spares" useGeneratedKeys="true" keyProperty="id"> insert into spares

  • mybatis插入与批量插入返回ID的原理详解

    背景 最近正在整理之前基于mybatis的半ORM框架.原本的框架底层类ORM操作是通过StringBuilder的append拼接的,这次打算用JsqlParser重写一遍,一来底层不会存在太多的文本拼接,二来基于其他开源包维护难度会小一些,最后还可以整理一下原有的冗余方法. 这两天整理insert相关的方法,在将对象插入数据库后,期望是要返回完整对象,并且包含实际的数据库id. 基础相关框架为:spring.mybatis.hikari. 底层调用方法 最底层的做法实际上很直白,就是利用my

  • 基于Java 利用Mybatis实现oracle批量插入及分页查询

    目录 1.单条数据insert 2.批量数据批量insert 3.创建序列 4.oracle分页查询 前端与后端交互,分页查询 后端海量数据导出,批量查询 1.单条数据insert <!--简单SQL--> insert into userinfo (USERID, USERNAME, AGE) values(1001,'小明',20); <!--Mybatis写法1,有序列,主键是自增ID,主键是序列--> <insert id="insert" par

  • 强烈推荐MyBatis 三种批量插入方式的比较

    目录 前言 代码 拼接SQL的xml Service类 测试类 测试结果 结论 前言 数据库使用的是SQLServer,JDK版本1.8,运行在SpringBoot环境下 对比3种可用的方式: 反复执行单条插入语句 xml拼接sql 批处理执行 先说结论:少量插入请使用反复插入单条数据,方便.数量较多请使用批处理方式.(可以考虑以有需求的插入数据量20条左右为界吧,在我的测试和数据库环境下耗时都是百毫秒级的,方便最重要) 无论何时都不用xml拼接sql的方式. 代码 拼接SQL的xml newI

  • Mybatis Plus 实现批量插入的示例代码

    目录 一. 添加依赖 二. 继承默认方法注入 三. 在 MybatisPlusConfig 配置文件中注入 Bean 四. 扩展自带 BaseMapper 五. 业务层面实现 Mybatis Plus 的 IService 接口中提供了批量插入的方法,然而,它的内部实现逻辑竟然是这样的: 居然是循环单条插入?!逗人玩嘛,好吧,自己动手,丰衣足食. 一. 添加依赖 <!--mybatis plus extension,包含了mybatis plus core--> <dependency&

  • InnoDB的关键特性-插入缓存,两次写,自适应hash索引详解

    InnoDB存储引擎的关键特性包括插入缓冲.两次写(double write).自适应哈希索引(adaptive hash index).这些特性为InnoDB存储引擎带来了更好的性能和更高的可靠性. 插入缓冲 插入缓冲是InnoDB存储引擎关键特性中最令人激动的.不过,这个名字可能会让人认为插入缓冲是缓冲池中的一个部分.其实不然,InnoDB缓冲池中有Insert Buffer信息固然不错,但是Insert Buffer和数据页一样,也是物理页的一个组成部分. 主键是行唯一的标识符,在应用程序

  • mybatis实现对数据的增删查改实例详解

    前期准备 新建java工程或java wweb工程,需要导入以下的包, 基本工作已经完成,接下来开始进入正题. 新建实体类 新建与数据库表对应的实体类 package com.edu.hpu.domain; /** * @author Administrator *user表所对应的实体类 */ public class User { //实体类的属性和表的字段名称一一对应 private int id; private String name; private int age; //对属性进行

  • Mybatis中的resultType和resultMap查询操作实例详解

    resultType和resultMap只能有一个成立,resultType是直接表示返回类型的,而resultMap则是对外部ResultMap的引用,resultMap解决复杂查询是的映射问题.比如:列名和对象属性名不一致时可以使用resultMap来配置:还有查询的对象中包含其他的对象等. MyBatisConfig.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configura

  • mybatis的mapper.xml中resultMap标签的使用详解

    1.前言 最近博主在做一个ssm框架的共享汽车管理系统,其中,数据库字段设计的有下划线方式,a_username,然后在写mapper.xml里面的sql语句的时候,一直出现查询语句查询的值为null的情况.或者是resultMap标签和驼峰规则不太明白的同学,可以看这里. 于是顺便梳理一下. 2.关于resultMap 2.1.什么是resultMap? 在mybatis中有一个resultMap标签,它是为了映射select查询出来结果的集合,其主要作用是将实体类中的字段与数据库表中的字段进

  • Mybatis开发要点-resultType和resultMap有什么区别详解

    目录 一.resultType 1.resultType介绍 2.映射规则 3.自动映射注意事项 4.代码演示 1.t_user_test.sql准备 2.实体类 3.Mapper接口类 4.Mapper xml 5.配置文件 6.启动测试类 7.执行结果 二.resultMap 1.resultMap  介绍 2.resultMap属性 3.使用场景 4.resultMap子元素属性 5.代码演示 1.mapper接口 2.Mapper.xml 3.启动测试 4.执行结果 三.结论 Mybat

  • Java MyBatis本地缓存原理详解

    目录 背景 发现问题 复现 解决问题 探究缓存的原理 Sql查询部分深入 初见缓存 告一段落 番外篇-Myabtis创建CacheKey的算法. 构造方法 结束语 背景 出现了一次生产事故,事情是这样的,我们有一个项目,Java访问数据库的框架使用的是MyBatis.然后一个业务员在系统中查询了一个订单,发现这个订单是未支付的状态,于是业务员联系客户,让客户支付,客户支付完成后,业务员又去系统查询,结果还是未支付状态,刷新了页面也是一样,不过过了一会就好了.业务员把这个延迟问题,反馈给了我们.我

随机推荐