Spring Boot JPA查询时为何自动递增实体@Version版本字段
问题产生原因
- 核心触发逻辑:通过
accountRepository.findById()获取的Account实例是JPA持久化上下文托管的实体对象,并非普通游离值对象。Spring Data JPA执行findAllByAccount查询前,Hibernate会遵循默认FlushMode.AUTO规则,先对当前持久化上下文内所有托管实体做脏检查:如果判定实体属性和加载时生成的快照不一致,会提前执行UPDATE语句将变更刷入数据库,保证查询结果和上下文内未提交的变更一致。 - 无主动修改却触发更新的根因:当前场景下大概率是实体类缺失JPA规范要求的无参构造(
@Getter、@Setter注解不会自动生成无参构造),或是未开启Hibernate字段级脏检查对应的字节码增强,导致Hibernate生成属性快照、对比属性值时出现误判,错误认定Account实体存在字段变更,因此执行UPDATE语句,每次操作都会让@Version标注的版本号自增1。 - 只读事务报错原因:
@Transactional(readOnly = true)会将数据库连接设置为只读模式,禁止执行UPDATE/INSERT类写操作,Hibernate误触发的UPDATE语句在该模式下会被数据库直接拒绝,抛出cannot execute UPDATE in a read-only transaction异常。
解决方案
按推荐优先级从高到低排列:
- 最优方案:避免传入托管实体作为查询参数,直接用关联实体ID做查询条件。修改EventRepository的方法定义,直接通过Account的id字段关联查询,不需要提前加载Account实体,从根源上规避托管实体脏检查问题,同时还能减少一次数据库查询,提升性能:
@Repository public interface EventRepository extends JpaRepository<Event, Long> { // 直接使用关联实体的ID作为查询参数,不需要传入实体对象 Page<Event> findAllByAccountId(Long accountId, Pageable pageable); }
对应业务代码调整为:
@Transactional(readOnly = true) // 纯查询场景可直接开启只读事务,进一步提升性能 public Page<Event> events(Long accountId, int page) { return eventRepository.findAllByAccountId(accountId, PageRequest.of(page, PAGE_SIZE)); }
- 备选方案:如果业务逻辑必须传入Account实体作为查询参数,可以给对应查询方法添加QueryHint,将查询前的Flush模式修改为
COMMIT,即查询执行前不触发flush逻辑,等事务提交时再统一刷入变更,避免查询前误触发UPDATE语句:
@Repository public interface EventRepository extends JpaRepository<Event, Long> { @QueryHints(value = { @QueryHint(name = org.hibernate.annotations.QueryHints.FLUSH_MODE, value = "COMMIT") }, forCounting = false) Page<Event> findAllByAccount(Account account, Pageable pageable); }
- 基础配置修复:修正JPA实体的配置,降低脏检查误判概率
- 给Account、Event实体类添加Lombok注解
@NoArgsConstructor(access = AccessLevel.PROTECTED),提供JPA规范要求的无参构造函数 - 可在构建工具(Maven/Gradle)中配置Hibernate字节码增强插件,开启字段级脏检查能力,提升属性对比的准确率,从框架层面减少脏数据误判。
- 给Account、Event实体类添加Lombok注解
内容的提问来源于stack exchange,提问作者D Mak
相关产品推荐
相关产品推荐

