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

ID类型为UUID时触发ObjectOptimisticLockingFailureException问题排查

问题描述

测试中对比两种不同ID类型的实体:使用Long类型ID时操作正常,但使用UUID类型ID时会抛出ObjectOptimisticLockingFailureException。


相关配置与代码

POM文件

...
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.6</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
...
<properties>
    <java.version>17</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
            
</dependencies>
...

配置文件为空。

Long类型实体、仓库与测试

实体类

@Entity
@Table(name = "LONG_ENTITY")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class LongEntity implements Serializable {

   @Id
   @GeneratedValue
   @EqualsAndHashCode.Include
   private Long id;

   private String name;
}

仓库接口

public interface LongEntityRepository extends CrudRepository<LongEntity,Long> {
   List<LongEntity> findByName(String name);
}

测试类

@SpringBootTest
@Transactional
public class LongEntityRepositoryTest {

   @Autowired
   private LongEntityRepository repository;

   @Test
   public void testSaveLong() {
      LongEntity expected = new LongEntity();
      expected.setName("Name");
      expected = repository.save(expected);
      expected.setName("NewName");
      expected = repository.save(expected);
    
      List<LongEntity> result = repository.findByName(expected.getName());

      assertThat(result).as("check result contains expected")
            .contains(expected);
   }
}

该测试执行正常。

UUID类型实体、仓库与测试

实体类

@Entity
@Table(name = "UUID_ENTITY")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class UuidEntity implements Serializable {

   @Id
   @GeneratedValue
   @EqualsAndHashCode.Include
   private UUID id;

   private String name;
}

仓库接口

public interface UuidEntityRepository extends CrudRepository<UuidEntity,UUID> {
   List<UuidEntity> findByName(String name);
}

测试类

@SpringBootTest
@Transactional
public class UuidEntityRepositoryTest {

   @Autowired
   private UuidEntityRepository repository;

   @Test
   public void testSaveUuid() {
      UuidEntity expected = new UuidEntity();
      expected.setName("Name");
      expected = repository.save(expected);
      expected.setName("NewName");           // 异常触发点
      expected = repository.save(expected);
    
      List<UuidEntity> result = repository.findByName(expected.getName());

      assertThat(result).as("check result contains expected")
            .contains(expected);
   }
}

该测试执行失败,错误信息如下:

org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update uuid_entity set name=? where id=?; nested exception is 
org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update uuid_entity set name=? where id=?
at net.tyt.sample.jpa.UuidEntityRepositoryTest.testSaveUuid(UuidEntityRepositoryTest.java:39)
Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update uuid_entity set name=? where id=?
at net.tyt.sample.jpa.UuidEntityRepositoryTest.testSaveUuid(UuidEntityRepositoryTest.java:39)

原因分析与解决方案

问题原因

当使用UUID作为@Id且仅标注@GeneratedValue时,Spring Boot 2.7.x搭配的Hibernate版本中,默认的主键生成策略无法正确处理UUID类型:

  • Hibernate默认不会自动为UUID类型选择合适的生成策略,导致第一次save后,虽然实体的UUID字段被赋值,但Hibernate未能正确跟踪该实体的持久化状态,或者生成的UUID在数据库中的存储格式与实体内存中的UUID对象不匹配。
  • 后续执行更新操作时,Hibernate生成的SQL语句中,WHERE id=?的条件无法匹配数据库中的记录,导致更新行数为0,触发ObjectOptimisticLockingFailureException。

而Long类型ID之所以正常,是因为Hibernate对Long类型默认使用IDENTITY生成策略,数据库会自动生成主键并返回给实体,Hibernate能正确跟踪实体状态,更新时可以匹配到对应行。

解决方案

给UUID类型的@Id字段明确指定生成策略,有两种方式:

方式1:使用JPA标准的GenerationType.UUID

修改UuidEntity的ID字段注解:

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@EqualsAndHashCode.Include
private UUID id;

方式2:使用Hibernate特定的@UuidGenerator

修改实体类注解:

import org.hibernate.annotations.UuidGenerator;

@Entity
@Table(name = "UUID_ENTITY")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class UuidEntity implements Serializable {

   @Id
   @UuidGenerator
   @EqualsAndHashCode.Include
   private UUID id;

   private String name;
}

两种方式都能让Hibernate正确生成并绑定UUID到实体,确保后续更新操作能匹配到数据库中的记录,解决异常问题。


内容的提问来源于stack exchange,提问作者Igor Tytar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 14:35:22