本文旨在解决hibernate在使用 LockModeType.OPTIMISTIC_FORCE_INCREMENT 结合 LEFT JOIN FETCH 查询时,因关联实体缺乏 @Version 字段而引发的 cannot force version increment on non-versioned entity 异常。文章将深入剖析此问题的根源,并提供两种实用的解决方案:移除强制版本递增锁模式,或为相关联的实体添加 @Version 字段,以确保数据操作的正确性和稳定性。
理解Hibernate乐观锁与版本控制
在并发环境中,为了维护数据的一致性,乐观锁是一种常用的策略。hibernate通过在实体类中引入 @version 注解来实现乐观锁。当一个实体被更新时,其 @version 字段会自动递增。如果在更新操作提交时,发现数据库中的版本号与加载时的版本号不一致,则说明该实体已被其他事务修改,hibernate会抛出 optimisticlockexception,从而避免“脏写”问题。
除了标准的乐观锁行为,Hibernate还提供了更精细的锁模式,例如 LockModeType.OPTIMISTIC_FORCE_INCREMENT。这种锁模式的特殊之处在于,它会强制递增实体的版本号,即使该实体在当前事务中并没有被实际修改。这在某些场景下非常有用,例如当一个父实体虽然自身未被修改,但其子集合发生了变化,需要通过递增父实体的版本号来通知其他并发事务。
异常分析:cannot force version increment on non-versioned entity
当我们在使用 LockModeType.OPTIMISTIC_FORCE_INCREMENT 锁模式时,如果查询中通过 LEFT JOIN FETCH 等方式急加载了关联实体,并且这些关联实体没有定义 @Version 字段,就可能触发 cannot force version increment on non-versioned entity 异常。
让我们结合提供的代码示例来具体分析:
@Repository interface RootEntityRepository extends JpaRepository<RootEntity, String>{ @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) // 强制版本递增锁 @Query("select re FROM RootEntity re LEFT JOIN FETCH re.collects WHERE re.id=:id") // 急加载 collects Optional<RootEntity> findById(String id); } @Entity @Getter @Setter class RootEntity{ @Id private String id; @OneToMany(...) private Set<Collect> collects = new HashSet<>(); @Version private Long version; // RootEntity 具有版本字段 } @Entity @Getter @Setter class Collect{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ElementCollection private Set<EmbeddedCollect> embeddedCollects = new HashSet<>(); @ManyToOne private RootEntity root; // Collect 实体没有 @Version 字段 }
在这个例子中:
- RootEntityRepository.findById 方法被注解为 LockModeType.OPTIMISTIC_FORCE_INCREMENT。这意味着当调用此方法时,Hibernate会尝试强制递增查询结果中实体的版本号。
- 查询语句 SELECT re FROM RootEntity re LEFT JOIN FETCH re.collects 不仅加载了 RootEntity,还通过 LEFT JOIN FETCH 急加载了其关联的 collects 集合中的 Collect 实体。
- RootEntity 类定义了 @Version private Long version; 字段,因此它支持版本控制。
- 然而,Collect 实体类没有定义 @Version 字段。
异常原因: 当 findById 方法被调用时,LockModeType.OPTIMISTIC_FORCE_INCREMENT 会指示 Hibernate 强制递增所有被加载并处于持久化上下文中的、且被认为需要进行版本控制的实体。由于 LEFT JOIN FETCH 将 Collect 实体也加载到了持久化上下文中,Hibernate 尝试对 Collect 实体执行版本递增操作。但 Collect 实体没有 @Version 字段,因此Hibernate无法执行此操作,从而抛出 cannot force version increment on non-versioned entity 异常。
解决方案
针对此问题,主要有两种解决方案,选择哪种取决于您的业务需求:
方案一:移除不必要的 LockModeType.OPTIMISTIC_FORCE_INCREMENT
如果您的业务逻辑中,只有 RootEntity 需要进行版本控制,并且只有在 RootEntity 自身被修改时才需要递增其版本号(或者当其子集合发生变化时,默认的乐观锁机制会自动处理父实体的版本递增),那么 LockModeType.OPTIMISTIC_FORCE_INCREMENT 可能是多余的。
实现方式: 从 RootEntityRepository.findById 方法中移除 @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) 注解。
@Repository interface RootEntityRepository extends JpaRepository<RootEntity, String>{ // 移除 @Lock 注解 @Query("SELECT re FROM RootEntity re LEFT JOIN FETCH re.collects WHERE re.id=:id") Optional<RootEntity> findById(String id); }
优点:
- 简单直接,解决了异常。
- 符合多数场景下的乐观锁行为,即只在实体实际修改时才更新版本。
注意事项:
- 如果确实需要在不修改 RootEntity 自身的情况下,仅因为其关联集合 collects 发生了变化就强制递增 RootEntity 的版本,此方案可能不适用。但通常情况下,当 Collect 实体被添加、删除或更新时,如果 RootEntity 与 Collect 之间配置了 CascadeType.ALL 或 CascadeType.MERGE,Hibernate 会自动处理 RootEntity 的版本递增。
方案二:为所有受影响的实体添加 @Version 字段
如果您的业务需求确实要求 Collect 实体也参与版本控制,并且在某些操作下需要对其进行强制版本递增(尽管这在集合元素中不常见),那么可以为 Collect 实体添加 @Version 字段。
实现方式: 在 Collect 实体类中添加一个 @Version 字段。
@Entity @Getter @Setter class Collect{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ElementCollection private Set<EmbeddedCollect> embeddedCollects = new HashSet<>(); @ManyToOne private RootEntity root; @Version private Long version; // 为 Collect 实体添加版本字段 }
优点:
- 满足了对 Collect 实体进行版本控制和强制递增的需求。
注意事项:
- 为集合中的每个元素都添加版本字段可能会增加数据库的存储开销,并可能使并发控制逻辑更加复杂。
- 需要仔细考虑 Collect 实体是否真的需要独立的版本控制。在多数情况下,集合元素的并发控制可以通过其父实体(RootEntity)的版本来间接管理。
总结与最佳实践
cannot force version increment on non-versioned entity 异常是由于 Hibernate 在尝试对一个没有 @Version 字段的实体强制递增版本号时发生的。解决此问题的关键在于理解 LockModeType.OPTIMISTIC_FORCE_INCREMENT 的作用范围以及 LEFT JOIN FETCH 对持久化上下文的影响。
最佳实践建议:
- 明确版本控制需求: 只有当实体需要参与乐观锁机制时,才为其添加 @Version 字段。
- 谨慎使用 LockModeType.OPTIMISTIC_FORCE_INCREMENT: 这种锁模式会强制递增版本,即使实体数据未发生改变。确保您确实需要这种行为,并且所有可能被此锁模式影响的实体都已配置 @Version 字段。
- 理解 FETCH 的作用: LEFT JOIN FETCH 不仅仅是查询数据,它还会将关联实体加载到持久化上下文中。这意味着这些实体也会受到当前事务和锁模式的影响。
- 优先考虑方案一: 在多数情况下,移除不必要的 LockModeType.OPTIMISTIC_FORCE_INCREMENT 是更简洁有效的解决方案,因为父实体的版本通常足以管理其关联集合的并发修改。只有当有特殊需求时,才考虑为集合元素添加独立的版本字段。
通过以上分析和解决方案,您应该能够有效解决 cannot force version increment on non-versioned entity 异常,并更好地理解Hibernate的乐观锁机制。
评论(已关闭)
评论已关闭