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:

Company-Employee.png
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;
        }
    }
}
shadow-left