MyBatis: 批量保存实例返回主键id

本文详细分析了MyBatis在Spring事务下执行批量插入操作时,如何利用Jdbc3KeyGenerator回写主键ID的过程。从SimpleExecutor开始,经过SqlSessionTemplate、SqlSessionInterceptor、DefaultSqlSession、BaseExecutor、SimpleExecutor等步骤,最终在PreparedStatementHandler的update方法中执行SQL并获取自增ID。Jdbc3KeyGenerator根据数据库返回的第一个自增ID和受影响的行数,依次计算出所有插入行的主键值,并回写到对应的对象实例中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主键ID是设定AUTO_INCREMENT , 当插入对象数据成功后,会给该对象回写id值。

 <insert id="patchInsert" useGeneratedKeys="true"  keyProperty="id" parameterType="java.util.List">
        insert into invoice( invoice_code, invoice_no) values
        <foreach item="item" collection="list" separator=",">
          ( #{item.invoiceCode}, #{item.invoiceNo}  )
        </foreach>
    </insert>

批量插入对象集合数据时,当PreparedStatement#execute执行sql之后的过程Jdbc3KeyGenerator#processAfter里:

getLastInsertID()拿到的是Resultset#getUpdateID里保存数据库插入数据成功返回的第一条记录的id,该id值就是首条插入行auto_increment值。拿到受影响行数,然后依次 'id+= auto_increment' 即可得到所有的主键值了,按rows数组index匹配对应的数据实例对象进行回写。 (insert sql是按入参List内记录顺序组装)

源码分析

mybatis(8) - spring 事务下 mybatis 的执行过程https://my.oschina.net/u/3434392/blog/3010626

在上面文章里简要描述了一个sql方法在MyBatis的执行过程。

这个过程从SimpleExecutor开始。受到 Configuration.defaultExecutorType = ExecutorType.SIMPLE 的影响, 如果设定为 ExecutorType.BATCH 则 Configuration#newExecutor 创建的是BatchExecutor 。 默认开启cacheEnabled,外层用CachingExecutor裹着(这里真正使用二级缓存还需要其他配置配合)。 

xxxMapper#insert (xxxMapper是由MapperFactoryBean使用MapperProxyFactory创建的MapperProxy代理对象) -> MapperProxy#invoke -> MapperMethod#execute 
​​​​​​-> SqlSessionTemplate#insert(内部持有代理类sqlSessionProxy, 它的InvocationHandler是SqlSessionInterceptor) -> SqlSessionInterceptor#invoke  -> DefaultSqlSession#insert -> BaseExecutor#update -> SimpleExecutor#doUpdate (通过Configuration创建RoutingStatementHandler,内部持有PreparedStatementHandler; 再获取到Connection; 使用Connection创建PreparedStatement(绑定了sql); PreparedStatementHandler给PreparedStatement使用TypeHandler设置入参值) -> StatementHandler#update -> PreparedStatement#execute

 这里主要看PreparedStatementHandler#update 的逻辑

// PreparedStatementHandler 
 @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute(); // 执行sql
    int rows = ps.getUpdateCount(); // 影响到的记录行数
    Object parameterObject = boundSql.getParameterObject(); // 入参实例对象
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); // Jdbc3KeyGenerator
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject); // 回写id
    return rows;
  }

插入后回写id

//  Jdbc3KeyGenerator#processAfter 实际的逻辑
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()) { // 先确定需要回写的id集合
      final ResultSetMetaData rsmd = rs.getMetaData();
      final Configuration configuration = ms.getConfiguration();
      if (rsmd.getColumnCount() < keyProperties.length) {
        // Error?
      } else {
        assignKeys(configuration, rs, rsmd, keyProperties, parameter); // 匹配数组index队形的对象,回写id属性
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    }
  }

 KeyGenerator 是在创建 MappedStatement 时 判定是 SqlCommandType.INSERT 指定为 Jdbc3KeyGenerator。

这里分析一下sql执行完毕后的过程Jdbc3KeyGenerator#processAfter。

  • JDBC的 com.mysql.jdbc.StatementImpl#getGeneratedKeys: 

批量插入对象集合数据时,#getLastInsertID()拿到的是Resultset#getUpdateID里保存数据库插入成功返回的第一个id。该id值是首条插入行的数据库表自增值。拿到受影响行数,然后依次 'id+= auto_increment' 即可得到所有的主键值了。

