回答
支持, Mybatis 支持两种批量操作。
foreach 标签
foreach 主要用在构建 in 条件中,它可以在 SQL 语句中遍历一个集合。foreach 标签的属性主要有 item,index,collection,open,separator,close。
- item:表示集合中每一个元素进行迭代时的别名,随便起的变量名。
- index:指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用。
- open:表示该语句以什么开始,常用“(”。
- separator:表示在每次进行迭代之间以什么符号作为分隔符,常用“,”。
- close:表示以什么结束,常用“)”。
- collection:指定要遍历的集合或数组。
public interface UserMapper {
void batchInsertUser(@Param("userList") List<User> userList);
void batchUpdateEmail(@Param("userList") List<User> userList);
}
<insert id="batchInsertUsers">
INSERT INTO t_user (id, name, email)
VALUES
<foreach collection="userList" item="user" separator=",">
(#{user.id}, #{user.name}, #{user.email})
</foreach>
</insert>
<update id="batchUpdateEmails">
<foreach collection="userList" item="user" separator=";">
UPDATE t_user
SET email = #{user.email}
WHERE id = #{user.id}
</foreach>
</update>
ExectorType.BATCH
Mybatis 内置的 ExecutorType 有 3 种,默认为 SIMPLE。
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
- SIMPLE:简单执行器,为每个语句的执行创建一个新的预处理语句,单条提交 sql。
- REUSE:可重用执行器,执行 SQL 语句时会先检查是否有可以重用的 Statement 对象,如果有则重用,没有则创建新的。
- BATCH:批量执行器,会将多条 SQL 语句累积到一起,等到批量执行时一起发送给数据库,减少数据库交互次数,提高性能。
备注: BATCH 模式下,事务在没有提交之前,无法获取到自增的 id。
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
for (User user : userList) {
userMapper.insertUser(user);
}
sqlSession.commit();
sqlSession.clearCache();
} catch (Exception e) {
e.printStackTrace();
}
上述 case,每次执行 insertUser() 方法时,SQL 语句会被加入到批处理列表中,而不是立即执行。最后,通过调用 commit() 方法一次性提交所有操作。
扩展
ExecutorType.BATCH 执行原理
MyBatis 的批处理依赖于 JDBC 的批处理机制。Mybatis 通过批处理模式将多条 SQL 语句打包成一个批次,并在一次网络交互中发送到数据库。从而减少网络延迟和数据库连接开销,提高整体性能。
MyBatis 在内部通过设置 ExecutorType.BATCH 类型来实现批处理。这种类型的 Executor 会缓存所有执行的 SQL 语句,直到调用 commit() 方法时才会统一发送到数据库执行。
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
// 检查当前 SQL 是否和上一次执行的 SQL 相同
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
int last = statementList.size() - 1;
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
// 设置SQL参数
handler.parameterize(stmt);// fix Issues 322
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
// 数据库连接
Connection connection = getConnection(ms.getStatementLog());
// 预编译
stmt = handler.prepare(connection, transaction.getTimeout());
// 参数设置
handler.parameterize(stmt); // fix Issues 322
currentSql = sql;
currentStatement = ms;
// 缓存SQL及结果,doFlushStatements 才真正执行sql
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 批量处理
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
// 执行结果
List<BatchResult> results = new ArrayList<>();
// 回滚检查
if (isRollback) {
return Collections.emptyList();
}
// 批量执行
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
// 执行批处理并获取更新计数
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
closeStatement(stmt);
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #").append(i + 1).append(")").append(" failed.");
if (i > 0) {
message.append(" ").append(i).append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
// 添加执行结果
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
ExecutorType.BATCH 核心流程
- SQL 复用:如果当前执行的 SQL 语句和上一次执行的 SQL 语句相同,则复用上一次的 Statement 对象,避免重复创建 Statement,提高性能。
- 新 SQL 语句:如果当前 SQL 语句和上一次不同,则创建新的 Statement 对象,并将其添加到批处理列表。
- 批量处理:每个 Statement 对象对应一次交互,将所有缓存的 SQL 语句一次性发送到数据库。
- 提交事务:一次交互来提交事务。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。