使用@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。
修复步骤
在保存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(); } }修复重复创建EntityManager的问题
原代码中em = emf.createEntityManager();重复执行,会导致初始创建的EntityManager未被关闭,存在资源泄漏风险,需移除该行冗余代码。可选:为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