//第一个插入行的AUTO_INCREMENT值, 拿到受影响行数,然后依次获取id。 
public synchronized ResultSet getGeneratedKeys() throws SQLException {
  if (!this.retrieveGeneratedKeys) {
   throw SQLError.createSQLException(Messages.getString("Statement.GeneratedKeysNotRequested"), "S1009", this.getExceptionInterceptor());
  } else if (this.batchedGeneratedKeys == null) {
   // 批量走这边的逻辑
   return this.lastQueryIsOnDupKeyUpdate ? this.getGeneratedKeysInternal(1) : this.getGeneratedKeysInternal();
  } else {
   Field[] fields = new Field[]{new Field("", "GENERATED_KEY", -5, 17)};
   fields[0].setConnection(this.connection);
   return ResultSetImpl.getInstance(this.currentCatalog, fields, new RowDataStatic(this.batchedGeneratedKeys), this.connection, this, false);
  }
 }
 
 protected ResultSet getGeneratedKeysInternal() throws SQLException {
    // 获取影响的行数
    int numKeys = this.getUpdateCount();
    return this.getGeneratedKeysInternal(numKeys);
  }

   protected ResultSetInternalMethods getGeneratedKeysInternal(long numKeys) throws SQLException {
        try {
            synchronized(this.checkClosed().getConnectionMutex()) {
                String encoding = this.session.getServerSession().getCharacterSetMetadata();
                int collationIndex = this.session.getServerSession().getMetadataCollationIndex();
                Field[] fields = new Field[]{new Field("", "GENERATED_KEY", collationIndex, encoding, MysqlType.BIGINT_UNSIGNED, 20)};
                ArrayList<Row> rowSet = new ArrayList();
                long beginAt = this.getLastInsertID(); // //第一条insert数据的id开始值
                if (this.results != null) {
                    String serverInfo = this.results.getServerInfo();
                    if (numKeys > 0L && this.results.getFirstCharOfQuery() == 'R' && serverInfo != null && serverInfo.length() > 0) {
                        numKeys = this.getRecordCountFromInfo(serverInfo);
                    }

                    if (beginAt != 0L && numKeys > 0L) {
                        for(int i = 0; (long)i < numKeys; ++i) {
                            byte[][] row = new byte[1][];
                            if (beginAt > 0L) {
                                row[0] = StringUtils.getBytes(Long.toString(beginAt));
                            } else {
                                byte[] asBytes = new byte[]{(byte)((int)(beginAt >>> 56)), (byte)((int)(beginAt >>> 48)), (byte)((int)(beginAt >>> 40)), (byte)((int)(beginAt >>> 32)), (byte)((int)(beginAt >>> 24)), (byte)((int)(beginAt >>> 16)), (byte)((int)(beginAt >>> 8)), (byte)((int)(beginAt & 255L))};
                                BigInteger val = new BigInteger(1, asBytes);
                                row[0] = val.toString().getBytes();
                            }

                          // 递增+1 塞到rowSet里, 会按index匹配实例对象集合数据                            
                            rowSet.add(new ByteArrayRow(row, this.getExceptionInterceptor()));
                            beginAt += (long)this.connection.getAutoIncrementIncrement(); // 自动递增值为1 
                        }
                    }
                }

                ResultSetImpl gkRs = this.resultSetFactory.createFromResultsetRows(1007, 1004, new ResultsetRowsStatic(rowSet, new DefaultColumnDefinition(fields)));
                return gkRs;
            }
        } catch (CJException var18) {
            throw SQLExceptionsMapping.translateException(var18, this.getExceptionInterceptor());
        }
    }

 

  •  Jdbc3KeyGenerator#assignKeys -> #assignKeysToParamMap

将得到的id数组按index匹配rows,回写到实例对象中。

### 使用 MyBatis Plus 批量插入并回填自增 ID #### 配置 `useGeneratedKeys` 和 `keyProperty` 为了使 MyBatis Plus 支持批量插入时回填自增 ID,在 XML 映射文件中的 SQL 插入语句需指定两个重要属性:`useGeneratedKeys="true"` 以及 `keyProperty="id"`。前者告知 JDBC 获取数据库生成的主键,后者则指明实体类中用于存储此主键值的字段名称[^3]。 ```xml <insert id="batchInsertWithIdBackfill" useGeneratedKeys="true" keyProperty="id"> INSERT INTO table_name (column1, column2,...) VALUES <foreach collection="list" item="item" separator=","> (#{item.column1}, #{item.column2},...) </foreach> </insert> ``` 上述代码片段展示了如何定义一个可以接受列表参数并通过循环结构来构建多个值集的方法,从而完成一次性的多条记录插入操作的同时确保每个新创建的对象都能获得其对应的唯一标识符[^1]。 #### Java 代码示例 在服务层编写相应的业务逻辑处理函数: ```java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; @Service public class YourServiceImpl extends ServiceImpl<YourMapper, YourEntity> { public boolean batchSave(List<YourEntity> entities){ try { saveBatch(entities); return true; } catch (Exception e) { log.error("Failed to perform batch insertion with id backfill", e); return false; } } } ``` 这里调用了来自父类 `ServiceImpl` 提供的一个便捷方法 `saveBatch()` 来执行实际的数据保存动作,并且自动实现了对传入集合内每一个实例对象的主键赋值过程[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值