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

使用@Version与LockModeType.OPTIMISTIC时Hibernate抛异常原因

问题描述

使用Hibernate时遇到如下报错:

Caused by: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : api.models.SaleItem.product -> api.models.Product

但移除@Version字段和LockModeType.OPTIMISTIC后,功能可正常运行。

相关实体类、仓库层及控制器层代码如下:

实体类代码

Product类

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(unique = true, nullable = false)
    @NotNull(message = "name is required")
    @Size(min = 3, max = 50, message = "name must be between 3 and 50 characters")
    private String name;
    @Min(value = 1, message = "price not be less than 1")
    @Max(value = 1000000, message = "price should not be greater than 1000000")
    private float price;
    @Min(value = 0, message = "stock not be less than 0")
    @Max(value = 10000, message = "stock should not be greater than 10000")
    private int stock;
    @Min(value = 1, message = "minimum stock not be less than 1")
    @Max(value = 1000, message = "minimum stock should not be greater than 1000")
    @Column(name = "minimum_stock")
    private int minimumStock;
    private Status status;
    @ImageSize
    private String image;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", nullable = false)
    @NotNull(message = "category cannot be null")
    private Category category;
    @Version
    private Integer version;

    public Product() {
        this.status = Status.ACTIVE;    
    }
    
    public Product(Integer id) {
        this.id=id;
        this.status=Status.ACTIVE;
    }

    @PrePersist
    void onCreate() {
        for (SaleItem item : items)
            item.setSale(this);
        for (Payment payment : payments)
            payment.setSale(this);
        if(this.discount!=null)
            this.discount.setSale(this);
        if(this.surcharge!=null)
            this.surcharge.setSale(this);
    }
    // 省略其他getter/setter
}

SaleItem类

@Entity
@Table(name = "sales_items")
public class SaleItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="product_id")
    @NotNull(message="product cannot be null")
    private Product product;
    @Min(value = 1, message = "quantity not be less than 1")
    @Max(value = 10000, message = "quantity should not be greater than 10000")
    private int quantity;
    @Min(value = 0, message = "price not be less than 1")
    @Max(value = 1000000, message = "price should not be greater than 1000000")
    private float price;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "sale_id")
    @JsonIgnore
    private Sale sale;
    // 省略其他getter/setter
}

仓库层代码

public Sale save(Sale entity) {
    EntityManager em = emf.createEntityManager();
    try {
        em = emf.createEntityManager(); // 重复创建EntityManager,需修复
        em.getTransaction().begin();
        em.persist(entity);
        updateStock(entity.getItems(), em);
        em.getTransaction().commit();
        em.close();
        return entity;
    } catch (PersistenceException e) {
        em.getTransaction().rollback();
        e.printStackTrace();
    } finally {
        em.close();
    }
}

private void updateStock(List<SaleItem> items, EntityManager em) {
    for (SaleItem item : items) {
        Product product = em.find(Product.class, item.getProduct().getId(),LockModeType.OPTIMISTIC);
        product.setStock(product.getStock() - item.getQuantity());
    }
}

控制器层代码

public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    Sale sale = mapper.readValue(Util.getPayload(request), Sale.class);
    List<String> errors = validate(sale);
    if (!errors.isEmpty())
        Util.sendAsJson(response, errors, mapper);
    else {
        sale.setUser(new User(Double.valueOf(request.getAttribute("id").toString()).intValue()));
        sale.setDate(new Date());
        Util.sendAsJson(response, saleRepo.save(sale), mapper);
    }
}

已尝试更换不同版本的Hibernate,问题仍存在,请问该如何解决?


解决方案

问题根源

报错的核心原因是:调用em.persist(entity)保存Sale时,Sale关联的SaleItem中的Product是仅包含ID的临时对象(transient instance),并未被Hibernate的持久化上下文托管。

未使用@Version和乐观锁时,Hibernate对关联对象的状态检查相对宽松,仅通过ID关联即可完成操作;但启用乐观锁后,Hibernate需要验证关联对象的版本状态,而临时对象无有效版本信息,因此触发TransientPropertyValueException。

修复步骤

  1. 在保存Sale前,将SaleItem中的Product替换为持久化上下文托管的实例
    修改仓库层的save方法,在em.persist(entity)前,先通过EntityManager查询获取已托管的Product实例,并设置回SaleItem:

    public Sale save(Sale entity) {
        EntityManager em = emf.createEntityManager();
        try {
            em.getTransaction().begin();
            // 先将每个SaleItem的Product替换为托管实例
            for (SaleItem item : entity.getItems()) {
                Product managedProduct = em.find(Product.class, item.getProduct().getId());
                item.setProduct(managedProduct);
            }
            em.persist(entity);
            updateStock(entity.getItems(), em);
            em.getTransaction().commit();
            return entity;
        } catch (PersistenceException e) {
            em.getTransaction().rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
    }
    
  2. 修复重复创建EntityManager的问题
    原代码中em = emf.createEntityManager();重复执行,会导致初始创建的EntityManager未被关闭,存在资源泄漏风险,需移除该行冗余代码。

  3. 可选:为SaleItem的Product关联添加nullable=false约束
    在SaleItem的@JoinColumn中显式设置nullable=false,与@NotNull注解保持一致,增强数据完整性约束:

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="product_id", nullable = false)
    @NotNull(message="product cannot be null")
    private Product product;
    

原理说明

通过em.find()获取的Product实例会被Hibernate持久化上下文托管,此时SaleItem关联的是已存在的、受托管的持久化对象,Hibernate可以正常验证其版本信息,乐观锁机制也能正常工作,同时避免了临时对象关联导致的报错。


内容的提问来源于stack exchange,提问作者maxc5

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 23:32:26