升级Spring Boot 3.4后触发ObjectOptimisticLockingFailureException求助
升级至Spring Boot 3.4.0后,单元测试中创建手动设置ID的UserEntity(实体配置了自定义IdentifierGenerator,设计目标是保留手动设置的ID),调用userRepository.save()时还未进入自定义生成器的generate方法,就抛出org.springframework.orm.ObjectOptimisticLockingFailureException异常。该测试在Spring Boot 3.3.x、3.2.x版本中可正常执行,需解决此问题以让自定义ID生成器正常运行。
自定义ID生成器代码
public class CustomIdentifierGenerator implements IdentifierGenerator { private ValueTransformer valueTransformer; private String entityName; @Override public Serializable generate(SharedSessionContractImplementor session, Object object) { // 从未执行到此处,已抛出ObjectOptimisticLockingFailureException return UUID.randomUUID().toString(); } }
单元测试与实体代码
@DataJpaTest @ContextConfiguration(classes = ServiceConfiguration.class) public class UserRepositoryTest { @Autowired private UserRepository userRepository; private UserEntity testUser; @BeforeEach public void setUp() { } @AfterEach public void tearDown() { userRepository.deleteAll(); } @Test public void testSaveUserEntity() { UserEntity user = new UserEntity(); user.setName("John Doe"); String id = UUID.randomUUID().toString(); user.setId(id); // 执行save时触发异常 UserEntity savedUser = userRepository.save(user); assertNotNull(savedUser.getId()); } } @Entity class UserEntity { @Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "com.example.demo.CustomIdentifierGenerator") @Column(name = "ID_STRING_FIELD") private String id; private String name; private String password; public UserEntity() { System.out.println("constructor invoked"); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } interface UserRepository extends CrudRepository<UserEntity, String> { }
完整堆栈信息
org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.demo.UserEntity#6ec53396-5c5b-4d13-b393-a63a0fbee3c8] at app//org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:325) ... Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.demo.UserEntity#6ec53396-5c5b-4d13-b393-a63a0fbee3c8] at app//org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:426) ...
问题原因
Spring Boot 3.4.0对应的Hibernate版本升级至6.5.x,该版本调整了实体状态判断逻辑:当实体配置了@GeneratedValue且手动设置了ID时,Hibernate会将其判定为detached(游离)状态,触发merge操作而非persist操作。merge会检查数据库中是否存在该ID的记录,若不存在则抛出StaleObjectStateException,最终被Spring封装为ObjectOptimisticLockingFailureException。而旧版本(Spring Boot 3.3.x/3.2.x对应Hibernate 6.4及以下)会直接执行insert操作,不会触发此判断。
解决方案
方案1:修改自定义ID生成器,优先使用手动设置的ID
自定义生成器的核心目标是保留手动设置的ID,因此需在generate方法中先检查实体已有的ID,存在则直接返回,不存在再生成新ID:
public class CustomIdentifierGenerator implements IdentifierGenerator { @Override public Serializable generate(SharedSessionContractImplementor session, Object object) { // 优先返回实体已设置的ID if (object instanceof UserEntity userEntity) { String existingId = userEntity.getId(); if (existingId != null && !existingId.isBlank()) { return existingId; } } // 未设置ID时生成新UUID return UUID.randomUUID().toString(); } }
方案2:明确实体的未保存值映射
在实体的@Id字段上添加@org.hibernate.annotations.Id(unsavedValue = null),告诉Hibernate:当ID为null时视为新实体,非null时若数据库无对应记录则执行插入,而非触发merge:
@Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "com.example.demo.CustomIdentifierGenerator") @Column(name = "ID_STRING_FIELD") @org.hibernate.annotations.Id(unsavedValue = null) private String id;
方案3:直接使用EntityManager执行持久化
绕过Spring Data JPA的save方法,直接注入EntityManager调用persist,强制执行插入操作(仅适用于确定是新实体的场景):
// 在测试类中注入EntityManager @Autowired private EntityManager entityManager; @Test public void testSaveUserEntity() { UserEntity user = new UserEntity(); user.setName("John Doe"); String id = UUID.randomUUID().toString(); user.setId(id); entityManager.persist(user); entityManager.flush(); assertNotNull(user.getId()); }
内容的提问来源于stack exchange,提问作者stephsmithnc

