如何正确更新含@Version字段的实体?乐观锁异常未触发
乐观锁不触发的原因及可靠更新方案
嘿,我来帮你拆解下问题所在,以及怎么实现可靠的乐观锁更新:
你的乐观锁完全没触发,核心是两个关键错误:
- 手动覆盖了实体的version字段:你在更新时直接把DTO里的version赋值给了tenant实体——这相当于绕开了JPA的乐观锁逻辑。JPA的乐观锁是依赖加载实体时的原始version值和数据库里的值做比对,你手动替换后,save时会用这个新设置的值去和数据库匹配,自然不会触发异常。
- 缺少事务上下文:没有显式声明事务的话,
findById和save是两个独立的持久化操作。findById加载实体后,实体就脱离了持久化上下文,save的时候JPA会把它当成detached实体处理,直接生成新的version覆盖数据库值,根本不会做乐观锁校验。
正确的实现方式
1. 删掉手动设置version的代码
DTO里的version应该用来做前置校验,而不是直接赋值给实体。我们要先比对DTO的version和数据库加载的实体version是否一致,不一致直接抛出异常:
Option<Tenant> updateTenant(Long id, UpdateTenantDTO dto) { return repository.findById(id) .map(tenant -> { // 先校验版本是否匹配 if (!tenant.getVersion().equals(dto.getVersion())) { throw new OptimisticLockingFailureException("该租户信息已被其他进程修改,请刷新后重试"); } // 只更新需要修改的字段 if (isNotEmpty(dto.getName())) { tenant.setName(dto.getName()); } return tenant; }) .map(repository::save); }
2. 添加事务注解
在方法上加上@Transactional,确保findById和save处于同一个持久化上下文内。这样JPA会在事务提交时,自动用加载实体时的原始version和数据库中的值做比对,不一致就抛出乐观锁异常:
import org.springframework.transaction.annotation.Transactional; @Transactional Option<Tenant> updateTenant(Long id, UpdateTenantDTO dto) { return repository.findById(id) .map(tenant -> { if (!tenant.getVersion().equals(dto.getVersion())) { throw new OptimisticLockingFailureException("该租户信息已被其他进程修改,请刷新后重试"); } if (isNotEmpty(dto.getName())) { tenant.setName(dto.getName()); } return tenant; }) .map(repository::save); }
3. 依赖Spring Data JPA的自动乐观锁处理
如果你用的是Spring Data JPA的JpaRepository,save方法默认会处理乐观锁逻辑。当实体处于managed状态(事务内加载的实体就是managed),save时会生成这样的SQL:
UPDATE tenant SET name = ?, version = ? WHERE id = ? AND version = ?
如果这条语句影响行数为0,说明数据库里的version已经变了,JPA就会自动抛出OptimisticLockingFailureException,这正是我们要的乐观锁异常。
额外提醒
- 不要手动修改实体的version字段(除了初始化):你的
Tenant类里version = Instant.now()初始化是没问题的,但更新时绝对不能碰这个值。 - 捕获乐观锁异常后要给用户友好提示:比如前端可以提示“数据已被修改,请刷新后重试”,后端可以统一处理这个异常返回对应的HTTP状态码(比如409 Conflict)。
内容的提问来源于stack exchange,提问作者Opal
相关产品推荐
相关产品推荐

