1. 概述
在这个教程中,我们将学习Hibernate的PersistentObjectException
,它会在尝试保存一个已分离的实体时出现。
首先,我们会理解什么是已分离状态,以及Hibernate的persist
和merge
方法之间的区别。然后,我们将在不同的用例中重现这个错误,展示如何修复它。
2. 已分离的实体
让我们先回顾一下什么是已分离状态,以及它与实体生命周期的关系【实体生命周期】。
一个已分离的实体是指不再被持久化上下文跟踪的Java对象。当我们关闭或清空会话时,实体可能会进入这种状态。同样,我们也可以通过手动从持久化上下文中移除实体来使其分离。
本文将使用Post
和Comment
实体作为代码示例。要分离特定的Post
实体,可以使用session.evict(post)
。通过调用session.clear()
,我们可以清除所有上下文中的实体。
例如,有些测试需要一个已分离的Post
。现在来看看如何实现:
@Before
public void beforeEach() {
session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
this.detachedPost = new Post("Hibernate Tutorial");
session.persist(detachedPost);
session.evict(detachedPost);
}
首先,我们持久化了Post
实体,然后使用session.evict(post)
将其分离。
3. 尝试保存已分离的实体
如果我们尝试保存一个已分离的实体,Hibernate将抛出一个带有“尝试保存已分离实体”错误消息的PersistenceException
。
让我们尝试保存一个已分离的Post
实体,以预见到这个异常:
@Test
public void givenDetachedPost_whenTryingToPersist_thenThrowException() {
detachedPost.setTitle("Hibernate Tutorial for Absolute Beginners");
assertThatThrownBy(() -> session.persist(detachedPost))
.isInstanceOf(PersistenceException.class)
.hasMessageContaining("detached entity passed to persist: com.baeldung.hibernate.exception.detachedentity.entity.Post");
}
为了避免这种情况,我们需要了解实体状态并使用合适的保存方法。
如果使用merge
方法,Hibernate将根据@Id
字段重新将实体与持久化上下文关联:
@Test
public void givenDetachedPost_whenTryingToMerge_thenNoExceptionIsThrown() {
detachedPost.setTitle("Hibernate Tutorial for Beginners");
session.merge(detachedPost);
session.getTransaction().commit();
List<Post> posts = session.createQuery("Select p from Post p", Post.class).list();
assertThat(posts).hasSize(1);
assertThat(posts.get(0).getTitle())
.isEqualTo("Hibernate Tutorial for Beginners");
}
同样,我们还可以使用其他Hibernate特有的方法,如update
、save
和saveOrUpdate
【Hibernate的保存、持久化、更新、合并和保存或更新】。与persist
和merge
不同,这些方法不属于JPA规范。因此,如果我们想利用JPA抽象,应该避免使用它们。
4. 通过关联保存已分离的实体
在本示例中,我们将引入Comment
实体:
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
@ManyToOne(cascade = CascadeType.MERGE)
private Post post;
// constructor, getters and setters
}
可以看到,Comment
实体与Post
有一个一对多关系。
级联类型设置为CascadeType.MERGE
;因此,我们只会将merge
操作传播到关联的Post
。
换句话说,如果我们merge
一个Comment
实体,Hibernate会将操作传播到关联的Post
,并将两个实体都更新到数据库。然而,如果我们想使用这个设置来persist
一个Comment
,我们首先必须merge
关联的Post
:
@Test
public void givenDetachedPost_whenMergeAndPersistComment_thenNoExceptionIsThrown() {
Comment comment = new Comment("nice article!");
Post mergedPost = (Post) session.merge(detachedPost);
comment.setPost(mergedPost);
session.persist(comment);
session.getTransaction().commit();
List<Comment> comments = session.createQuery("Select c from Comment c", Comment.class).list();
Comment savedComment = comments.get(0);
assertThat(savedComment.getText()).isEqualTo("nice article!");
assertThat(savedComment.getPost().getTitle())
.isEqualTo("Hibernate Tutorial");
}
相反,如果级联类型设置为PERSIST
或ALL
,Hibernate会尝试在已分离的关联字段上传播persist
操作。因此,当我们使用这些级联类型之一来persist``Post
实体时,Hibernate会试图persist
关联的已分离Comment
,这将导致另一个PersistentObjectException
。
5. 总结
在这篇文章中,我们讨论了Hibernate的PersistentObjectException
及其主要原因。我们可以通过正确使用Hibernate的save
、persist
、update
、merge
和saveOrUpdate
方法来避免它。
此外,合理利用JPA级联类型将防止PersistentObjectException
出现在我们的实体关联中。
如往常一样,文章的源代码可以在GitHub上找到。