You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 17:54:52