1. Обзор
В этом руководстве мы увидим, как решить распространенную ошибку Hibernate — «org.hibernate.TransientObjectException: объект ссылается на несохраненный временный экземпляр»
. Мы получаем эту ошибку из сеанса Hibernate
, когда пытаемся сохранить управляемый объект , и этот объект ссылается на несохраненный временный экземпляр.
2. Описание проблемы
Исключение TransientObjectException
« вызывается, когда пользователь передает временный экземпляр методу сеанса, который ожидает постоянный экземпляр».
Теперь самым простым решением, позволяющим избежать этого исключения, было бы получение сохраняемого экземпляра требуемой сущности путем либо сохранения нового экземпляра, либо извлечения его из базы данных и связывания его с зависимым экземпляром перед его сохранением . Однако это относится только к этому конкретному сценарию и не подходит для других вариантов использования.
Чтобы охватить все сценарии, нам нужно решение для каскадирования операций сохранения/обновления/удаления для отношений сущностей, которые зависят от существования другой сущности . Мы можем добиться этого, используя правильный CascadeType
в ассоциациях сущностей.
В следующих разделах мы создадим несколько сущностей Hibernate и их ассоциации. Затем мы попытаемся сохранить эти сущности и посмотрим, почему сеанс
выдает исключение. Наконец, мы решим эти исключения, используя правильный CascadeType
(s).
3. Ассоциация @OneToOne
В этом разделе мы увидим, как решить TransientObjectException
в ассоциациях @OneToOne
.
3.1. Сущности
Во-первых, давайте создадим сущность пользователя :
@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. Создание ошибки
Далее мы добавим модульный тест для сохранения пользователя
в базе данных:
@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.foreach.hibernate.exception.transientobject.entity.Address
Здесь, в этом примере, мы связали новый/временный экземпляр Address
с новым/временным экземпляром User
. Затем мы получили TransientObjectException
, когда попытались сохранить экземпляр User
, потому что сеанс Hibernate ожидает, что объект Address
будет постоянным экземпляром . Другими словами, Address
уже должен быть сохранен/доступен в базе данных при сохранении User
.
3.3. Решение ошибки
Наконец, давайте обновим сущность User
и используем правильный CascadeType
для ассоциации User-Address
:
@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. Ассоциации @OneToMany
и @ManyToOne
В этом разделе мы увидим, как решить TransientObjectException
в ассоциациях @OneToMany
и @ManyToOne
.
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
}
И связанный объект отдела :
@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. Создание ошибки
Далее мы добавим модульный тест для сохранения сотрудника
в базе данных:
@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.foreach.hibernate.exception.transientobject.entity.Department
Здесь, в этом примере, мы связали новый/временный экземпляр Employee
с новым/временным экземпляром отдела
. Затем мы получили TransientObjectException
, когда попытались сохранить экземпляр Employee
, потому что сеанс Hibernate ожидает, что сущность Department
будет постоянным экземпляром . Другими словами, Department
уже должен быть сохранен/доступен в базе данных при сохранении Employee
.
4.3. Решение ошибки
Наконец, давайте обновим сущность Employee
и используем правильный CascadeType
для ассоциации Employee-Department
:
@Entity
@Table(name = "employee")
public class Employee {
...
@ManyToOne
@Cascade(CascadeType.SAVE_UPDATE)
@JoinColumn(name = "department_id")
private Department department;
...
}
И давайте обновим сущность отдела
, чтобы использовать правильный CascadeType
для ассоциации отдел-сотрудники
:
@Entity
@Table(name = "department")
public class Department {
...
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Employee> employees = new HashSet<>();
...
}
Теперь, используя @Cascade(CascadeType.SAVE_UPDATE)
в ассоциации Employee-Department , всякий раз,
когда мы связываем новый экземпляр Department
с новым экземпляром Employee
и сохраняем Employee
, сеанс Hibernate также сохраняет связанный экземпляр Department
.
Точно так же, используя cascade = CascadeType.ALL
для ассоциации Department-Employees
, сеанс Hibernate будет каскадировать все операции из отдела
к соответствующему сотруднику
(ам) . Например, удаление отдела
приведет к удалению всех сотрудников
, связанных с этим отделом
.
5. Ассоциация @ManyToMany
В этом разделе мы увидим, как решить TransientObjectException
в ассоциациях @ManyToMany
.
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. Создание проблемы
Далее добавим несколько модульных тестов для сохранения книги
с несколькими авторами и автора
с несколькими книгами в базе данных соответственно:
@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.foreach.hibernate.exception.transientobject.entity.Author
java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.foreach.hibernate.exception.transientobject.entity.Book
Точно так же в этих примерах мы получили TransientObjectException
, когда связали новые/временные экземпляры с экземпляром и попытались сохранить этот экземпляр.
5.3. Решение проблемы
Наконец, давайте обновим сущность Author
и используем правильные CascadeType
для ассоциации Author
s -Book
s:
@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
и используем правильные CascadeType
для ассоциации Book
s- Author
:
@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
в ассоциации @ManyToMany
, потому что мы не хотим удалять Книгу
при удалении Автора
и наоборот .
6. Заключение
Подводя итог, в этой статье показано, как определение правильного CascadeType
может решить ошибку «o
rg.hibernate.TransientObjectException: объект ссылается на несохраненный временный экземпляр»
.
Как всегда, вы можете найти код для этого примера на GitHub .