2

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.

bluelurker
  • 1,353
  • 3
  • 19
  • 27
  • 1. it cannot be a "strict OneToOne", when student 1 can have many. (-> ManyToOne more appropiate/"correct") 2. ->InvalidDataUsage makes sense, when `cascade = CascadeType.NONE` – xerx593 Apr 18 '20 at 13:32
  • Try to save 2 Student, becasue null value for mentor id is not possible, if you set not null for mentor _id then must be a mentor for every student. – Eklavya Apr 18 '20 at 13:47
  • @xerx593 Yes, you are right about OneToOne part. I think I should approach it as a ManyToOne relationship since technically it is because id 1 has ManyToOne r'ship. For second point I removed cascade field. Please see Edit 1 in question – bluelurker Apr 18 '20 at 14:07
  • @AbinashGhosh what do you mean? I have a use case to store only 1 record first. How does saving 2 records help? – bluelurker Apr 18 '20 at 14:08
  • re: EDIT1 don't remove, but try `cascade = PERSIST` (cautiously ... or boldly: `ALL` :) [(removing is equivalent to `NONE`:)](https://stackoverflow.com/a/8048360/592355) – xerx593 Apr 18 '20 at 14:12
  • @xerx593 I already tried that. See Edit 2. – bluelurker Apr 18 '20 at 14:21
  • for 1 record how to set mentor id ? for 2 records you can do like (id-1 mentor-2 )(id-2, mentor-1) atleast – Eklavya Apr 18 '20 at 14:24
  • @xerx593 I think the Cascade type should to be none. since parent is not dependent on child. – bluelurker Apr 18 '20 at 14:31
  • (hen-egg-problem...): set the column nullable (optional = true), persist john, then set john-self-mentor (i think this is the best hack utilizing the seq./auto-id .. the not null constraint should be ensured/forced on "higher level") – xerx593 Apr 18 '20 at 14:35
  • ...or insert "john" (only) via "data.sql" (or so..) – xerx593 Apr 18 '20 at 14:36
  • @xerx593.. setting optional = true won't really help since the constraint is the database side. I have tried that and it didn't work. importing the first record via sql like INSERT INTO T_STUDENT(1, 'John', 1) works but we don't really want to go for that approach. Because we have another column 'subject' and relationships are grouped by subject. So there will be another case where another student will have same id as mentor_id for a diferent subject. Some how if jpa can be instructed to use the same sequence id for id and mentor that could solve the problem. – bluelurker Apr 18 '20 at 14:44
  • ..the conflict is in "id auto generation" vs. "not null mentor" (and only for "john"-like students:) ..you could go with the "data.sql" approach with all "john-like" enities .. and otherwise i googled around [this](https://stackoverflow.com/q/3194721/592355) – xerx593 Apr 18 '20 at 14:57
  • @xerx593 Contrary to the link in your last comment, I have to somehow manage to get the current sequence id and assign it to master_id column. I can replace master_id mapping with simple Long master_id. I was trying to see if I can get the current sequence id and try to set it to master_id usign some JPA / hibernate apis. – bluelurker Apr 18 '20 at 15:59
  • One dirty solution I can think of is to remove SequenceGenerator and GeneratedValue annotations and manually assign the same sequence id to both id and master id. It breaks abstraction but it may work. – bluelurker Apr 18 '20 at 16:01
  • Do you need to generate the first entry with the repository, can't you just create it with a seperate sql query / load a precreated sql dumb with at least 1 dummy entry? – PaulD Apr 20 '20 at 07:09

1 Answers1

1

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;
}
bluelurker
  • 1,353
  • 3
  • 19
  • 27