Issues
- Issue with orphanRemoval = true and FetchType.LAZY in Hibernate 6.6.X.FinalHHH-19249
- EntityGraph.addSubgraph fails on entity attribute with more than one level of inheritanceHHH-19242
- Significant increase in heap allocation for queries after migrating Hibernate ORM 6.5 to 6.6HHH-19240
- BeanValidationEventListener not called if only associated collection is updated via getterHHH-19232Resolved issue: HHH-19232Marco Belladelli
- errors in class OracleSDOFunctionDescriptorsHHH-19227Resolved issue: HHH-19227Karel Maesen
- TransientObjectException on session.remove()HHH-19224
- SAP Hana - Memory Leak with Identity GenerationHHH-19222
- Performance decrease for SELECT query with complex WHERE clause in Hibernate 6 (compared with 5)HHH-19213
- Is @EmbeddedId intentionally excluded from Dirty Checking, considering it as immutable?HHH-19212Resolved issue: HHH-19212
- Bytecode-enhanced dirty checking ineffective if entity's embedded ID set manually (to same value)HHH-19206Resolved issue: HHH-19206Christian Beikov
- ClassCastException with usages of static java.sql.Date#from(Instant) that are in fact calling java.util.Date#from(Instant) and returning java.util.Date objectsHHH-19204
- array_intersects doesn't work with an array as a parameterHHH-19202
- Embeddable inheritance: discriminator values are not hierarchically orderedHHH-19195Resolved issue: HHH-19195Čedomir Igaly
13 of 13
Issue with orphanRemoval = true and FetchType.LAZY in Hibernate 6.6.X.Final
Description
Created 20 hours ago
Updated 5 hours ago
Activity
Show:
I’ve encountered an issue after upgrading to Hibernate 6.6.9.Final from 6.4.10.Final related to the behavior of
orphanRemoval = true
when combined withFetchType.LAZY
.Issue Description
In previous Hibernate versions, if I had a lazily-loaded collection with
orphanRemoval = true
and saved the parent entity without explicitly initializing the collection, Hibernate would not attempt to delete any orphans—unless the collection was explicitly modified.However, after updating to 6.6.9.Final, it seems that Hibernate attempts to remove orphans, even if the lazy collection was never loaded or accessed. This behavior is unexpected and leads to incorrect deletions.
The last version where this behavior worked as expected (i.e., the test passes) is 6.5.3.Final. From version 6.6.0.Final onward, this issue starts to occur.
Reproducer:
package org.hibernate.bugs; import static org.junit.jupiter.api.Assertions.assertEquals; import jakarta.persistence.*; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.HashSet; /** * This test case reproduces the scenario where an exercise is updated via an API. * The exercise instance coming from the API is created outside a transaction (detached), * and it does not include its lazy-loaded student participations. The test then verifies * that the existing student participations remain intact after the update. * <p> * Participations are not needed for the update and are not sent in the exercise update request, * because they are large and not necessary for the update. * <p> * This test succeeds with Hibernate version 6.5.3.Final, but fails with version 6.6.X.Final. */ class JPAUnitTestCase { private EntityManagerFactory entityManagerFactory; @BeforeEach void init() { entityManagerFactory = Persistence.createEntityManagerFactory("templatePU"); } @AfterEach void destroy() { entityManagerFactory.close(); } @Test void testUpdateExerciseDoesNotRemoveStudentParticipations() { // Create and persist an exercise with two student participations. EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); Exercise exercise = new Exercise(); exercise.setName("Initial Exercise"); StudentParticipation participation1 = new StudentParticipation(); participation1.setName("Participation 1"); participation1.setExercise(exercise); StudentParticipation participation2 = new StudentParticipation(); participation2.setName("Participation 2"); participation2.setExercise(exercise); exercise.getStudentParticipations().add(participation1); exercise.getStudentParticipations().add(participation2); em.persist(exercise); em.persist(participation1); em.persist(participation2); em.getTransaction().commit(); em.close(); // Simulate the API payload: a detached exercise instance is created outside any transaction. Exercise apiExercise = new Exercise(); apiExercise.setId(exercise.getId()); apiExercise.setName("Updated Exercise"); // Note: The API payload does not include studentParticipations. // Start a new transaction to update the managed entity using the API payload. em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.merge(apiExercise); em.getTransaction().commit(); em.close(); // Verify that the student participations are still present. em = entityManagerFactory.createEntityManager(); Exercise verifiedExercise = em.find(Exercise.class, exercise.getId()); assertEquals("Updated Exercise", verifiedExercise.getName(), "Exercise name should be updated"); assertEquals(2, verifiedExercise.getStudentParticipations().size(), "Student participations should not be removed"); em.close(); } // --- Entity definitions for the purpose of the test --- @Entity(name = "Exercise") public static class Exercise { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "exercise", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) private java.util.Set<StudentParticipation> studentParticipations = new HashSet<>(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public java.util.Set<StudentParticipation> getStudentParticipations() { return studentParticipations; } public void setStudentParticipations(java.util.Set<StudentParticipation> studentParticipations) { this.studentParticipations = studentParticipations; } } @Entity(name = "StudentParticipation") public static class StudentParticipation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToOne private Exercise exercise; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Exercise getExercise() { return exercise; } public void setExercise(Exercise exercise) { this.exercise = exercise; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; StudentParticipation that = (StudentParticipation) o; if (id == null) return false; return id.equals(that.id); } @Override public int hashCode() { return id != null ? id.hashCode() : 0; } } }