一、代码
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.function.Consumer;
/**
* 批处理工具
* @author hulei
*/
public class BatchInsertUtil {
private static final Logger logger = LoggerFactory.getLogger(BatchInsertUtil.class);
private static final SqlSessionFactory sqlSessionFactory = SpringUtils.getBean(SqlSessionFactory.class);
private static final int DEFAULT_BATCH_SIZE = 1000;
private static int validateAndReturnBatchSize(Integer handleCount) {
if (handleCount == null || handleCount <= 0) {
throw new IllegalArgumentException("批处理条数必须大于0");
}
return handleCount;
}
public static <T, U> int batchUpdateOrInsert(List<T> data, Class<U> mapperClass, Consumer<Pair<T, U>> consumer) {
int handleCount = validateAndReturnBatchSize(DEFAULT_BATCH_SIZE);
int processedCount = 0;
SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
U mapper = batchSqlSession.getMapper(mapperClass);
for (T element : data) {
consumer.accept(new Pair<>(element, mapper));
processedCount++;
if (processedCount % handleCount == 0) {
batchSqlSession.flushStatements();
}
}
batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
} catch (Exception e) {
logger.error("批处理发生异常:{}", e.getMessage(), e);
batchSqlSession.rollback();
throw new RuntimeException("批量处理失败", e);
} finally {
batchSqlSession.close();
}
return processedCount;
}
public static void jdbcTemplateBatchUpdateOrInsert(JdbcTemplate jdbcTemplate, String updateOrInsertSQL, List<Object[]> batchArgs) {
int handleCount = validateAndReturnBatchSize(DEFAULT_BATCH_SIZE);
try (Connection con = jdbcTemplate.getDataSource().getConnection()) {
con.setAutoCommit(false);
int batchSize = batchArgs.size() / handleCount + 1;
for (int i = 0; i < batchSize; i++) {
int fromIndex = i * handleCount;
int toIndex = Math.min(fromIndex + handleCount, batchArgs.size());
List<Object[]> subList = batchArgs.subList(fromIndex, toIndex);
jdbcTemplate.batchUpdate(updateOrInsertSQL, subList);
}
con.commit();
} catch (Exception e) {
logger.error("批量提交处理失败:{}", e.getMessage(), e);
rollbackAndCloseConnection(con);
throw new RuntimeException("批量提交处理失败", e);
}
}
private static void rollbackAndCloseConnection(Connection con) {
if (con != null) {
try {
con.rollback();
con.setAutoCommit(true);
} catch (SQLException e) {
logger.error("无法回滚事务或重置自动提交状态", e);
} finally {
try {
con.close();
} catch (SQLException e) {
logger.error("关闭数据库连接时发生错误", e);
}
}
}
}
// 辅助类,用于传递实体和Mapper的组合
private static class Pair<T, U> {
private final T first;
private final U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public U getSecond() {
return second;
}
}
}
二、代码分析
这段代码是Java实现的一个批量处理工具类,主要包含了两个方法,分别用于MyBatis和JdbcTemplate的批量处理操作。这个类主要用于提高数据插入或更新的效率,通过批处理方式减少数据库交互次数,提高性能。
1. 类定义和成员变量
BATCH_SIZE: 定义了默认的批处理大小为1000。
logger: 使用SLF4J的日志记录器,用于输出日志信息。
sqlSessionFactory: 从Spring容器中获取的SqlSessionFactory,用于创建SqlSession实例。
2. 方法定义
2.1 batchUpdateOrInsert
这是一个泛型方法,接收一个数据列表、一个Mapper类以及一个BiFunction。BiFunction用于处理每个数据项,通常是调用Mapper的方法执行插入或更新操作。方法内部通过SqlSession的批处理功能执行操作,每次handleCount个数据项后提交一次事务。
2.2 jdbcTemplateBatchUpdateOrInsert
这个方法使用JdbcTemplate进行批量插入操作,接收一个JdbcTemplate实例、SQL语句和一个包含参数的列表。方法内部通过循环处理参数列表,每次处理handleCount个元素,然后调用batchUpdate方法执行批处理。在非事务环境中,手动提交事务;在事务环境中,提交操作无效。
3. 其他辅助方法
validateAndReturnBatchSize: 检查handleCount是否大于0,如果不是,则抛出异常,否则返回handleCount。此方法用于确保批处理大小的有效性。
rollbackAndCloseConnection: 用于回滚事务、恢复自动提交状态并关闭数据库连接,主要用于处理异常情况下的资源清理。
4. 总体评价
代码结构清晰,职责明确,分别实现了MyBatis和JdbcTemplate的批量处理功能。
使用了批处理和分批提交,提高了数据库操作的效率。
异常处理和资源管理比较完整,能够确保在异常情况下资源能够被正确释放。
通过BiFunction和Consumer提供了灵活的处理逻辑,可以适应不同的业务需求。
需要注意的是,这段代码假设了SpringUtils.getBean()方法可以从Spring上下文中获取SqlSessionFactory。如果实际项目中没有这样的工具类,需要使用Spring的依赖注入来获取SqlSessionFactory。
三、mybatis的foreach
实际开发中我们可能经常出现如下写法来完成数据的批量插入
<insert id="batchInsert" parameterType="java.util.List">
insert into USER (id, name) values
<foreach collection="list" item="model" index="index" separator=",">
(#{model.id}, #{model.name})
</foreach>
</insert>
这个方法提升批量插入速度的原理是,将传统的:
INSERT INTO `user` (`name`, `age`)
VALUES ("zs", "10"),
("ls", "12"),
("ww", "14"),
("hl", "11"),
("sw", "54");
转化为:
INSERT INTO `user` (`name`, `age`)
VALUES ("zs", "10"),
("ls", "12"),
("ww", "14"),
("hl", "11"),
("sw", "54");
当插入数量很多时,不能一次性全放在一条语句里。因为默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。当我们不停地使用这个批量插入方法,而MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。
如果我们的foreach后有5000+个values,那么这个PreparedStatement特别长,他包含了很多占位符,对于占位符和参数的映射尤其耗时。查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。
mybatis的官方文档也给出了一种方式
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
List<SimpleTableRecord> records = getRecordsToInsert(); // not shown
BatchInsert<SimpleTableRecord> batchInsert = insert(records)
.into(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);
batchInsert.insertStatements().stream().forEach(mapper::insert);
session.commit();
} finally {
session.close();
}
基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
"insert into tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();
所以推荐使用笔者给出的批处理方式,批量提交sql到数据库执行,显著提升性能。