user384729
user384729

Reputation: 403

HashSet.contains() changes from true to false after persisting parent entity

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

Answers (3)

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"})

Comments

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

Tobb
Tobb

Reputation: 12215

The @EqualsAndHashCode annotation uses the member variables to create a hashcode. When persisting 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

Nikolay Tomitov
Nikolay Tomitov

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

Related Questions