Why composite keys break equals() / hashCode() in ORM
ORMs rely on
equals()/hashCode()to track entity identity.
Composite keys make identity mutable, incomplete, or unstable during an entity’s lifecycle.
That’s the root problem.
First: how ORM uses equals() / hashCode()
Hibernate uses them in:
- Persistence Context (1st-level cache)
Set/Mapcollections- Dirty checking
- Association management
- Proxy resolution
Hibernate assumes:
- Identity is stable
- Identity exists from creation to deletion
equals()does not change over time
Composite keys violate these assumptions.
Problem 1️⃣: null values before persist (very common)
Composite key example
@Embeddable
class UserRoleId {
Long userId;
Long roleId;
}
Before persist():
userId = null
roleId = null
But equals() / hashCode() must already work.
What happens
hashCode()changes after IDs are assigned- Entity moves between hash buckets
- ORM loses track of the entity
What happens
hashCode()changes after IDs are assigned- Entity moves between hash buckets
- ORM loses track of the entity
Result
- Entity “disappears” from
Set - Duplicate entries appear
- Updates silently fail
This bug is nightmare-level to debug.
Problem 2️⃣: Mutable identity (identity must be immutable)
Composite keys are built from:
- Foreign keys
- Business columns
These can change.
Example:
(user_id, role_id)
What if:
- Role is reassigned
- User is merged
- FK updated
Now:
- Primary key changes
equals()/hashCode()changes- ORM breaks identity tracking
Surrogate IDs never change.
Problem 3️⃣: Partial equality before flush
Entity lifecycle:
- New entity created
- Added to a
Set - Persisted
- Flushed → ID assigned
With composite key:
- Step 2: equality based on nulls
- Step 4: equality based on real values
This violates the Java contract:
If an object is in a
HashSet, itshashCode()must not change.
Composite keys almost guarantee violation.
Problem 4️⃣: Proxy vs real object comparison
Hibernate often compares:
- Proxy instance
- Real entity instance
With composite PK:
- Multiple fields to compare
- More chances of mismatch
- Broken lazy-loading equality
Example bug:
proxy.equals(entity) == false
This breaks collections and caches.
Problem 5️⃣: Developers implement equals/hashCode wrong
Typical mistakes:
- Forgetting one key field
- Including non-key fields
- Using mutable fields
- Using Lombok
@Datablindly
With composite keys:
- More fields = more risk
- One mistake = silent corruption
Problem 6️⃣: Identity vs business equality confusion
Composite keys blur the line:
- Is
(userId, roleId)identity? - Or just a uniqueness rule?
ORM wants:
- Identity = immutable technical key
Composite keys encode business rules into identity.
That’s a conceptual mismatch.
Concrete failure example (realistic)
Set<UserRole> roles = new HashSet<>();
UserRole ur = new UserRole();
ur.setUser(user);
ur.setRole(role);
roles.add(ur); // hashCode based on nulls
entityManager.persist(ur);
entityManager.flush(); // IDs assigned → hashCode changes
roles.contains(ur); // false ❌
Why surrogate keys avoid all of this
@Id
Long id;
- Assigned once
- Immutable
- Simple equality
- Stable hash code
ORMs are optimized for this model.
One-sentence interview answer (perfect)
Composite keys make entity identity depend on multiple mutable or initially-null fields, which causes
equals()andhashCode()to change during the entity lifecycle and breaks ORM caching and collections.
Final mental model (remember this)
ORM identity must be boring.
Composite keys are interesting — and that’s the problem.