Spring JPA 批量插入与更新优化

Spring JPA 中批量插入和更新是使用 SimpleJpaRepository#saveAll,saveAll 会循环调用 save 方法,save 方法会根据实体 id 查找记录,记录存在则更新,不存在则插入。n 个实体需要执行 2n 条语句,因而效率较低。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@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 条记录。

image.png
总共执行事务 5000*2 次,用时 543 s。

Hibernate 本身支持批量执行,通过 spring.jpa.properties.hibernate.jdbc.batch_size 指定批处理的容量。
image.png
共执行事务 5000+5 次,用时 439 s。

利用 EntityManager 批量插入 5k 条记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码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();
}
}

image.png
总共执行事务 5 次,用时 255 s,和 SimpleJpaRepository#saveAll 相比,节省了查询的性能消耗。

通过拼接 SQL 语句的方式,使用一条语句插入多条记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
scss复制代码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();
}
}

image.png
执行一次事务,用时 0.2 s。

拼接语句需要注意 sql 语句长度限制,可以通过 show VARIABLES WHERE Variable_name LIKE 'max_allowed_packet'; 查询,这是 Server 一次接受的数据包大小,通过 my.ini 配置。

批量更新

批量更新和批量插入类似,也是四种写法,结论也一致。区别仅在于 sql 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@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);
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%