Tomáš Müller (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=5b50823...
) *created* an issue
Hibernate ORM (
https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiYTVkNTY4NmJi...
) / Bug (
https://hibernate.atlassian.net/browse/HHH-16627?atlOrigin=eyJpIjoiYTVkNT...
) HHH-16627 (
https://hibernate.atlassian.net/browse/HHH-16627?atlOrigin=eyJpIjoiYTVkNT...
) Duplicate records being created for many-to-many relation implemented as two one-to-many
relations with cascading (
https://hibernate.atlassian.net/browse/HHH-16627?atlOrigin=eyJpIjoiYTVkNT...
)
Issue Type: Bug Affects Versions: 6.2.2 Assignee: Unassigned Attachments: Course.java,
DuplicatesTest.java, Enrollment.java, Student.java Components: hibernate-core Created:
18/May/2023 01:33 AM Environment: Java 11 using MySQL 8 or Oracle 18 Priority: Major
Reporter: Tomáš Müller (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=5b50823...
)
I am in the process of migrating an application from Hibernate 4.3 to Hibernate 6.2. I
have noticed something that works differently, causing duplicate records to be created in
the database when there is many-to-many relation implemented with a middle entity
containing additional fields and two many-to-one relations enabling full cascading.
Here is our example (simplified). Imagine having students and courses and a table/entity
mapping enrollments of students to courses containing additional fields like timestamp,
grade, etc.
Student entity:
@Entity
@Table(name = "student")
public class Student {
private UUID id;
private String name;
private Set<Enrollment> enrollments;
@Id
@GeneratedValue
@Column(name = "id")
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
@Column(name = "name")
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "student", cascade = {
CascadeType.ALL }, orphanRemoval = true)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public Set<Enrollment> getEnrollments() { return enrollments; }
public void setEnrollments(Set<Enrollment> enrollments) { this.enrollments =
enrollments; }
public void addEnrollment(Enrollment enrollment) {
if (enrollments == null) enrollments = new HashSet<Enrollment>();
enrollments.add(enrollment);
}
}
Course entity:
@Entity
@Table(name = "course")
public class Course {
private UUID id;
private String name;
private Set<Enrollment> enrollments;
@Id
@GeneratedValue
@Column(name = "id")
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
@Column(name = "name")
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "course", cascade = {
CascadeType.ALL }, orphanRemoval = true)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public Set<Enrollment> getEnrollments() { return enrollments; }
public void setEnrollments(Set<Enrollment> enrollments) { this.enrollments =
enrollments; }
public void addEnrollment(Enrollment enrollment) {
if (enrollments == null) enrollments = new HashSet<Enrollment>();
enrollments.add(enrollment);
}
}
Enrollment entity (many-to-many relation between courses and students):
@Entity
@Table(name = "student")
public class Student {
private UUID id;
private String name;
private Set<Enrollment> enrollments;
@Id
@GeneratedValue
@Column(name = "id")
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
@Column(name = "name")
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "student", cascade = {
CascadeType.ALL }, orphanRemoval = true)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public Set<Enrollment> getEnrollments() { return enrollments; }
public void setEnrollments(Set<Enrollment> enrollments) { this.enrollments =
enrollments; }
public void addEnrollment(Enrollment enrollment) {
if (enrollments == null) enrollments = new HashSet<Enrollment>();
enrollments.add(enrollment);
}
}
Now, imagine that a student and two courses already exist, and we want to enroll the
student into the two courses like this:
final Session hibSession1 = sf.openSession();
Transaction t1 = hibSession1.beginTransaction();
// Lookup the student and the two courses
Student s1 = hibSession1.createQuery("from Student where name = :name",
Student.class).setParameter("name", "John").uniqueResult();
Course c1 = hibSession1.createQuery("from Course where name = :name",
Course.class).setParameter("name", "ENGL 101").uniqueResult();
Course c2 = hibSession1.createQuery("from Course where name = :name",
Course.class).setParameter("name", "BIOL 101").uniqueResult();
// Enroll student to the two courses
Enrollment e1 = new Enrollment();
e1.setScore(10);
e1.setStudent(s1); s1.addEnrollment(e1);
e1.setCourse(c1); c1.addEnrollment(e1);
Enrollment e2 = new Enrollment();
e2.setScore(20);
e2.setStudent(s1); s1.addEnrollment(e2);
e2.setCourse(c2); c2.addEnrollment(e2);
// update student
hibSession1.merge(s1);
t1.commit();
hibSession1.close();
This, however, creates four records, two for each relation. The following code returns
four lines
final Session hibSession2 = sf.openSession();
Transaction t2 = hibSession2.beginTransaction();
List<Enrollment> enrls = hibSession2.createQuery("from Enrollment e where
e.student.name = :name", Enrollment.class).setParameter("name",
"John").list();
System.out.println("Id,Student,Course,Score");
for (Enrollment e: enrls)
System.out.println(e.getId() + ", " + e.getStudent().getName() +
"," + e.getCourse().getName() + "," + e.getScore());
t2.commit();
hibSession2.close();
Returns the following output
Id,Student,Course,Score
7ce95b57-8664-4261-871e-60d5beaf1069, John,BIOL 101,20
2189a22a-3d03-40db-9627-adc57118cbae, John,ENGL 101,10
866c83a3-1dfd-42b1-8d1f-7d8b58168314, John,ENGL 101,10
2d91cc26-3863-4e47-9439-64512a89ad2b, John,BIOL 101,20
The problem does not occur when
* the student is being created together with the new enrollment records (i.e. when calling
hibSession1.persist(s1))
* when each enrollment is persisted before merging the student
* when one of the relations (e.g., Course.enrollments) does not cascade
Is that intentional? We have several such relations in our application, and we never had
this issue in the past using Hibernate 3 or 4.
See the attached files for the whole test.
(
https://hibernate.atlassian.net/browse/HHH-16627#add-comment?atlOrigin=ey...
) Add Comment (
https://hibernate.atlassian.net/browse/HHH-16627#add-comment?atlOrigin=ey...
)
Get Jira notifications on your phone! Download the Jira Cloud app for Android (
https://play.google.com/store/apps/details?id=com.atlassian.android.jira....
) or iOS (
https://itunes.apple.com/app/apple-store/id1006972087?pt=696495&ct=Em...
) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:d57183e )