Reputation: 403
I'm having trouble to remove a child entity from a one-to-many relationship. I managed to narrow it down to HashSet.contains()
returning false only after the parent (and child) entity is persisted.
The following code
Parent parent = ParentFactory.parent();
Child child = ChildFactory.child();
parent.addChild(child);
System.out.println("contains? " + parent.getChilds().contains(child));
System.out.println("child.hashCode " + child.hashCode());
System.out.println("parent.child.hashCode " + parent.getChilds().iterator().next().hashCode());
System.out.println("equals? " + parent.getChilds().iterator().next().equals(child));
parentDao.save(parent);
System.out.println("contains? " + parent.getChilds().contains(child));
System.out.println("child.hashCode " + child.hashCode());
System.out.println("parent.child.hashCode " + parent.getChilds().iterator().next().hashCode());
System.out.println("equals? " + parent.getChilds().iterator().next().equals(child));
Will print:
contains? true
child.hashCode 911563320
parent.child.hashCode 911563320
equals? true
contains? false
child.hashCode -647032511
parent.child.hashCode -647032511
equals? true
I read in similar questions, that it could be caused by overloading instead of overriding equals()
, but I think that that can't be the problem because then it'd print false the first time I check contains()
. By the way, I'm using Lombok's EqualsAndHashCode
.
My entities:
@Entity
@Table(name = "parents")
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class Parent implements Serializable {
private static final long serialVersionUID = 6063061402030020L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Boolean isActive;
@OneToMany(fetch = EAGER, mappedBy = "parent", cascade = {ALL}, orphanRemoval = true)
private final Set<Child> childs = new HashSet<>();
public void addChild(Child child) {
this.childs.add(child);
child.setParent(this);
}
}
@Entity
@Table(name = "childs")
@Getter
@Setter
@EqualsAndHashCode(exclude = "parent")
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "parent")
public class Child implements Serializable {
private static final long serialVersionUID = 5086351007045447L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = EAGER)
@JoinColumn(name = "parentId_fk")
private Parent parent;
}
The only thing that changes after persist is the child
's id, but that both child
and parent.child
reference to the same instance, so the id is the same. This is proven by hashCode()
and equals()
returning true.
Why does this happen? How can I fix it?
Upvotes: 3
Views: 1311
Reputation: 3149
You have broken the contract for equals
and hashCode
, to keep them same for the life of the object:
contains? true
child.hashCode 911563320
parent.child.hashCode 911563320
equals? true
//persist happens here
contains? false
child.hashCode -647032511
parent.child.hashCode -647032511
equals? true
You can see that the hashCode
has changed and that's all it takes to break the Set
.
You can exclude the id field from Lombok's generated equals
and hashCode
methods as this:
@EqualsAndHashCode(exclude={"id"})
it could be caused by overloading instead of overriding equals(), but I think that that can't be the problem because then it'd print false the first time I check contains()
It would not print false
, because when you call addChild
, only adding the child to the Set
takes place. No id is assigned until you persist the changes with save
. If the id was assigned upon adding to the set, you wouldn't have this problem.
The only thing that changes after persist is the child's id, but that both child and parent.child reference to the same instance, so the id is the same. This is proven by hashCode() and equals() returning true.
The only thing that changed is indeed the id, but thats the problem - if the set was somehow smarter and could detect changes in stored objects, it could reflect the change in hashCode
and return proper object what you call contains
for the second time. But that didnt happen. equals
return true
because references didn't change, not even in the set. contains? false
does not mean the set is empty, it merely means it can't find the object by -647032511
hash code, because it was stored with 911563320
hash code. If you somehow pass an object with 911563320
hash code, it will happily return the instance with -647032511
hash (not sure here, maybe it will throw exception, but the point is it will try to return object with different hash).
Also note that you could now add the very same object twice in the set, which is also against the Set
contract. Iterator of the set would return one object with same identity twice - you don't want it to happen.
Upvotes: 3
Reputation: 12215
The @EqualsAndHashCode
annotation uses the member variables to create a hashcode. When persist
ing an object, it is assigned an id, which means that the hashcode will differ before and after the persist. If the annotation is the one from Lombok you can exclude fields.
Upvotes: 2
Reputation: 947
HashSet
uses hashCode
to find the right bucket in which to put given element. When you search given element with contains
it again calls hashCode
on the object, but when it returns new value, it cannot find it on the place where it was inserted and thus returns false. Maybe save
operation causes an object to have different hashCode.
Upvotes: 2