[JIRA] (HHH-16628) Two transactions within the same session cause unsaved transient instance error
by Tomáš Müller (JIRA)
Tomáš Müller ( https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=5b50823... ) *updated* an issue
Hibernate ORM ( https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiODVlMWE0MDJk... ) / Bug ( https://hibernate.atlassian.net/browse/HHH-16628?atlOrigin=eyJpIjoiODVlMW... ) HHH-16628 ( https://hibernate.atlassian.net/browse/HHH-16628?atlOrigin=eyJpIjoiODVlMW... ) Two transactions within the same session cause unsaved transient instance error ( https://hibernate.atlassian.net/browse/HHH-16628?atlOrigin=eyJpIjoiODVlMW... )
Change By: 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. In several cases, one Hibernate session is opened for each request done on the server. However, the request may chain multiple transactions which share the same Hibernate session. This has never been an issue in the past, but now it is causing TransientObjectException errors.
I have been able to isolate the following case. Imagine having students and courses and a table/entity mapping enrollments of students to courses containing additional fields like timestamp, grade, etc.
Student entity:
{noformat}@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);
}
}{noformat}
Course entity:
{noformat}@Entity
@Table(name = "course")
public class Course {
private UUID id;
private String name;
private Integer credit;
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; }
@Column(name = "credit")
public Integer getCredit() { return credit; }
public void setCredit(Integer credit) { this.credit = credit; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "course", 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);
}
}
{noformat}
Enrollment entity (many-to-many relation between courses and students):
{noformat}@Entity
@Table(name = "enrollment")
public class Enrollment {
private UUID id;
private Integer score;
private Student student;
private Course course;
@Id
@GeneratedValue
@Column(name = "id")
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
@Column(name = "score")
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
@ManyToOne(optional = false)
@JoinColumn(name = "student_id", nullable = false)
public Student getStudent() { return student; }
public void setStudent(Student student) { this.student = student; }
@ManyToOne(optional = false)
@JoinColumn(name = "course_id", nullable = false)
public Course getCourse() { return course; }
public void setCourse(Course course) { this.course = course; }
}{noformat}
Now, imagine that a student and two courses already exist, and we want to enroll the student into the two courses like this. And in the following transaction credit information of one of the two courses is updated:
{noformat}// Shared session
final Session hibSession = sf.openSession();
// FIRST TRANSACTION
Transaction t1 = hibSession.beginTransaction();
// Lookup the student and the two courses
Student s1 = hibSession.createQuery("from Student where name = :name", Student.class).setParameter("name", "John").uniqueResult();
Course c1 = hibSession.createQuery("from Course where name = :name", Course.class).setParameter("name", "ENGL 101").uniqueResult();
Course c2 = hibSession.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);
// merge student & commit
hibSession.merge(s1);
t1.commit();
// SECOND TRANSACTION
Transaction t2 = hibSession.beginTransaction();
// Lookup a course and update credit information
Course course = hibSession.createQuery("from Course where name = :name", Course.class).setParameter("name", "ENGL 101").uniqueResult();
course.setCredit(3);
// merge course & commit
hibSession.merge(course);
t2.commit();
// Shared session is closed at the end
hibSession.close();{noformat}
The course lookup in the second transaction (like 29) fails with the following exception:
{noformat}java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: org.hibernate.bugs.twotrans.Enrollment
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:152)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:378)
at org.hibernate.query.spi.AbstractSelectionQuery.uniqueResult(AbstractSelectionQuery.java:467)
at org.hibernate.bugs.twotrans.TwoTransactionsTest.twoTransactionsTestMerge(TwoTransactionsTest.java:65)
...
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: org.hibernate.bugs.twotrans.Enrollment
at org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:346)
at org.hibernate.collection.spi.AbstractPersistentCollection.getOrphans(AbstractPersistentCollection.java:1296)
at org.hibernate.collection.spi.PersistentSet.getOrphans(PersistentSet.java:94)
at org.hibernate.engine.spi.CollectionEntry.getOrphans(CollectionEntry.java:393)
at org.hibernate.engine.internal.Cascade.deleteOrphans(Cascade.java:601)
at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:583)
at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:477)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:437)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:153)
at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:155)
at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:145)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:79)
at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:48)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1375)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$0(ConcreteSqmSelectQueryPlan.java:107)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:302)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:243)
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:518)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:367)
... 45 more{noformat}
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
* a new session is opened for the second transaction
Is this behavior intentional? This has not been an issue in the past using Hibernate 3 or 4.
See the attached files for the whole test.
[^TwoTransactionsTest.java]
[^Student.java]
[^Enrollment.java]
[^Course.java]
( https://hibernate.atlassian.net/browse/HHH-16628#add-comment?atlOrigin=ey... ) Add Comment ( https://hibernate.atlassian.net/browse/HHH-16628#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=EmailN... ) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:d57183e )
2 years, 11 months
[JIRA] (HHH-16628) Two transactions within the same session cause unsaved transient instance error
by Tomáš Müller (JIRA)
Tomáš Müller ( https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=5b50823... ) *created* an issue
Hibernate ORM ( https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiOTIxYTk5YzRj... ) / Bug ( https://hibernate.atlassian.net/browse/HHH-16628?atlOrigin=eyJpIjoiOTIxYT... ) HHH-16628 ( https://hibernate.atlassian.net/browse/HHH-16628?atlOrigin=eyJpIjoiOTIxYT... ) Two transactions within the same session cause unsaved transient instance error ( https://hibernate.atlassian.net/browse/HHH-16628?atlOrigin=eyJpIjoiOTIxYT... )
Issue Type: Bug Affects Versions: 6.2.2 Assignee: Unassigned Components: hibernate-core Created: 18/May/2023 02: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. In several cases, one Hibernate session is opened for each request done on the server. However, the request may chain multiple transactions which share the same Hibernate session. This has never been an issue in the past, but now it is causing TransientObjectException errors.
I have been able to isolate the following case. 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 Integer credit;
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; }
@Column(name = "credit")
public Integer getCredit() { return credit; }
public void setCredit(Integer credit) { this.credit = credit; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "course", 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 = "enrollment")
public class Enrollment {
private UUID id;
private Integer score;
private Student student;
private Course course;
@Id
@GeneratedValue
@Column(name = "id")
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
@Column(name = "score")
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
@ManyToOne(optional = false)
@JoinColumn(name = "student_id", nullable = false)
public Student getStudent() { return student; }
public void setStudent(Student student) { this.student = student; }
@ManyToOne(optional = false)
@JoinColumn(name = "course_id", nullable = false)
public Course getCourse() { return course; }
public void setCourse(Course course) { this.course = course; }
}
Now, imagine that a student and two courses already exist, and we want to enroll the student into the two courses like this. And in the following transaction credit information of one of the two courses is updated:
// Shared session
final Session hibSession = sf.openSession();
// FIRST TRANSACTION
Transaction t1 = hibSession.beginTransaction();
// Lookup the student and the two courses
Student s1 = hibSession.createQuery("from Student where name = :name", Student.class).setParameter("name", "John").uniqueResult();
Course c1 = hibSession.createQuery("from Course where name = :name", Course.class).setParameter("name", "ENGL 101").uniqueResult();
Course c2 = hibSession.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);
// merge student & commit
hibSession.merge(s1);
t1.commit();
// SECOND TRANSACTION
Transaction t2 = hibSession.beginTransaction();
// Lookup a course and update credit information
Course course = hibSession.createQuery("from Course where name = :name", Course.class).setParameter("name", "ENGL 101").uniqueResult();
course.setCredit(3);
// merge course & commit
hibSession.merge(course);
t2.commit();
// Shared session is closed at the end
hibSession.close();
The course lookup in the second transaction (like 29) fails with the following exception:
java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: org.hibernate.bugs.twotrans.Enrollment
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:152)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:378)
at org.hibernate.query.spi.AbstractSelectionQuery.uniqueResult(AbstractSelectionQuery.java:467)
at org.hibernate.bugs.twotrans.TwoTransactionsTest.twoTransactionsTestMerge(TwoTransactionsTest.java:65)
...
Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: org.hibernate.bugs.twotrans.Enrollment
at org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:346)
at org.hibernate.collection.spi.AbstractPersistentCollection.getOrphans(AbstractPersistentCollection.java:1296)
at org.hibernate.collection.spi.PersistentSet.getOrphans(PersistentSet.java:94)
at org.hibernate.engine.spi.CollectionEntry.getOrphans(CollectionEntry.java:393)
at org.hibernate.engine.internal.Cascade.deleteOrphans(Cascade.java:601)
at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:583)
at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:477)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:437)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:153)
at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:155)
at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:145)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:79)
at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:48)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1375)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$0(ConcreteSqmSelectQueryPlan.java:107)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:302)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:243)
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:518)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:367)
... 45 more
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
* a new session is opened for the second transaction
Is this behavior intentional? This has not been an issue in the past using Hibernate 3 or 4.
See the attached files for the whole test.
( https://hibernate.atlassian.net/browse/HHH-16628#add-comment?atlOrigin=ey... ) Add Comment ( https://hibernate.atlassian.net/browse/HHH-16628#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=EmailN... ) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:d57183e )
2 years, 11 months
[JIRA] (HHH-16627) Duplicate records being created for many-to-many relation implemented as two one-to-many relations with cascading
by Tomáš Müller (JIRA)
Tomáš Müller ( https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=5b50823... ) *updated* an issue
Hibernate ORM ( https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiNDAwYmY1ZWFh... ) / Bug ( https://hibernate.atlassian.net/browse/HHH-16627?atlOrigin=eyJpIjoiNDAwYm... ) HHH-16627 ( https://hibernate.atlassian.net/browse/HHH-16627?atlOrigin=eyJpIjoiNDAwYm... ) 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=eyJpIjoiNDAwYm... )
Change By: 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:
{noformat}@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);
}
}{noformat}
Course entity:
{noformat}@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);
}
}{noformat}
Enrollment entity (many-to-many relation between courses and students):
{noformat}@Entity
@Table(name = " student enrollment ")
public class Student Enrollment {
private UUID id;
private String name Integer score ;
private Set<Enrollment> enrollments Student student ;
private Course course;
@Id
@GeneratedValue
@Column(name = "id")
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
@Column(name = " name score ")
public String getName Integer getScore () { return name score ; }
public void setName setScore ( String name Integer score ) { this. name score = name score ; }
@ OneToMany ManyToOne ( fetch optional = FetchType.LAZY, mappedBy false)
@JoinColumn(name = " student student_id ", cascade nullable = { CascadeType.ALL }, orphanRemoval = true false )
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public Set<Enrollment> getEnrollments Student getStudent () { return enrollments student ; }
public void setEnrollments setStudent ( Set<Enrollment> enrollments Student student ) { this. enrollments student = enrollments student ; }
public void addEnrollment
@ManyToOne ( Enrollment enrollment optional = false ) {
if @JoinColumn ( enrollments name = "course_id", nullable = null false ) enrollments = new HashSet<Enrollment>
public Course getCourse () { return course ; }
enrollments.add public void setCourse ( enrollment Course course ) { this.course = course ;
}
}{noformat}
Now, imagine that a student and two courses already exist, and we want to enroll the student into the two courses like this:
{noformat}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();{noformat}
This, however, creates four records, two for each relation. The following code returns four lines
{noformat}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();{noformat}
Returns the following output
{quote}{{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}}{quote}
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=EmailN... ) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:d57183e )
2 years, 11 months
[JIRA] (HHH-16627) Duplicate records being created for many-to-many relation implemented as two one-to-many relations with cascading
by Tomáš Müller (JIRA)
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=EmailN... ) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:d57183e )
2 years, 11 months