Spring JPA 中批量插入和更新是使用 SimpleJpaRepository#saveAll,saveAll 会循环调用 save 方法,save 方法会根据实体 id 查找记录,记录存在则更新,不存在则插入。n 个实体需要执行 2n 条语句,因而效率较低。
@Transactional
@Override
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
注:id 为基本类型且为 null 时,会直接插入记录,只执行 n 条语句。
此文中,将在主键自增的前提下,以借助 druid 监控,探讨 Spring JPA 批量插入与更新优化思路。
使用 SimpleJpaRepository#saveAll,插入 5k 条记录。
总共执行事务 5000*2 次,用时 543 s。
Hibernate 本身支持批量执行,通过 spring.jpa.properties.hibernate.jdbc.batch_size 指定批处理的容量。
共执行事务 5000+5 次,用时 439 s。
利用 EntityManager 批量插入 5k 条记录。
private <S extends T> void batchExecute(Iterable<S> s, Consumer<S> consumer) {
Session unwrap = entityManager.unwrap(Session.class);
try {
unwrap.getTransaction().begin();
Iterator<S> iterator = s.iterator();
int index = 0;
while (iterator.hasNext()) {
consumer.accept(iterator.next());
index++;
if (index % BATCH_SIZE == 0) {
entityManager.flush();
entityManager.clear();
if (index % BATCH_SIZE != 0) {
entityManager.flush();
entityManager.clear();
unwrap.getTransaction().commit();
} catch (Exception e) {
unwrap.getTransaction().rollback();
总共执行事务 5 次,用时 255 s,和 SimpleJpaRepository#saveAll 相比,节省了查询的性能消耗。
通过拼接 SQL 语句的方式,使用一条语句插入多条记录。
public void insertUsingConcat() {
StringBuilder sb = new StringBuilder("insert into t_comment(id, content, name) values ");
List<CommentPO> l = new ArrayList<>();
for (int i = 10000; i < 15000; i++) {
sb.append("(")
.append(i)
.append(",'content of demo batch#")
.append(i)
.append("','name of demo batch#")
.append(i)
.append("'),");
sb.deleteCharAt(sb.length() - 1);
executeQuery(sb);
final EntityManager entityManager = ApplicationContextHolder.getApplicationContext()
.getBean("entityManagerSecondary", EntityManager.class);
@Transactional
public void executeQuery(StringBuilder sb) {
Session unwrap = entityManager.unwrap(Session.class);
unwrap.setJdbcBatchSize(1000);
try {
unwrap.getTransaction().begin();
Query query = entityManager.createNativeQuery(sb.toString());
query.executeUpdate();
unwrap.getTransaction().commit();
} catch (Exception e) {
e.printStackTrace();
unwrap.getTransaction().rollback();
执行一次事务,用时 0.2 s。
拼接语句需要注意 sql 语句长度限制,可以通过 show VARIABLES WHERE Variable_name LIKE 'max_allowed_packet';
查询,这是 Server 一次接受的数据包大小,通过 my.ini 配置。
批量更新和批量插入类似,也是四种写法,结论也一致。区别仅在于 sql 写法:
@PutMapping("/concatUpdateClause")
public void updateUsingConcat() {
List<CommentPO> l = getDemoBatches(5000, 10000, "new");
StringBuilder sb = new StringBuilder("update t_comment set content = case");
for (CommentPO commentPO : l) {
sb.append(" when id = ").append(commentPO.getId()).append(" then '").append(commentPO.getContent())
.append("'");
sb.append(" else content end")
.append(" where id in (")
.append(l.stream().map(i -> String.valueOf(i.getId())).collect(Collectors.joining(",")))
.append(")");
executeQuery(sb);
复制代码