Skip to content
AITroveRead. Build. Understand.
Make this comfortable

JPA entity equality: keep hash keys stable across persist and detach

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

Entity identity and Java collection equality have different lifecycles; choose one key strategy and test it across persistence states.

The collection failure

A new receipt entity enters a HashSet before its database-generated ID exists. An equals/hashCode implementation based directly on that mutable ID changes its hash after persist, so the set may no longer find the same object. Within one persistence context, Java reference identity is often enough. Across sessions, two instances can represent the same row, which is where domain equality becomes a deliberate design choice. Managed and detached states expose the issue.

Use a stable identity rule

If the domain has an immutable, unique natural key such as a receipt reference assigned before persistence, equality can use that key, provided the database enforces uniqueness and the value never changes. Otherwise an ID-based strategy needs careful transient handling and a stable hash implementation. Proxy classes complicate strict getClass comparisons. Avoid Lombok-generated equality over mutable fields, lazy associations or whole object graphs; it can trigger queries and recursion.

Test the actual transitions

Create two transient receipts with different references; put one into a set; persist and flush it; then assert membership still works. Reload the same row in another transaction and verify the intended equality result. Also test that two unsaved records with null IDs are not accidentally equal. This is not only a unit-test concern: unexpected equality can collapse children in a mapped Set before Hibernate writes them.

Implementation contract

Java
@Entity
class ReceiptRecord {
    @Id @GeneratedValue Long id;
    @Column(nullable = false, unique = true, updatable = false)
    String receiptReference;
    protected ReceiptRecord() {}
    ReceiptRecord(String receiptReference) { this.receiptReference = java.util.Objects.requireNonNull(receiptReference); }
    @Override public boolean equals(Object other) {
        return other instanceof ReceiptRecord record
            && receiptReference != null
            && receiptReference.equals(record.receiptReference);
    }
    @Override public int hashCode() { return receiptReference == null ? 0 : receiptReference.hashCode(); }
}

Cost and verification

Natural-key equality is constant time for a short key and avoids fetching an association. A database uniqueness constraint adds index write cost; mutating the key would invalidate both Java hash placement and domain identity.

Common Mistakes

  • Do not hash an ID that changes after adding the entity to a HashSet.
  • Do not include mutable fields or lazy collections in generated equality.
  • Do not use a natural key unless it is assigned before collection insertion and stays immutable.

Read next

Spring JPA entity lifecycle: managed changes and detached objects, Spring Data JPA assigned IDs: tell save when an entity is new, Spring JPA optimistic locking: reject a stale stock update, Spring JPA fetch plans: measure N+1 queries before changing mappings, Spring Data JPA repositories: derive a query from mapped properties.

spring
spring-data
jpa
jpa-entity-equality-hash
Storage details