equals, hashCode and detached entities
The default implementations break the moment an entity is persisted or detached.
Open this lesson in the learning hubKey points
- Using the generated id in
hashCodeis the classic bug. A new entity has a null id, so its hash changes when it is persisted - and an entity already in a HashSet is then unfindable. - Using all fields is no better: any mutation changes the hash, with the same result, and it forces lazy associations to load just to compare.
- The workable approach is a business key - an immutable natural identifier such as an order number or an email - assigned at construction and never changed.
- Where no business key exists, assign a UUID in the constructor. It is stable from creation, survives persist and detach, and does not depend on the database.
hashCodemay also return a constant for entities. It degrades a HashSet to a list, which is fine for the small collections entities normally live in, and it is always correct.- Watch for proxies: a lazy proxy is a subclass, so
getClass()comparison fails against the real entity. Useinstanceof, or HibernategetClassunwrapping.
Example
// BROKEN - hash changes when the entity is persisted.
@Entity
class OrderBad {
@Id @GeneratedValue private Long id;
@Override public int hashCode() { return Objects.hash(id); }
@Override public boolean equals(Object o) { /* compares id */ }
}
Set<OrderBad> set = new HashSet<>();
OrderBad o = new OrderBad();
set.add(o); // hashed with id == null
em.persist(o); // id assigned -> hashCode CHANGES
set.contains(o); // false. The object is in the set and lost.
// CORRECT - a business key, immutable from construction.
@Entity
class Order {
@Id @GeneratedValue private Long id;
@Column(nullable = false, unique = true, updatable = false)
private String orderNumber; // assigned once, never changed
protected Order() { } // JPA needs this
public Order(String orderNumber) { this.orderNumber = orderNumber; }
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
// instanceof, NOT getClass() - a lazy proxy is a subclass.
if (!(o instanceof Order other)) { return false; }
return orderNumber != null && orderNumber.equals(other.orderNumber);
}
@Override
public int hashCode() {
// Constant is legal and stable. It degrades a HashSet to a list,
// which is irrelevant for the collection sizes entities live in.
return getClass().hashCode();
}
}
// No natural key? Assign one yourself, at construction.
@Entity
class Event {
@Id private UUID id = UUID.randomUUID(); // stable before AND after persist
}
Never hash on a generated id - use an immutable business key or a constructor-assigned UUID, and compare with instanceof to survive proxies.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Hibernate course, and every lesson in it is listed on the Hibernate contents page.