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
相关产品推荐
相关产品推荐

