使用@Version时,如何避免关联关系变更触发实体版本更新?
当实体同时具备@Version字段和ManyToMany关联关系时,修改关联关系(例如为Shop添加Worker)会触发该实体的更新操作,导致version值被修改。希望在保留@Version乐观锁功能的前提下,避免每次关联关系变更都更新版本。
示例代码
Shop实体
@Entity @Getter @Setter @AllArgsConstructor @NoArgsConstructor @EntityListeners(AuditingEntityListener.class) public class Shop { @NotNull @LastModifiedDate @Column(name = "last_updated") private Instant lastUpdated; @NotNull @Version @Column(name="version") private Long version; @Id @SequenceGenerator(name = "shop_id_seq", sequenceName = "shop_id_seq", allocationSize = 1) @GeneratedValue( generator = "shop_id_seq", strategy = GenerationType.SEQUENCE ) private Long id; private String name; @ManyToOne() private Location location; @OneToMany(mappedBy = "shop") private List<ShopProduct> shopProducts; @ManyToMany() @JoinTable( name="shop_worker", joinColumns = @JoinColumn(name = "shop_id"), inverseJoinColumns = @JoinColumn(name = "worker_id") ) private Set<Worker> workers = new HashSet<>(); public void addWorker(Worker worker) { workers.add(worker); worker.getShops().add(this); } public void removeWorker(Worker worker) { workers.remove(worker); worker.getShops().remove(this); } }
Worker实体
@Entity @Getter @Setter @AllArgsConstructor @NoArgsConstructor @EntityListeners(AuditingEntityListener.class) public class Worker { @NotNull @LastModifiedDate @Column(name = "last_updated") private Instant lastUpdated; @Id @SequenceGenerator(name = "worker_id_seq", sequenceName = "worker_id_seq", allocationSize = 1) @GeneratedValue( generator = "worker_id_seq", strategy = GenerationType.SEQUENCE ) private Long id; private String name; @ManyToMany(mappedBy = "workers") public Set<Shop> shops = new HashSet<>(); public void addShop(Shop shop) { shops.add(shop); shop.getWorkers().add(this); } public void removeShop(Shop shop) { shops.remove(shop); shop.getWorkers().remove(this); } }
测试代码
@Test @Transactional void testAddingShopToWorker(){ System.out.println("TEST SETUP"); var shop = new Shop(); shop.setName("Shop1"); em.persist(shop); em.flush(); em.clear(); System.out.println("TEST BEGINS"); Shop myShop = em.find(Shop.class, 1L); var worker = new Worker(); worker.setName("John"); worker.addShop(myShop); workerRepository.save(worker); em.flush(); // hibernate triggers update on shop even though its not needed }
日志输出
Hibernate: update shop set last_updated=?, location_id=?, name=?, version=? where id=? and version=?
注:注释掉version字段后,Hibernate仅向中间表插入数据,不会触发Shop的更新操作,符合预期。
解决方案
1. 使用@OptimisticLock(excluded = true)排除关联字段
在Shop实体的workers集合字段上添加@OptimisticLock(excluded = true),告知Hibernate该集合的变更不触发版本更新:
@ManyToMany() @JoinTable( name="shop_worker", joinColumns = @JoinColumn(name = "shop_id"), inverseJoinColumns = @JoinColumn(name = "worker_id") ) @OptimisticLock(excluded = true) // 添加此注解 private Set<Worker> workers = new HashSet<>();
该注解会让Hibernate在检查乐观锁时忽略该字段的变更,仅当实体本身的属性(如name、location)修改时才更新version值。
2. 映射独立的中间表实体(推荐方案)
将ManyToMany关联的中间表shop_worker映射为独立的实体(例如ShopWorker),用两个OneToMany关联替代原有的ManyToMany关系:
@Entity @Table(name = "shop_worker") public class ShopWorker { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "shop_id") private Shop shop; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "worker_id") private Worker worker; // 构造方法、getter、setter }
修改Shop和Worker实体的关联逻辑:
// Shop类 @OneToMany(mappedBy = "shop", cascade = CascadeType.ALL, orphanRemoval = true) private Set<ShopWorker> shopWorkers = new HashSet<>(); public void addWorker(Worker worker) { ShopWorker shopWorker = new ShopWorker(); shopWorker.setShop(this); shopWorker.setWorker(worker); shopWorkers.add(shopWorker); worker.getShopWorkers().add(shopWorker); } // Worker类 @OneToMany(mappedBy = "worker", cascade = CascadeType.ALL, orphanRemoval = true) private Set<ShopWorker> shopWorkers = new HashSet<>();
这种方式下,关联关系的变更只会操作中间实体ShopWorker,不会触发Shop或Worker的版本更新,同时还能灵活扩展中间表的属性(例如添加关联创建时间、状态等)。
3. 使用@Immutable标记集合(限制较大,谨慎使用)
如果Shop的workers集合仅用于查询,不需要通过Shop侧修改关联,可以添加@Immutable注解:
@ManyToMany() @JoinTable( name="shop_worker", joinColumns = @JoinColumn(name = "shop_id"), inverseJoinColumns = @JoinColumn(name = "worker_id") ) @Immutable private Set<Worker> workers = new HashSet<>();
注意:该注解会让集合变为只读,无法通过Shop侧的addWorker或removeWorker方法修改关联,仅能通过Worker侧维护关系,适用场景有限。
验证效果
应用上述任意一种方案后,执行测试代码,Hibernate只会向中间表插入数据,不会触发Shop的update语句,version值将保持不变。
内容的提问来源于stack exchange,提问作者Holden Karl Hain

