Careeroza
equals and hashCode on Entities
The Problem
A new entity has id == null. After persist it gets an id. If hashCode() uses the id, the object's hash changes while it sits in a HashSet, and the set can no longer find it.
Example
An OrderItem added to Order.items (a HashSet) before saving:
import java.util.*;
public class Main {
public static void main(String[] args) {
Set<BadItem> bad = new HashSet<>();
BadItem b = new BadItem();
bad.add(b);
b.id = 101L; // persist() assigned the id
System.out.println("bad: contains after persist? " + bad.contains(b));
System.out.println("bad: remove worked? " + bad.remove(b) + ", size " + bad.size());
Set<GoodItem> good = new HashSet<>();
GoodItem g = new GoodItem();
good.add(g);
g.id = 101L;
System.out.println("good: contains after persist? " + good.contains(g));
GoodItem a = new GoodItem(), c = new GoodItem();
System.out.println("good: two new (unsaved) items equal? " + a.equals(c));
}
}
class BadItem {
Long id;
@Override public boolean equals(Object o) { return o instanceof BadItem x && Objects.equals(id, x.id); }
@Override public int hashCode() { return Objects.hash(id); } // changes when id is set!
}
class GoodItem {
Long id;
@Override public boolean equals(Object o) {
if (this == o) return true;
return o instanceof GoodItem x && id != null && id.equals(x.id); // unsaved: only equal to itself
}
@Override public int hashCode() { return GoodItem.class.hashCode(); } // constant for the entity type
}Output
bad: contains after persist? false bad: remove worked? false, size 1 good: contains after persist? true good: two new (unsaved) items equal? false
Recommended Implementation
@Entity
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (Hibernate.getClass(this) != Hibernate.getClass(o)) return false; // proxy-safe
OrderItem other = (OrderItem) o;
return getId() != null && getId().equals(other.getId()); // getter: works on proxies
}
@Override
public final int hashCode() {
return Hibernate.getClass(this).hashCode();
}
}- If the entity has a stable business key (SKU, email, PAN number), use it instead: annotate it with
@NaturalIdand base equals/hashCode on it. - Never use Lombok
@Dataor@EqualsAndHashCodeon entities: it includes all fields, including lazy collections. - A constant hashCode puts all items of one type in one bucket. That is fine for the small sets you keep in an entity; do not put millions of entities in one HashSet.