1. 概述

在这个教程中,我们将探讨如何解决一个常见的Hibernate错误——“org.hibernate.TransientObjectException: 对象引用了一个未保存的瞬态实例”。当我们在尝试持久化一个被管理的实体(Managed Entity),而这个实体引用了一个未保存的瞬态实例时,就会遇到这个错误。

2. 问题描述

TransientObjectException 是在用户将瞬态实例传递给期望持久实例的会话方法时抛出的。“当用户将瞬态实例传递给期望持久实例的会话方法时”。

避免这种异常的最直接解决方案是通过保存一个新的实例或从数据库中获取一个已存在的实例,并在依赖实例中关联它,然后再进行持久化。然而,这仅解决了特定场景,不适用于其他情况。

为了涵盖所有场景,我们需要一个解决方案来在依赖于其他实体存在的实体关系上级联我们的保存、更新和删除操作。我们可以通过在实体关联中使用适当的CascadeType来实现这一点。

接下来的章节中,我们将创建一些Hibernate实体及其关联,然后尝试保存这些实体,看看为什么会话会抛出异常。最后,我们将通过使用合适的CascadeType(s)来解决这些异常。

3. 一对一关联

在这一节,我们将了解如何解决一对一关联中的TransientObjectException

3.1. 实体

首先,创建一个User实体:

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @OneToOne
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    private Address address;

    // standard getters and setters
}

接着,创建关联的Address实体:

@Entity
@Table(name = "address")
public class Address {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "city")
    private String city;

    @Column(name = "street")
    private String street;

    @OneToOne(mappedBy = "address")
    private User user;

    // standard getters and setters
}

3.2. 引发错误

接下来,添加一个单元测试来将一个User保存到数据库:

@Test
public void whenSaveEntitiesWithOneToOneAssociation_thenSuccess() {
    User user = new User("Bob", "Smith");
    Address address = new Address("London", "221b Baker Street");
    user.setAddress(address);
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    session.save(user);
    session.getTransaction().commit();
    session.close();
}

运行上述测试时,我们会得到一个异常:

java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.baeldung.hibernate.exception.transientobject.entity.Address

在这个例子中,我们将一个新的、瞬态的Address实例与一个新的、瞬态的User实例关联。然后,在尝试保存User实例时,由于Hibernate会话期望Address实体是一个持久实例,所以得到了TransientObjectException。换句话说,当保存User时,Address应该已经在数据库中存在。

3.3. 解决错误

最后,更新User实体,并为User-Address关联使用适当的CascadeType:

@Entity
@Table(name = "user")
public class User {
    ...
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    private Address address;
    ...
}

现在,每当保存/删除一个User时,Hibernate会话也会保存/删除关联的Address,并且不会抛出TransientObjectException

4. 一对多和多对一关联

在这一节,我们将了解如何解决一对多和多对一关联中的TransientObjectException

4.1. 实体

首先,创建一个Employee实体:

@Entity
@Table(name = "employee")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String name;

    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;

    // standard getters and setters
}

以及关联的Department实体:

@Entity
@Table(name = "department")
public class Department {

    @Id
    @Column(name = "id")
    private String id;

    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "department")
    private Set<Employee> employees = new HashSet<>();

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }

    // standard getters and setters
}

4.2. 引发错误

接下来,添加一个单元测试将一个Employee保存到数据库:

@Test
public void whenPersistEntitiesWithOneToManyAssociation_thenSuccess() {
    Department department = new Department();
    department.setName("IT Support");
    Employee employee = new Employee("John Doe");
    employee.setDepartment(department);
    
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    session.persist(employee);
    session.getTransaction().commit();
    session.close();
}

运行上述测试时,我们会得到一个异常:

java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.baeldung.hibernate.exception.transientobject.entity.Department

在这个例子中,我们将一个新的、瞬态的Employee实例与一个新的、瞬态的Department实例关联。尝试保存Employee实例时,由于Hibernate会话期望Department实体是一个持久实例,所以得到了TransientObjectException。换句话说,当保存Employee时,Department应该已经在数据库中存在。

4.3. 解决错误

