Khushi
Khushi

Reputation: 333

Child entity is not getting inserted/updated on parent entity update with onetomany

I am using @OneToMany annotation to save parent and child entities but I am facing issues while saving child entity in a particular case.

Child entity is getting saved in two cases:

  1. During first insert of a parent with child.
  2. During update of a parent with child when there was no child inserted/saved in database in first insert

But When parent is inserted with child 1 and then during update of a parent I try to insert child 2 then I am not able to save the child 2

it is failing with below exception:

o.h.e.jdbc.spi.SqlExceptionHelper - ORA-01407: cannot update ("app_Name"."Child_entity"."REFERENCE_ID") to NULL\n 
23:22:06.068 ERROR o.h.i.ExceptionMapperStandardImpl - HHH000346: Error during managed flush [org.hibernate.exception.ConstraintViolationException: could not execute statement] 

Please see my code as below:

@Data
@Entity
@Table("Parent_table")
public class Parent_entity implements Serializable {


    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval =true)
    @JoinColumn(name="REFERENCE_ID")
    private Set<Child_Entity> childrens ;

    }


@Data
@Entity
@Table("child_table")
public class Child_entity implements Serializable {

    @Id
    @GeneratedValue(generator = "seq_gen", strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(name = "seq_gen", sequenceName = "child_SEQ",allocationSize=1)
    @Column(name ="col_name")
    private Integer asSeq;

    @Column(name ="REFERENCE_ID")
    private String referenceid;

    }   

In mapper class, I am explicitly setting primary key of the parent table.

Oracle database side I have below foreign key constraint added

  ALTER TABLE child_table
  ADD CONSTRAINT FK_parent_table
  FOREIGN KEY (REFERENCE_ID)
  REFERENCES Parent_table(REFERENCE_ID);

I have browsed through similar question on stackoverflow, which suggests that if you are using existing column for foreign key then existing values for that column should not be null. But in my case column "REFERENCE_ID" is already non nullable.

Please let me know or suggest if I need to add something else to make it work.

Thank you.

Edit: In update scenario, Hibernate is generating below query:

update child_table set reference_id=null where reference_id=? and child_seq=?

where reference_id is Parent's primary key and child_seq is Child's primary key

Any idea why hibernate is trying to update Parent's primary key I am explicitly setting Parent Primary key's value in Child's entity

Upvotes: 2

Views: 8009

Answers (1)

Mafor
Mafor

Reputation: 10661

There are actually three problems here:

  1. Apparently "update scenario" inserts two new children instead of keeping one and adding one.
  2. Unidirectional OneToMany relationship with a non-nullable join column
  3. Lombok-generated equals and hashCode

TL;TR: GOTO 2

1. Update scenario

Hibernate is trying to update reference_id to NULL because it wants to "detach" a child entity from the parent. That means, that during update, you are adding two new children instead of keeping one and adding one. I haven't seen the relevant piece of code of yours, but I assume it might look more or less like this:

ParentEntity parent = new ParentEntity();
parent.setId("test");
ChildEntity child1 = new ChildEntity();
child1.setReferenceid(parent.getId());
parent.setChildrens(new HashSet<>(Arrays.asList(child1)));
repository.save(parent);

ChildEntity child2 = new ChildEntity();
child2.setReferenceid(parent.getId());
parent.getChildrens().add(child2);
repository.save(parent);

It ends up with a ConstraintViolationException. In the second save call, child1 is still a "detached" instance, its id is NULL and Hibernate treats both children as they were new. So first it adds them to the child_table and later tries to remove the "old" one, by setting its referenceId to NULL (orphan removal hapens later, and is kind of unrelated). It could be easily fixed:

// ...
parent = repository.save(parent); // <- save(parent) returns updated object

ChildEntity child2 = new ChildEntity();
child2.setReferenceid(parent.getId());
parent.getChildrens().add(child2);
repository.save(parent);

No exceptions anymore but it doesn't solve the problem. Sooner or later you are going to remove a child from the children set and it will always result in an exception.

2. Unidirectional OneToMany relationship with a non-nullable join column

The canonical way of modeling it would be as follows:

ParentEntity

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "REFERENCE_ID", nullable = false)
private Set<ChildEntity> childrens;

ChildEntity

@Column(name = "REFERENCE_ID", insertable = false, updatable = false)
private String referenceid;

It should work but Hibernate will generate unnecessary 'update' queries:

select parententi0_.parent_id as parent_i1_1_1_, childrens1_.reference_id as referenc3_0_3_, childrens1_.id as id1_0_3_, childrens1_.id as id1_0_0_, childrens1_.name as name2_0_ ...
select nextval ('child_seq')
select nextval ('child_seq')
insert into child_table (name, reference_id, id) values (?, ?, ?)
insert into child_table (name, reference_id, id) values (?, ?, ?)
update child_table set reference_id=? where id=?
update child_table set reference_id=? where id=?
delete from child_table where id=?

Not a big deal with one or two items, but with 100? This happens because the ParentEntity is the owner of the relationship (due to the @JoinTable annotation). It knows nothing about child_table foreign key and doesn't know how to deal with it.

2b. "Half of" bidirectional OneToMany relationship

Alternatively, we can try to make ChildEntity the owner of the relationship by removing @JoinColumn and adding mappedBy. In theory, there should be a corresponding @ManyToOne on the other side of the relationship, but it seems to work without it. This solution is optimal, might be not portable though (to different JPA providers or different Hibernate versions).

ParentEntity

@OneToMany(mappedBy = "referenceid", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<ChildEntity> childrens;

ChildEntity (no changes, same as in the question)

@Column(name = "REFERENCE_ID")
private String referenceid;

On update Hibernate generates following queires:

select parententi0_.parent_id as parent_i1_1_1_, childrens1_.reference_id as referenc3_0_3_, childrens1_.id as id1_0_3_, childrens1_.id as id1_0_0_, childrens1_.name as name2_0_ ...
select nextval ('child_seq')
select nextval ('child_seq')
insert into child_table (name, reference_id, id) values (?, ?, ?)
insert into child_table (name, reference_id, id) values (?, ?, ?)
delete from child_table where id=?

3. Lombok-generated equals and hashCode

This is not directly related to your question but I think you will face this problem sooner or later. You are using @Data annotations (I assume they are Lombok's, if not, ignore this section). They will generate equals and hashCode methods from all the fields by default, including ids. It is fine in the ParentEntity, where the id is set manually. But in the ChildEntity, where the id (aka asSeq) is generated by the database, it breaks the hashCode()/equals() contract. It may lead to really sneaky bugs. From Hibernate documentation:

The issue here is a conflict between the use of the generated identifier, the contract of Set, and the equals/hashCode implementations. Set says that the equals/hashCode value for an object should not change while the object is part of the Set. But that is exactly what happened here because the equals/hasCode are based on the (generated) id, which was not set until the JPA transaction is committed.

You may want to read more about it here:

Upvotes: 2

Related Questions