equals, hashCode and detached entities

Hibernate · lesson 31 of 32 · 6 min read

The default implementations break the moment an entity is persisted or detached.

Open this lesson in the learning hub

Key points

  • Using the generated id in hashCode is 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.
  • hashCode may 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. Use instanceof, or Hibernate getClass unwrapping.

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.