JPA One-to-One Mapping – Complete Deep Dive

1. What exactly is One-to-One in ORM?
In database:
One row in Table A is linked to exactly one row in Table B.
In Java:
One object holds reference to one object only.
Example:
- A User has exactly one Address.
- An Employee has exactly one ID Card.
This is different from:
- One-to-Many → One user, many orders.
- Many-to-One → Many orders, one user.
2. Unidirectional vs Bidirectional (Core Design Concept)
Unidirectional
Only one side knows the relationship.
UserDetails --> UserAddress
Database:
- Only USER_DETAILS table has FK column.
Java:
@OneToOne
private UserAddress userAddress;
UserAddress has no idea about UserDetails.
This is:
- Simpler
- Less bugs
- Recommended for most APIs
Bidirectional
Both sides know each other.
UserDetails <--> UserAddress
Java:
// UserDetails
@OneToOne
private UserAddress userAddress;
// UserAddress
@OneToOne(mappedBy="userAddress")
private UserDetails userDetails;
Only one side owns FK (called owning side).
Other side is inverse (object-only).
3. Foreign Key Internals (How Hibernate decides)
Hibernate rule:
FK column name =
<fieldName>_id
So:
private UserAddress userAddress;
FK becomes:
user_address_id
Hibernate also:
- Always uses PK of target table.
- You cannot link to non-PK unless explicitly.
4. @JoinColumn – Real Control (Production Important)
Without JoinColumn → Hibernate auto decides.
With JoinColumn:
@JoinColumn(
name = "address_id",
referencedColumnName = "id"
)
Now:
- Column name is controlled.
- Useful for:
- Legacy DB
- Naming conventions
- DB migrations
This is mandatory in real companies.
5. Composite Key – Why It’s Dangerous
Composite key = multiple columns as PK.
Example:
(street, pinCode)
Mapping:
@EmbeddedId
private UserAddressCK id;
Problems:
- Slow joins
- Hard indexing
- Difficult caching
- Ugly APIs
Industry rule:
Avoid composite keys unless forced.
Better:
- Use surrogate key (Long id)
- Put business fields as normal columns
CASCADE – The Most Misunderstood Concept
6. What is Cascade internally?
Cascade =
When JPA executes an operation on Parent, it automatically executes same operation on Child.
No cascade:
save(user);
// address is ignored
With cascade:
save(user);
// address auto saved
7. CascadeType.PERSIST (Insert logic)
Internally flow:
save(User)
→ persist(User)
→ persist(Address)
Hibernate does:
- Generates ID for Address
- Inserts Address
- Inserts User with FK
Atomic transaction.
8. CascadeType.MERGE (Update logic)
Important concept:
save()in Spring Data JPA = merge if ID exists.
So:
{
"id": 1,
"name": "New Name",
"userAddress": {
"id": 1,
"city": "Mumbai"
}
}
Hibernate:
- Sees ID → update
- Without MERGE → child ignored
- With MERGE → child updated
9. Why PERSIST alone is dangerous
Scenario:
- Insert works
- Update fails silently
This causes:
- Data mismatch
- Partial updates
- Hidden bugs
Correct real-world config:
cascade = {PERSIST, MERGE}
Not ALL blindly.
10. CascadeType.REMOVE (Delete strategy)
Without REMOVE:
- User deleted
- Address becomes orphan row
With REMOVE:
- Both deleted
Critical for:
- GDPR
- Banking
- PII systems
11. First Level Cache – The Hidden Layer
Every EntityManager has:
Persistence Context (Map<ID, Object>)
During transaction:
- DB is hit once
- After that all reads come from memory
This is why:
- INSERT response has full object
- GET response fails (LAZY)
12. CascadeType.REFRESH (Cache bypass)
Flow:
entityManager.refresh(user);
Hibernate:
- Ignores cache
- Fetches from DB again
With cascade:
- Child also refreshed
Used in:
- Stock prices
- Banking balance
- Realtime systems
13. DETACH – When JPA loses control
After detach:
- JPA stops tracking
- No dirty checking
- No auto update
Used in:
- Read-only services
- Heavy batch jobs
Lazy vs Eager – Real Performance Engineering
14. EAGER (Default for OneToOne)
Means:
Always join child table.
SQL:
SELECT *
FROM user_details u
JOIN user_address a
Problem:
- Unnecessary joins
- Memory waste
- Slow APIs
15. LAZY (Default for OneToMany)
Means:
Child loaded only when accessed.
Proxy object created:
UserAddress$HibernateProxy
When you call:
getUserAddress()
Hibernate fires:
SELECT * FROM user_address
16. Why GET API fails but POST works
POST:
- Objects already in memory
- No DB hit
- Serialization success
GET:
- LAZY proxy
- No data
- Jackson crashes
This is core ORM concept.
Serialization Hell (Real Production Problem)
17. Infinite Recursion in Bidirectional
Flow:
User → Address → User → Address → ∞
Jackson keeps serializing forever.
StackOverflowError.
18. Solutions ranked by quality
Worst (Hack)
@JsonIgnore
Okay (Jackson specific)
@JsonManagedReference
@JsonBackReference
Best (Industry standard)
@JsonIdentityInfo
Uses primary key to avoid duplication.
Ultimate Industry Best Practices
These are what real architects follow:
1. Never return Entity from Controller
Always use DTO.
2. Avoid CascadeType.ALL
Use only what you need.
3. Prefer LAZY + DTO
Not EAGER.
4. Avoid Bidirectional unless needed
Unidirectional is safer.
5. Never expose composite keys
Always use single Long ID.
6. Always test:
- Insert
- Update
- Delete
- Get
7. Use @Transactional wisely
Lazy works only inside transaction.
Interview-Level Killer Statements
You can literally say this:
“Cascade controls lifecycle propagation, not relationship.”
“Lazy loading is implemented using proxy objects.”
“save() is not insert; it’s merge based on identifier.”
“First level cache exists per EntityManager, not globally.”
“Infinite recursion is a serialization problem, not ORM problem.”