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=Em...
) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:d57183e )