JPA @OnetoOne self reference relationship with both columns non null

Tags: , , , ,



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.

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;
}


Source: stackoverflow