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,回写到实例对象中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值