1. 概述
本文将介绍如何使用 Hibernate/JPA 实现批量插入和更新操作。
批量操作的核心在于:将多个 SQL 语句通过一次网络请求发送给数据库,从而减少网络开销,提高性能。对于高并发或数据量大的系统,合理使用批量操作可以显著提升系统吞吐量。
2. 环境准备
2.1 示例数据模型
我们使用两个实体类来演示批量操作:School
和 Student
。
@Entity
public class School {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@OneToMany(mappedBy = "school")
private List<Student> students;
// Getters and setters...
}
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@ManyToOne
private School school;
// Getters and setters...
}
2.2 SQL 日志追踪
为了验证是否真正进行了批量操作,我们使用数据源代理来追踪 SQL 语句:
private static class ProxyDataSourceInterceptor implements MethodInterceptor {
private final DataSource dataSource;
public ProxyDataSourceInterceptor(final DataSource dataSource) {
this.dataSource = ProxyDataSourceBuilder.create(dataSource)
.name("Batch-Insert-Logger")
.asJson().countQuery().logQueryToSysOut().build();
}
// 其他方法...
}
3. 默认行为
Hibernate 默认 不启用批量操作。这意味着每次插入或更新操作都会单独发送 SQL 语句。
示例代码如下:
@Transactional
@Test
public void whenNotConfigured_ThenSendsInsertsSeparately() {
for (int i = 0; i < 10; i++) {
School school = createSchool(i);
entityManager.persist(school);
}
entityManager.flush();
}
日志显示每个插入语句都是独立执行的:
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
"params":[["School1","1"]]
...
要启用批量操作,必须设置以下配置:
hibernate.jdbc.batch_size=5
如果是 Spring Boot 项目,配置如下:
spring.jpa.properties.hibernate.jdbc.batch_size=5
4. 单表批量插入
4.1 无显式 flush 的批量插入
当插入单一类型实体时,启用批量配置后,Hibernate 会自动进行批量插入。
示例代码如下:
@Transactional
@Test
public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() {
for (int i = 0; i < 10; i++) {
School school = createSchool(i);
entityManager.persist(school);
}
}
日志显示插入语句被合并为两个批次,每批 5 条:
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"],
"params":[["School1","1"],["School2","2"],["School3","3"],["School4","4"],["School5","5"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"],
"params":[["School6","6"],["School7","7"],["School8","8"],["School9","9"],["School10","10"]]
⚠️ 注意:Hibernate 会将插入的实体缓存在 Persistence Context 中,插入大量数据时可能导致内存溢出(OutOfMemoryError)。
4.2 显式 flush 和 clear
为了减少内存占用,可以在每批插入后执行 flush()
和 clear()
:
@Transactional
@Test
public void whenFlushingAfterBatch_ThenClearsMemory() {
for (int i = 0; i < 10; i++) {
if (i > 0 && i % BATCH_SIZE == 0) {
entityManager.flush();
entityManager.clear();
}
School school = createSchool(i);
entityManager.persist(school);
}
}
✅ 这样做可以避免内存中缓存过多实体,同时不影响批量插入效果。
5. 多表批量插入
5.1 多实体插入问题
当插入多个不同类型的实体时,Hibernate 默认不会跨类型合并批次,而是为每个类型创建新批次。
示例代码如下:
@Transactional
@Test
public void whenThereAreMultipleEntities_ThenCreatesNewBatch() {
for (int i = 0; i < 10; i++) {
if (i > 0 && i % BATCH_SIZE == 0) {
entityManager.flush();
entityManager.clear();
}
School school = createSchool(i);
entityManager.persist(school);
Student firstStudent = createStudent(school);
Student secondStudent = createStudent(school);
entityManager.persist(firstStudent);
entityManager.persist(secondStudent);
}
}
日志显示插入 School
和 Student
时批次大小不理想:
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school ..."]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student ..."]
...
❌ 这种方式效率较低,无法充分利用批量插入的优势。
5.2 优化:启用 hibernate.order_inserts
为了优化多表插入行为,需启用以下配置:
hibernate.order_inserts=true
Spring Boot 配置如下:
spring.jpa.properties.hibernate.order_inserts=true
启用后,Hibernate 会先收集所有插入语句,再按实体类型进行排序并批量发送。
优化后日志如下:
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school ..."]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student ..."]
✅ 插入效率大幅提升。
6. 批量更新
Hibernate 同样支持批量更新操作,但需要启用两个配置项:
hibernate.order_updates=true
hibernate.batch_versioned_data=true
Spring Boot 配置如下:
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true
示例代码如下:
@Transactional
@Test
public void whenUpdatingEntities_thenCreatesBatch() {
TypedQuery<School> schoolQuery =
entityManager.createQuery("SELECT s from School s", School.class);
List<School> allSchools = schoolQuery.getResultList();
for (School school : allSchools) {
school.setName("Updated_" + school.getName());
}
}
日志显示更新语句被合并为两个批次,每批 5 条:
"batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"],
"params":[["Updated_School1","1"],["Updated_School2","2"],...]
✅ 批量更新同样可以显著减少数据库交互次数,提升性能。
7. 主键生成策略影响
Hibernate 的批量插入功能对主键生成策略有要求:
- ✅ 支持批量操作的主键策略:
GenerationType.SEQUENCE
、GenerationType.TABLE
- ❌ 不支持批量操作的主键策略:
GenerationType.IDENTITY
如果使用 IDENTITY
,Hibernate 会自动禁用批量插入,即使配置了 hibernate.jdbc.batch_size
也不会生效。
所以,建议使用 SEQUENCE
作为主键生成策略。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
8. 总结
本文介绍了如何在 Hibernate/JPA 中启用并优化批量插入和更新操作:
✅ 关键配置点:
hibernate.jdbc.batch_size
:启用批量操作hibernate.order_inserts
:优化多表插入顺序hibernate.order_updates
+hibernate.batch_versioned_data
:启用批量更新- 主键策略使用
SEQUENCE
而非IDENTITY
✅ 优化建议:
- 插入大量数据时,定期调用
flush()
和clear()
减少内存占用 - 使用数据源代理工具(如
ProxyDataSource
)验证是否真正执行了批量操作
通过合理配置和使用,Hibernate 的批量操作可以显著提升数据处理性能。