Reputation: 93
I have an existing parent-child relationship in our application that has become more complex recently because we have added a "type" column to the primary key of both the parent and the child. After this, adding, reading and modifying children works well but removing them is a pain.
Using the advice given in this article by Vlad Mihalcea on @OneToMany relationships and also various examples on the composite keys, I have tried an implementation similar to the following model. However, removing children still doesn't work and I now have a weird error message as a bonus.
I am using Spring Boot 1.4.1 and Hibernate 5.1.9.Final.
A Parent entity has an @EmbeddedId ParentPK with two fields and a children
collection with Cascade.ALL
and orphanRemoval
set to true.
@Entity
@Table(name = "z_parent")
public class Parent {
@EmbeddedId
private ParentPK pk;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumns({
@JoinColumn(name = "parent_code", referencedColumnName = "code"),
@JoinColumn(name = "parent_type", referencedColumnName = "type")
})
List<Child> children = new ArrayList<>();
public Parent() {
}
public Parent(String code, String type) {
this.pk = new ParentPK(code, type);
}
public void addChild(Child child){
child.setParent(this);
children.add(child);
}
public void removeChild(Child child){
child.setParent(null);
children.remove(child);
}
//getters and setters, including delegate getters and setters
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Parent)) return false;
Parent parent = (Parent) o;
return pk.equals(parent.pk);
}
@Override
public int hashCode() {
return pk.hashCode();
}
}
@Embeddable
public class ParentPK implements Serializable {
@Column(name = "code")
private String code;
@Column(name = "type")
private String type;
public ParentPK() {
}
public ParentPK(String code, String type) {
this.code = code;
this.type = type;
}
//getters and setters
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ParentPK)) return false;
ParentPK parentPK = (ParentPK) o;
if (!getCode().equals(parentPK.getCode())) return false;
return getType().equals(parentPK.getType());
}
@Override
public int hashCode() {
int result = getCode().hashCode();
result = 31 * result + getType().hashCode();
return result;
}
}
The Child
entity has its own code
identifier which, together with the two Strings identifying the parent, form another composite primary key. The relation with Parent is bidirectional, so the Child also has a parent
field annotated with @ManyToOne.
@Entity
@Table(name = "z_child")
public class Child {
@EmbeddedId
private ChildPk pk = new ChildPk();
//The two columns of the foreign key are also part of the primary key
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "parent_code", referencedColumnName = "code", insertable = false, updatable = false),
@JoinColumn(name = "parent_type", referencedColumnName = "type", insertable = false, updatable = false)
})
private Parent parent;
public Child() {
}
public Child(String code, String parentCode, String parentType) {
this.pk = new ChildPk(code, parentCode, parentType);
}
//getters and setters, including delegate getters and setters
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Child)) return false;
Child child = (Child) o;
return pk.equals(child.pk);
}
@Override
public int hashCode() {
return pk.hashCode();
}
}
@Embeddable
class ChildPk implements Serializable {
@Column(name = "code")
private String code;
@Column(name = "parent_code")
private String parentCode;
@Column(name = "parent_type")
private String parentType;
public ChildPk() {
}
public ChildPk(String code, String parentCode, String parentType) {
this.code = code;
this.parentCode = parentCode;
this.parentType = parentType;
}
//getters and setters
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ChildPk)) return false;
ChildPk childPk = (ChildPk) o;
if (!getCode().equals(childPk.getCode())) return false;
if (!getParentCode().equals(childPk.getParentCode())) return false;
return getParentType().equals(childPk.getParentType());
}
@Override
public int hashCode() {
int result = getCode().hashCode();
result = 31 * result + getParentCode().hashCode();
result = 31 * result + getParentType().hashCode();
return result;
}
}
Since I am using Spring, I have declared a simple CRUD repository for the Parent:
@Repository
public interface ParentRepository extends JpaRepository<Parent, ParentPK> {
}
Let's say that I already have a Parent with two children in the database:
z_Parent
"code", "type"
"Parent", "Adoptive"
z_child
"code", "parent_code", "parent_type"
"Child1", "Parent", "Adoptive"
"Child2", "Parent", "Adoptive"
, and that I have to persist an updated version of the parent, containing only the first child:
public Parent mapFromUpperLayer(){
Parent updatedParent =new Parent("Parent", "Adoptive");
List<Child> children = new ArrayList<>();
Child child1 = new Child("Child1", updatedParent);
child1.setParent(updatedParent);
children.add(child1);
updatedParent.setChildren(children);
return updatedParent;
}
If I simply save the entity with one child:
@Autowired
private ParentRepository parentRepository;
@Test
@Commit
public void saveUpdate(){
Parent updatedParent = mapFromUpperLayer();
parentRepository.save(updatedParent);
}
then I have the following result (I have cleared the log a bit):
Hibernate: select parent0_.code as code1_50_1_, parent0_.type as type2_50_1_, children1_.parent_code as parent_c2_49_3_, children1_.parent_type as parent_t3_49_3_, children1_.code as code1_49_3_, children1_.code as code1_49_0_, children1_.parent_code as parent_c2_49_0_, children1_.parent_type as parent_t3_49_0_ from z_parent parent0_ left outer join z_child children1_ on parent0_.code=children1_.parent_code and parent0_.type=children1_.parent_type where parent0_.code=? and parent0_.type=?
TRACE 12412 --- : binding parameter [1] as [VARCHAR] - [Parent]
TRACE 12412 --- : binding parameter [2] as [VARCHAR] - [Adoptive]
Hibernate: update z_child set parent_code=null, parent_type=null
where parent_code=? and parent_type=? and code=?
TRACE 12412 --- : binding parameter [1] as [VARCHAR] - [Parent]
TRACE 12412 --- : binding parameter [2] as [VARCHAR] - [Adoptive]
TRACE 12412 --- : binding parameter [3] as [VARCHAR] - [Child2]
TRACE 12412 --- : binding parameter [4] as [VARCHAR] - [Parent]
INFO 12412 --- : HHH000010: On release of batch it still contained JDBC statements
WARN 12412 --- : SQL Error: 0, SQLState: 22023
ERROR 12412 --- : L'indice de la colonne est hors limite : 4, nombre de colonnes : 3.
There are two problems here. Hibernate correctly identifies that Child2 is to be removed from the parent generates an update rather than a delete query. I used a bidirectional relationship exactly in order to avoid this but it appears I have not understood completely how it works. And, of course, the update it generates contains four parameters for three columns ("Parent" appears twice), which is bizarre.
First, I have retrieved the entity from the database, removed both it's children and set their parent to null (removeChild
method) and added the new list also taking care to set every time the parent to the instance I was going to save (addChild
method).
@Test
@Commit
public void saveUpdate2(){
Parent updatedParent = mapFromUpperLayer();
Parent persistedParent = parentRepository.findOne(new ParentPK(updatedParent.getCode(), updatedParent.getType()));
//remove all the children and add the new collection, both one by one
(new ArrayList<>(persistedParent.getChildren()))
.forEach(child -> persistedParent.removeChild(child));
updatedParent.getChildren().forEach(child -> persistedParent.addChild(child));
parentRepository.save(persistedParent);
}
Secondly I have tried the solution from this question, that is I have declared the @ManyToOne part of the relationship directly inside ChildPK:
@Embeddable
class ChildPk implements Serializable {
@Column(name = "code")
private String code;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "parent_code", referencedColumnName = "code"),
@JoinColumn(name = "parent_type", referencedColumnName = "type")
})
private Parent parent;
public ChildPk() {
}
public ChildPk(String code, Parent parent) {
this.code = code;
this.parent = parent;
}
....
In both cases I get the same generated queries and the same error.
How can I structure my Parent - Child relationship so that Hibernate will be able to delete the removed children when I save the new version of the parent? Ideally I would like to not change the structure of the database too much - a join table, for example, would be rather time-consuming to implement.
Less important but intriguing: why on earth does Hibernate try to bind the four parameters "[Parent], [Adoptive], [Child2], [Parent]" to identify Child2 in the update query?
Thank you for your patience!
Upvotes: 2
Views: 1611
Reputation: 26026
Annotations on Parent.children
are the source of the problem.
Add mappedBy
, remove @JoinColumns
on the parent side.
The correct way to set this up:
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER, cascade =
CascadeType.ALL, orphanRemoval = true)
List<Child> children = new ArrayList<>();
I believe the query generated for removal is the desired outcome.
Hibernate: delete from z_child where code=? and parent_code=? and parent_type=?
In addition, removeChild
can be simplified - setting child's parent to null is not necessary - it will be handled anyway. This does not affect the query generated.
public void removeChild(Child child){
// child.setParent(null); No need to do that
children.remove(child);
}
Upvotes: 1