1. 概述

在这个教程中,我们将学习Hibernate的PersistentObjectException,它会在尝试保存一个已分离的实体时出现。

首先,我们会理解什么是已分离状态,以及Hibernate的persistmerge方法之间的区别。然后,我们将在不同的用例中重现这个错误,展示如何修复它。

2. 已分离的实体

让我们先回顾一下什么是已分离状态,以及它与实体生命周期的关系【实体生命周期】。

一个已分离的实体是指不再被持久化上下文跟踪的Java对象。当我们关闭或清空会话时,实体可能会进入这种状态。同样,我们也可以通过手动从持久化上下文中移除实体来使其分离。

本文将使用PostComment实体作为代码示例。要分离特定的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特有的方法,如updatesavesaveOrUpdate【Hibernate的保存、持久化、更新、合并和保存或更新】。与persistmerge不同,这些方法不属于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");
}

相反,如果级联类型设置为PERSISTALL,Hibernate会尝试在已分离的关联字段上传播persist操作。因此,当我们使用这些级联类型之一来persist``Post实体时,Hibernate会试图persist关联的已分离Comment,这将导致另一个PersistentObjectException

5. 总结

在这篇文章中,我们讨论了Hibernate的PersistentObjectException及其主要原因。我们可以通过正确使用Hibernate的savepersistupdatemergesaveOrUpdate方法来避免它。

此外,合理利用JPA级联类型将防止PersistentObjectException出现在我们的实体关联中。

如往常一样,文章的源代码可以在GitHub上找到。