Hibernate and Collections
When adding/removing entities to/from a collection in Hibernate one may easily run into annoying
TransientObjectException or TransientPropertyValueException exceptions.
This blog uses a simple example to describes how to avoid these issues.
Data Model
Suppose you have the following data model:
Figure 1. Data Model
With the following Java code:
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
// Immutable ID, set upon 'new', independent from DB
@Column(name = "business_key", nullable = false, unique = true, updatable = false)
private String businessKey = UUID.randomUUID().toString();
public Long getId() {
return id;
}
public String getBusinessKey() {
return businessKey;
}
@Override
public final boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof BaseEntity)) return false;
BaseEntity other = (BaseEntity) obj;
return Objects.equals(this.getBusinessKey(), other.getBusinessKey());
}
@Override
public final int hashCode() {
return Objects.hashCode(getBusinessKey());
}
}
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
@Entity
public class Company extends BaseEntity{
@Column(name = "name", length = 100, nullable = false)
public String name;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true)
public Set<Employee> employees;
}
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@Entity
public class Employee extends BaseEntity {
@Column(name = "first_name", length = 100, nullable = false)
public String firstName;
@Column(name = "last_name", length = 100, nullable = false)
public String lastName;
@Column(name = "email", length = 100, nullable = false)
public String email;
@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "fk_employee_company"))
public Company company;
}
The Problem
Until Hibernate version 6.5.3.Final the following code used to work:
import jakarta.persistence.EntityNotFoundException;
import org.springframework.transaction.annotation.Transactional;
public class CompanyService {
@Transactional
public addEmployee(long companyId, String firstName, String lastName, String email) {
var company = repository.findById(companyId);
var employee = new Employee();
employee.firstName = firstName;
employee.lastName = lastName;
employee.email = email;
company.employees.add(employee);
// cascade = CASCADE_ALL ensures this will persist the new employee as well.
repository.save(company);
}
}
But in more recent versions of Hibernate this will likely result in a TransientObjectException or TransientPropertyValueException exception.
The Solution
To avoid these exceptions also set employee.company:
import jakarta.persistence.EntityNotFoundException;
import org.springframework.transaction.annotation.Transactional;
public class CompanyService {
@Transactional
public void addEmployee(long companyId, String firstName, String lastName, String email) {
var company = repository.findById(companyId).orElseThrow(() -> new EntityNotFoundException("Company not found"));
var employee = new Employee();
employee.firstName = firstName;
employee.lastName = lastName;
employee.email = email;
// Link the relationship on both sides.
employee.company = company;
company.employees.add(employee);
// cascade = CASCADE_ALL ensures this will persist the new employee as well.
repository.save(company);
}
}
A similar issues may arise when removing an entity from the collection. So in that case also set employee.company to null like this:
import jakarta.persistence.EntityNotFoundException;
import org.springframework.transaction.annotation.Transactional;
public class CompanyService {
// other methods
@Transactional
public void removeEmployee(long companyId, long employeeId) {
var company = repository.findById(companyId).orElseThrow(() -> new EntityNotFoundException("Company not found"));
var employee = company.emplyees.stream()
.filter(em -> em.getId() == employeeId)
.findFirst()
.orElse(null);
if (employee != null) {
// Unlink the relationship on both sides.
company.emplyees.remove(employee);
employee.company = null;
}
}
}