使用JPA/Crud Repository与@Version注解时的版本更新问题
问题描述
在MySQL中使用@Version注解实现乐观锁,执行两次连续更新时抛出org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction错误。原因是第一次更新后数据库版本号已递增,但本地实体对象仍保留旧版本号,导致第二次更新时乐观锁校验失败。调试发现save()/saveAll()方法没有返回更新后的版本号。
实体类核心代码:
@Entity @Table(name = "CO_PAR_ACTIVATION_DIFFUSION_ENTITE") public class ActivationDiffusionEntiteDb extends AbstractDatabaseObject { private static final long serialVersionUID = -6355794857690807029L; @Id @SequenceGenerator(name = "TCO_ACTENT_GENERATOR", allocationSize = 1) @GeneratedValue(strategy = GenerationType.AUTO, generator = "TCO_ACTENT_GENERATOR") private Long id; // 其他字段省略 @Version private Long version; }
无需重新查询的解决方案
1. 使用merge()替代save()/saveAll()
merge()方法会处理分离状态的实体,执行数据库更新后,返回一个处于托管状态的实体实例,该实例包含最新的版本号。直接使用返回的对象进行后续操作即可:
// 假设yourEntity是分离状态的实例 ActivationDiffusionEntiteDb updatedEntity = entityManager.merge(yourEntity); // 此时updatedEntity.getVersion()就是更新后的版本号,可用于后续更新
2. 确保操作在同一持久化上下文(Session)内
如果两次更新操作处于同一个Hibernate Session(或Spring的@Transactional方法内),Hibernate会在第一次更新完成后(事务提交或flush时)自动同步本地实体的版本号。示例:
@Transactional public void updateTwice(ActivationDiffusionEntiteDb entity) { // 第一次更新,Hibernate自动标记实体为脏数据 entityManager.save(entity); // 此时Hibernate尚未执行数据库操作,若要立即同步可调用flush entityManager.flush(); // 本地实体的version已更新,可直接执行第二次更新 entity.setSomeField("newValue"); entityManager.save(entity); }
3. 调用flush()强制同步上下文
如果坚持使用save(),可以在第一次save()后调用entityManager.flush(),强制Hibernate立即执行数据库更新操作,并将最新的版本号同步回本地实体:
entityManager.save(yourEntity); entityManager.flush(); // 强制同步数据库状态到本地实体 // 此时yourEntity.getVersion()已更新为最新值,可进行第二次更新
原理说明
save()方法对于托管状态的实体,仅会将其标记为"脏数据",实际的数据库更新和版本号递增操作会延迟到事务提交或flush()时执行。如果在这之前尝试第二次更新,本地实体的版本号还是旧值,导致乐观锁校验失败。而merge()会直接处理实体的更新逻辑,并返回同步后的实例;同一Session内的操作则会由Hibernate自动维护实体状态的一致性。
内容的提问来源于stack exchange,提问作者CJunior

