Reputation: 333
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:
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
Reputation: 10661
There are actually three problems here:
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