最后,更新Employee实体,并为Employee-Department关联使用适当的CascadeType:

@Entity
@Table(name = "employee")
public class Employee {
    ...
    @ManyToOne
    @Cascade(CascadeType.SAVE_UPDATE)
    @JoinColumn(name = "department_id")
    private Department department;
    ...
}

同时,更新Department实体,为其与Employee的关联使用适当的CascadeType:

@Entity
@Table(name = "department")
public class Department {
    ...
    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Employee> employees = new HashSet<>();
    ...
}

通过在Employee-Department关联上使用**@Cascade(CascadeType.SAVE_UPDATE)*,当我们将新的Department实例与新的Employee实例关联并保存Employee时,Hibernate会话也会保存关联的Department*实例。

同样地,通过在Department-Employees关联上使用cascade = CascadeType.ALL,Hibernate会将所有操作从Department级联到关联的Employee(s),例如,删除一个Department将移除与之关联的所有Employee(s)。

5. 多对多关联

在这一节,我们将了解如何解决多对多关联中的TransientObjectException

5.1. 实体

创建一个Book实体:

@Entity
@Table(name = "book")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "title")
    private String title;

    @ManyToMany
    @JoinColumn(name = "author_id")
    private Set<Author> authors = new HashSet<>();

    public void addAuthor(Author author) {
        authors.add(author);
    }

    // standard getters and setters
}

以及关联的Author实体:

@Entity
@Table(name = "author")
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "name")
    private String name;

    @ManyToMany
    @JoinColumn(name = "book_id")
    private Set<Book> books = new HashSet<>();

    public void addBook(Book book) {
        books.add(book);
    }

    // standard getters and setters
}

5.2. 引发问题

接下来,添加一些单元测试分别保存带有多个作者的Book和带有多个书籍的Author到数据库:

@Test
public void whenSaveEntitiesWithManyToManyAssociation_thenSuccess_1() {
    Book book = new Book("Design Patterns: Elements of Reusable Object-Oriented Software");
    book.addAuthor(new Author("Erich Gamma"));
    book.addAuthor(new Author("John Vlissides"));
    book.addAuthor(new Author("Richard Helm"));
    book.addAuthor(new Author("Ralph Johnson"));
    
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    session.save(book);
    session.getTransaction().commit();
    session.close();
}

@Test
public void whenSaveEntitiesWithManyToManyAssociation_thenSuccess_2() {
    Author author = new Author("Erich Gamma");
    author.addBook(new Book("Design Patterns: Elements of Reusable Object-Oriented Software"));
    author.addBook(new Book("Introduction to Object Orient Design in C"));
    
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    session.save(author);
    session.getTransaction().commit();
    session.close();
}

运行这些测试时,我们会得到相应的异常:

java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.baeldung.hibernate.exception.transientobject.entity.Author

java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.baeldung.hibernate.exception.transientobject.entity.Book

同样地,在这些示例中,当我们关联新的、瞬态的实例到某个实例并尝试保存时,也会出现TransientObjectException

5.3. 解决问题

最后,更新Author实体,并为Authors-*Book*s关联使用适当的CascadeType:

@Entity
@Table(name = "author")
public class Author {
    ...
    @ManyToMany
    @Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.PERSIST})
    @JoinColumn(name = "book_id")
    private Set<Book> books = new HashSet<>();
    ...
}

同样地,更新Book实体,为Books-*Author*s关联使用适当的CascadeType:

@Entity
@Table(name = "book")
public class Book {
    ...
    @ManyToMany
    @Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.PERSIST})
    @JoinColumn(name = "author_id")
    private Set<Author> authors = new HashSet<>();
    ...
}

需要注意的是,在多对多关联中不能使用CascadeType.ALL,因为我们不想在删除一个Author时删除对应的Book,反之亦然

6. 总结

总之,本文展示了如何通过定义适当的CascadeType来解决“org.hibernate.TransientObjectException: 对象引用了一个未保存的瞬态实例”错误。

如往常一样,您可以在GitHub上的代码示例中找到这个例子的完整代码。


« 上一篇: Java Weekly, 第406期