I have an existing database table For e.g. T_STUDENTS
on top of which I have to create a JPA entity. All three columns in the table are NON NULL
and the table has a self-reference as mentor_id
id | name | mentor_id -----|---------|---------- 1 | John | 1 -----|---------|---------- 2 | Marc | 1 -----|---------|---------- 3 | Abby | 2 -----|---------|---------- 4 | Jimy | 3 -----|---------|---------- 5 | Boni | 4 -----|---------|----------
Each student has a mentor who is also a student. There is a strict OneToOne relationship between the student and the mentor. For id 1, there can’t be any mentor, therefore it has the mentor id as it’s own id
. The ids are generated using a database sequence.
The problem is that while generating the first record with id 1, hibernate is not assigning the same id as mentor id even though I have created necessary relationships. Since columns can’t be null and hibernate is not assigning mentor_id, SQLConstraint nonnull exception is thrown.
Following is how I have created the relationship.
@Entity @Table(name = 'T_STUDENTS') public class Student implements Serializable { @Id @SequenceGenerator(name = 'S_STUDENTS_SEQUENCE', allocationSize = 1) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 'S_STUDENTS_SEQUENCE') @Column(name = "id") private Long studentId; @Column(name = "name", length = 20) private String studentName; @OneToOne(optional = false, cascade = CascadeType.NONE) @JoinColumn(name = "mentor_id") private Student mentor; // getters and setters }
I have set CascadeType.NONE
because else hibernate tries to retrieve 2 id’s from sequence and tries to create 2 records which are not desirable.
The problem is how can I insert the very first record. Following is how the insert is being done.
Student student = Student.builder() .setName('John') .build(); student = student.toBuilder().setMentor(student).build(); return studentRepository.save(student);
If I change the relationship annotation to @ManyToOne
since technically mentor_id is 1 is mapped to 2 students, I get the following exception
.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation
Edit 1: If relationship type changed to @ManyToOne and cascade is removed following error is observed.
org.hibernate.action.internal.UnresolvedEntityInsertActions.logCannotResolveNonNullableTransientDependencies - HHH000437: Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity. The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
Edit 2: Changed the cascade type to cascade = CascadeType.PERSIST
and hibernate tries to persist the mentor as a separate record. I verified from logs that it tries to retrieve 2 different sequence ids and creates 2 insert queries, with both mentor_id as null.
Advertisement
Answer
NOTE: Finally I found the root cause. I was using Lombok builder in the JPA entity and it does not support the self-reference relationship yet.
I switched to public setters and it worked fine. See the link below for more details https://github.com/rzwitserloot/lombok/issues/2440#event-3270871969
You can ignore the below solution.
I’m not very proud of the solution, but here is how I achieved it.
1.Removed auto sequence generation from the id.
@Id @SequenceGenerator(name = 'S_STUDENTS_SEQUENCE', allocationSize = 1) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 'S_STUDENTS_SEQUENCE') @Column(name = "id") private Long studentId
to
@Id @Column(name = "id") private Long studentId;
2.Changed the mapping to the simple foreign key field.
@OneToOne(optional = false, cascade = CascadeType.NONE) @JoinColumn(name = "mentor_id") private Student mentorId;
to
@Column(name = "mentor_id") private Long mentorId;
3.Created a method to retrieve the sequence manually and then assigned the value to both ‘id’ and ‘mentorId’
@Override public Student saveExtended(Student student) { Object sequence = em.createNativeQuery( "SELECT NEXT VALUE FOR S_STUDENTS_SEQUENCE AS VALUE FROM SYSIBM.SYSDUMMY1") .getSingleResult(); BigInteger sequenceLong = (BigInteger) sequence; student = student.toBuilder().id(sequenceLong.longValue()).mentorId(sequenceLong.longValue()).build(); em.persist(student); em.flush(); return student; }