使用客户端分配ID保存一对多关联实体时遇JpaObjectRetrievalFailureException
当使用客户端分配ID(比如手动生成UUID)时,Hibernate会将带有非空ID的实体判定为「持久化状态」(即认为该实体已存在于数据库中)。保存Cart时,关联的Item因为已有ID,Hibernate会尝试去数据库查询该Item,但此时Item还未被保存,因此抛出EntityNotFoundException。
而使用@GeneratedValue时,实体ID为null,Hibernate会判定为「临时状态」,自动触发级联保存操作,因此不会报错。
方案1:开启级联持久化操作
在Cart的@OneToMany注解中添加cascade = CascadeType.PERSIST(或CascadeType.ALL,根据业务需求选择),让Hibernate在保存Cart时自动保存关联的Item。
修改Cart实体代码:
@Entity @Table(name = "cart") public class Cart { @Id @Column(name = "id") private UUID id; @Column(name = "name") private String name; @OneToMany(cascade = CascadeType.PERSIST) // 新增级联配置 @JoinColumn(name = "cart_id") private List<Item> items; // constructors, getters, setters, equals and hashCode omitted }
测试代码无需修改,保存Cart时会自动将关联的Item一起插入数据库。
方案2:手动先保存关联实体
如果不想开启级联,可以先手动保存Item,再将其关联到Cart后保存Cart。需要先创建ItemRepository:
public interface ItemRepository extends JpaRepository<Item, UUID> { }
修改测试代码:
@DataJpaTest class CartRepositoryTest { @Autowired private CartRepository cartRepository; @Autowired private ItemRepository itemRepository; // 注入ItemRepository @Test void should_save_cart() { // GIVEN final var cart = new Cart(UUID.randomUUID(), "cart"); final var item = new Item(UUID.randomUUID(), "item"); itemRepository.save(item); // 先保存Item cart.setItems(List.of(item)); // WHEN final var saved = cartRepository.save(cart); // THEN final var fetched = cartRepository.findById(saved.id()); assertThat(fetched).isPresent(); } }
方案3:自定义实体状态判断逻辑
通过让实体实现Persistable接口,重写isNew()方法来告诉Hibernate该实体是否为新实体(需要插入),而不是仅通过ID是否为空判断。
修改Item实体:
@Entity @Table(name = "item") public class Item implements Persistable<UUID> { @Id @Column(name = "id") private UUID id; @Column(name = "name") private String name; // 新增临时字段,标记是否为新实体 @Transient private boolean isNew = true; // constructors, getters, setters, equals and hashCode omitted @Override public UUID getId() { return id; } @Override public boolean isNew() { return isNew; } // 保存后触发,标记为已持久化 @PostPersist public void setNotNew() { this.isNew = false; } }
同时Cart的@OneToMany需要开启cascade = CascadeType.PERSIST,Hibernate会根据isNew()的返回值决定是否插入Item。
优先推荐方案1,它最符合JPA的关联映射设计,代码侵入性最低。如果业务上不允许级联操作,再考虑方案2。方案3适合复杂场景下需要自定义实体状态判断的情况。
内容的提问来源于stack exchange,提问作者Loïc

