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