JPA One-to-One Mapping – Complete Deep Dive

 


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:

  1. Generates ID for Address
  2. Inserts Address
  3. 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.”

 

Leave a Reply