You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级Spring Boot 3.4后触发ObjectOptimisticLockingFailureException求助

Spring Boot 3.4.0升级后手动设置ID的实体保存抛出乐观锁异常

升级至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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 02:35:54