JPA save() 的性能影响和潜在风险分析
1. 新增场景优化:明确区分新增逻辑,手动设置ID或使用SEQUENCE策略,直接调用persist()避免merge()的额外查询开销。
2. 更新场景优化:先查询再修改替代直接merge(),批量操作时分批处理,配置动态更新避免全字段更新,自定义更新方法绕过isNew()判断。
使用建议:JPA 的最大优势是简单高效,不用书写大量SQL,通常在业务简单、并发较低、涉及单表增删改查的后台管理系统中,比如:应用维护、设备管理、部门管理、商家管理等场景。但其存在的风险和性能影响不可忽视,不建议在以下系统中使用:
-
高并发、高性能,要极高的写入和读需求(性能低)。
-
业务复杂,通常涉及复杂的多表联查(较笨重)。
-
多写多读,需要经常更新多表数据(有风险)。
实体对象的四种状态:
瞬时态(TRANSIENT):当new一个实体对象后,这个对象处于瞬时态,即一个保存临时数据的内存区域,如果没有变量引用这个对象,会被JVM的垃圾回收机制回收。这个对象所保存的数据与数据库没有任何关系,除非通过Session的save()、saveOrUpdate()、persist()、merge()把瞬时态对象与数据库关联,并插入或者更新到数据库,才转换为持久态对象。
持久态(PERSISTENT):持久态对象的实例在数据库中有对应的记录,并拥有一个持久化标识(ID)。对持久态对象进行delete操作后,数据库中对应的记录将被删除,那么持久态对象与数据库记录不再存在对应关系,持久态对象变成移除态(可以视为瞬时态)。持久态对象被修改变更后,不会马上同步到数据库,直到数据库事务提交。
游离态(DETACHED):当Session进行了close()、clear()、evict()或flush()后,实体对象从持久态变成游离态,对象虽然拥有持久和与数据库对应记录一致的标识值,但是因为对象已经从会话中清除掉,对象不在持久化管理之内,所以处于游离态(也叫脱管态)。游离态的对象与临时状态对象是十分相似的,只是它还含有持久化标识。
移除态(DELETED):对持久态对象进行delete操作后,数据库中对应的记录将被删除,那么持久态对象与数据库记录不再存在对应关系,持久态对象变成移除态(可以视为瞬时态)。
save() 性能分析
JPA 提供的save方法,既是新增也是更新,通过 isNew() 判断。不同情况的性能影响较大
1 | public <S extends T> S save(S entity) { |
批量新增实际也是使用 save()
1 | public <S extends T> List<S> saveAll(Iterable<S> entities) { |
isNew(entity)
isNew() 的判断逻辑就是根据ID,通过一系列的方式获取实例ID值,如果为空就是新增,否则就是更新
-
通过@Id判断:如果id为null或基本类型默认值,则是新实体
1
2
3
4
5
6
7
8
public class User {
// 数据库生成
private Long id;
// isNew()逻辑:return id == null; 新实体,执行persist()
} -
通过@Version判断:如果有@Version字段且为null,则是新实体
-
自定义策略:实现Persistable接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Product implements Persistable<String> {
private String sku; // 主键
public boolean isNew() {
return true;
}
// 保存后设置为false
private void markNotNew() {
this.isNew = false;
}
}
persist(entity)
em.persist(entity) 插入新记录:
-
实体进入"managed"状态
-
INSERT操作延迟到flush时执行
-
返回的是原对象(同一个引用)
-
如果id由数据库生成,执行后id会被设置
1 | public void testPersist() { |
merge(entity)
em.merge(entity) 更新或合并对象。传入的实体可以是detached或transient状态,返回的是新的managed实体,原实体不会被修改。
内部流程,先检查实体状态:
-
如果实体是managed:直接返回
-
如果实体是detached,从数据库查找(SELECT查询):
- 如果找到:复制属性,返回managed副本,执行UPDATE
- 如果没找到:当作新实体persist
-
如果实体是transient:当作新实体persist,执行 INSERT
1 | public void testMerge() { |
合并原理
SessionImpl 的相关方法
1 | public Object merge(Object object) throws HibernateException { |
事件监听器链
-
自定义监听器(用户注册)
-
默认监听器(Hibernate内置,包括MergeEventListener)
-
Envers审计监听器(如果启用)
-
验证器监听器(Bean Validation)
1 | public void onMerge(MergeEvent event) throws HibernateException { |
处理实体合并的核心逻辑:根据不同的状态做不同处理,DETACHED(分离状态) 最复杂且影响性能。
1 | public void onMerge(MergeEvent event, Map copiedAlready) { |
自定义 Listener
自定义 MergeEventListener 实现持久化前的数据处理
1 |
|
性能影响
-
当调用save()更新实体时,
merge()的额外SELECT查询,和一系列反射解析操作1
2
3
4
5
6
7
8
9
10
11User user = new User();
user.setId(1L); // 已存在的ID
user.setName("张三");
repository.save(user); // 会调用merge()
// merge()内部会先执行:
// SELECT * FROM user WHERE id = 1
// 然后执行UPDATE
// 更新操作变成了 1次SELECT + 1次UPDATE可以开启日志查看详细记录:
会打印1条 SELECT 和 1条INSERT或UPDATE
1
2
3
4
5
6
7
8
9
10spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# 显示所有SQL
logging.level.org.hibernate.SQL=DEBUG
# 显示参数值
logging.level.org.hibernate.type.descriptor.sql=TRACE
# 显示统计信息
logging.level.org.hibernate.stat=DEBUG
# 查看merge过程
logging.level.org.hibernate.event.internal=DEBUG -
返回不同对象引用
1
2
3
4
5
6User user = userRepository.findById(1L);
user.setName("测试");
// 执行save()
User saved = userRepository.save(user);
// 如果user是detached状态,saved != user(两个是不同对象,如何存在对象引用,会有问题) -
批量保存的性能陷阱
1
2
3
4
5
6List<User> users = getUsers();
// 错误用法:循环调用save(),每次可能先SELECT再INSERT/UPDATE
for (User user : users) {
repository.save(user);
}
// 如果有1000个用户:可能执行1000次SELECT + 1000次INSERT/UPDATE -
ID生成策略的影响
-
MySQL自增,persist()后需要立即INSERT才能获取id,不支持批处理
1
2
3
4
5
6
7
8
9
public class UserWithIdentity {
private Long id; // 数据库生成 id == null,执行persist()
// 如果persist()后需要立即获取id,就需要立即INSERT
// 此时单条提交事务,性能影响严重。且无法使用JDBC批处理
} -
Oracle序列,预分配ID,性能较好,支持批处理
1
2
3
4
5
6
7
8
public class UserWithSequence {
private Long id; // 先获取序列值,id == null(但可能已预分配)
// 调用persist()时获取序列值
} -
手动分配ID,比如雪花算法或UUID,此时调用
save()会导致多余查询1
2
3
4
5
6
7
8
9
10
11
public class UserWithManualId {
private String uuid;
public UserWithManualId() {
this.uuid = UUID.randomUUID().toString();
}
// isNew()会检查ID是否已存在,如果存在save()可能错误调用merge()导致SELECT
} -
级联操作的影响
1
2
3
4
5
6
7
8
9
10
11public void testCascadeSave() {
Order order = new Order();
order.setId(1L);
OrderItem item = new OrderItem();
item.setProduct("袜子");
order.getItems().add(item);
// 保存order会自动保存item, 如果item有ID,且是已存在的。merge()会先SELECT,性能可能受影响
orderRepository.save(order);
}
-
优化建议和手段
-
对于数据库自增ID,可以直接调用
save()。 -
对于需要提前设置 ID 的新实体,为了避免进入
merge()执行 SELECT。自定义save(),跳过ID 判断,执行新增方法 -
对于新增的方法,可以直接在同一个事务中,不执行
save()。
避免 merge()
-
新增数据
当使用UUID或Snowflake等算法预生成ID,此时ID 不为空,为了避免执行
merge(),可以做如下: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// 1. 基础实体类
public abstract class BaseEntity implements Persistable<Long> {
private Long id;
// 不持久化到数据库
private transient boolean isNewFlag = true;
public Long getId() {
return id;
}
protected void prePersist() {
if (id == null) {
id = UUID.randomUUID().toString(); // 预分配ID
}
}
// 覆盖默认判断逻辑,确保isNew()总是返回true
public boolean isNew() {
return true;
}
protected void markAsNotNew() {
this.isNewFlag = false;
}
} -
更新数据
第一种情况:先手动调用
findById()查询,再覆盖参数,不执行save(),持久态(PERSISTENT)实体 Flush 后会自动更新先查询 managed实体,提交时不需要调用save(),flush时会自动UPDATE
注意:必须要添加事务,且需要事务生效
1
2
3
4
5
6// 实体添加 @DynamicUpdate 只更新非null字段
public void updateDirectly(Long id, String newName) {
User user = userRepo.findById(id); // 找到managed实体
user.setName(newName);
}第二种情况:直接实例化实体赋新值提交的问题
存在问题:如果某个属性有
@Column(name = "code", nullable = false)属于必填字段,但是更新时没有赋值,会出错not-null property references a null or transient value即使取消Java 层面检查,DB 层也会出错,原因分析:
merge() 内部:先SELECT获取数据库中的实体
合并属性:code(null) 覆盖了数据库中的值,在持久化上下文中,实体状态变为:code = null(违反约束)
抛出异常:not-null property references a null or transient value,这个异常在flush时抛出,不是在生成SQL时
1
2
3
4
5
6
7
8
9
10// 以下无法实现更新,会出错:Column 'code' cannot be null
public void updateDirectly(Long id, String newName) {
User user = new User();
user.setId(id);
// user.setCode("123");
user.setName(newName);
return userRepo.save(user);
}
自定义 save()
方案一:使用 EntityManager 重写方法,跳过判断和查询
1 |
|
方案二:使用 jdbcTemplate 书写原生SQL的方式,性能最佳:
1 | jdbcTemplate.update( |
方案三:只做独立的方法,避免多余的查询和判断
1 | /** |
方案四:使用 @Modifying 写原生SQL
1 |
|
批量新增
图示为使用 saveAll 的TPS性能
图示为使用 jdbcTemplate.batchUpdate 后的插入性能:1秒约 44444 条数据(两库5表)
批量新增配置:
1 | spring.jpa.properties.hibernate.jdbc.batch_size=500 |
1 | # 数据源关闭自动提交 |
方案一:使用jdbcTemplate
1 |
|
或者通过 PreparedStatement 提交:
1 | public int jdbcBatchInsert(List<User> entities) throws SQLException { |
方案二:使用 EntityManager 批量提交
1 |
|
方案三:使用 TransactionTemplate 管理事务,实现批量提交
1 | public void batchInsertWithTransaction(List<User> users) { |
方案四:使用 StatelessSession 管理事务(无一级缓存,性能更高)
1 | public void statelessBatchInsert(List<YourEntity> entities) { |
批量更新
方案一:原生SQL批量UPDATE,使用 jdbcTemplate 实现原生SQL更新。一次SQL更新所有记录
使用 VALUES() JOIN 更新
1 | public int batchUpdateWithValuesJoin(List<User> users) { |
使用 CASE WHEN 批量更新:
1 | public int batchUpdateWithCaseWhen(List<User> users) { |
方案二:Criteria API批量更新,类型安全,动态条件
1 |
|
方案三:JPQL批量UPDATE,类型安全,支持JPA实体映射
1 |
|
更新风险避坑
事务内多个持久态对象的更新风险与解决方案
如果在一个事务内包含多个持久态(PERSISTENT)状态的对象,当你想对其中一个执行更新时,有可能会更新到多个对象,导致一些异常发生。
此种情况在开发中较常见,是JPA的一大风险,即使你没有使用
save()提交保存,JPA 在事务刷新时也会提交保存对象。如果不想牵扯无关对象的更新,最简单的方式是转换为 瞬时态,或者事务精细化控制
可能的核心风险:
脏检查意外更新:修改一个实体可能触发其他实体的更新
级联更新扩散:关联实体会被自动更新
乐观锁冲突:未修改的实体可能因版本检查失败
内存溢出:太多持久态实体占用内存
场景分析
在同一个事务中更新多个持久态对象,可能的潜在风险:
-
Hibernate 的脏检查可能意外更新其他对象
-
其他对象可能被级联操作影响
-
如果这些对象有关联关系,可能触发额外更新
1 |
|
风险类型
脏检查更新
自动脏检查:任何字段修改都会在事务提交或手动flush时更新,只要在同一事务中
自动执行:
UPDATE user SET name = '李四' WHERE id = 1
Hibernate比较当前状态与快照,发现name变化,自动生成UPDATE
1 |
|
自动更新的原理与限制:只有持久态(PERSISTENT)实体才会自动更新,游离态/分离态(DETACHED)、临时态(TRANSIENT)、删除态(DELETED)实体 都不会自动更新。
执行:
User user = em.find(User.class, 1L);Hibernate内部:
创建EntityEntry记录实体状态
创建快照(原始值副本)
将实体注册到PersistenceContext
修改属性后,Hibernate不立即检测变化,
user.setName("测试")flush触发脏检查:
em.flush()
- 遍历PersistenceContext中所有持久态实体
- 对比当前值与快照值(字段级比较)
- 发现变化, 标记为"脏"
- 为脏实体生成UPDATE语句
- 批量执行所有UPDATE
执行成功后,更新快照值为新值,下次flush时以此为新基准
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 public void dirtyCheckAtSourceLevel() {
// 在AbstractFlushingEventListener.flushEntities()中:
for (Object entity : persistenceContext.getEntities()) {
EntityEntry entry = persistenceContext.getEntry(entity);
// 检查实体状态
if (entry.getStatus() == Status.MANAGED) {
// 获取原始快照值
Object[] loadedState = entry.getLoadedState();
// 获取当前值
Object[] currentState = persister.getPropertyValues(entity);
// 逐字段比较
boolean dirty = false;
for (int i = 0; i < loadedState.length; i++) {
if (!isSame(loadedState[i], currentState[i])) {
dirty = true;
break;
}
}
// 如果是脏数据,安排更新
if (dirty) {
scheduleUpdate(entity, entry, currentState);
}
}
}
}
Flush的自动触发时机:
1 | public class FlushTriggers { |
级联更新
级联操作:修改order会影响所有items,包含引用的另一个对象
1 |
|
乐观锁冲突
同一事务中的对象即使没有修改值,也可能存在乐观锁检查,导致其他对象事务提交失败
1 |
|
解决方案
通过以下手段控制更新风险,避免数据异常问题。
在事务中操作多个持久态实体时,使用@DynamicUpdate + 及时clear() + 分字段更新策略,避免意外更新扩散。
精细控制刷新时机
-
手动控制flush
1
2
3
4
5
6
7
8
9
10
11
12
13public void updateWithManualFlush(List<Long> userIds) {
// 按需加载,及时flush和clear
for (Long userId : userIds) {
User user = em.find(User.class, userId);
// 执行特定更新
performUpdate(user);
// 立即flush并clear当前实体
em.flush();
em.clear(); // 清除持久化上下文,确保当前实体被保存,释放内存,避免影响后续操作
}
} -
使用多个事务
1
2
3
4
5
6
7
8
9
10
11
12
13
public void updateInNewTransaction(User user) {
// 每个更新在独立事务中
performUpdate(user);
// 事务提交后自动flush,然后持久化上下文被清空
}
public void batchUpdateSafely(List<Long> userIds) {
for (Long userId : userIds) {
// 每次都重新查询(新事务,新持久化上下文)
controlledFlushService.updateInNewTransaction(userId);
}
}
使用只读查询分离数据
-
使用DTO模式,脱离持久态实体,并单独更新
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
27public void updateWithDTO(List<Long> userIds) {
// 只读查询获取数据(不变成持久态)
List<UserDTO> userDTOs = em.createQuery(
"SELECT new com.example.UserDTO(u.id, u.name, u.email) " +
"FROM User u WHERE u.id IN :ids", UserDTO.class)
.setParameter("ids", userIds)
.setHint(QueryHints.READ_ONLY, true) // 只读,不注册到持久化上下文
.getResultList();
// 业务逻辑处理
processBusinessLogic(userDTOs);
// 单独更新(每次一个)
for (UserDTO dto : userDTOs) {
userService.updateSingleUser(dto.getId(), dto);
}
}
public void updateSingleUser(Long userId, UserDTO dto) {
// 单独查询和更新
User user = em.find(User.class, userId);
user.setName(dto.getName());
user.setEmail(dto.getEmail());
em.flush();
em.clear(); // 清除,不影响其他
} -
使用StatelessSession(Hibernate特有),脱离 持久化 状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void updateWithStatelessSession(List<User> users) {
SessionFactory sessionFactory = em.getEntityManagerFactory()
.unwrap(SessionFactory.class);
try (StatelessSession session = sessionFactory.openStatelessSession()) {
Transaction tx = session.beginTransaction();
for (User user : users) {
// StatelessSession没有持久化上下文,不会进行脏检查,不会级联更新
session.update(user); // 显式更新,只更新当前对象
}
tx.commit();
}
}
动态更新配置
使用@DynamicUpdate,只生成必要的UPDATE。但是可能会无效
1 |
|
使用原生SQL精确控制
SQL 精确字段更新,只更新指定字段
1 |
|
使用Update DSL精确更新
-
使用CriteriaUpdate构建精确更新
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
35
36
37
private EntityManager em;
public int preciseCriteriaUpdate(List<PreciseUpdate> updates) {
CriteriaBuilder cb = em.getCriteriaBuilder();
int totalUpdated = 0;
for (PreciseUpdate update : updates) {
CriteriaUpdate<User> criteriaUpdate = cb.createCriteriaUpdate(User.class);
Root<User> root = criteriaUpdate.from(User.class);
// 动态构建SET子句
boolean hasUpdate = false;
if (update.shouldUpdateName()) {
criteriaUpdate.set(root.get("name"), update.getName());
hasUpdate = true;
}
if (update.shouldUpdateEmail()) {
criteriaUpdate.set(root.get("email"), update.getEmail());
hasUpdate = true;
}
// 如果没有需要更新的字段,跳过
if (!hasUpdate) {
continue;
}
// 精确WHERE条件
criteriaUpdate.where(cb.equal(root.get("id"), update.getId()));
totalUpdated += em.createQuery(criteriaUpdate).executeUpdate();
}
return totalUpdated;
} -
使用QueryDSL构建精确更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private JPAQueryFactory queryFactory;
public void updateWithQueryDSL(List<UserUpdateDTO> updates) {
QUser user = QUser.user;
for (UserUpdateDTO dto : updates) {
// 构建动态更新
SQLUpdateClause update = queryFactory.update(user);
if (dto.getName() != null) {
update.set(user.name, dto.getName());
}
if (dto.getEmail() != null) {
update.set(user.email, dto.getEmail());
}
// 精确条件
update.where(user.id.eq(dto.getId()));
update.execute();
}
}