One-to-One Mapping in JPA – Complete Guide with Cascade, Fetch & JSON Issues

One-to-One mapping in JPA is used when one entity is associated with exactly one instance of another entity. This relationship is very common in real-world systems such as:
- User → Address
- Employee → Passport
- Customer → Profile
Let’s understand it properly with practical examples.
OneToOne – Unidirectional Mapping
In unidirectional mapping, only one entity knows about the other.
@Entity
@Table(name = "user_details")
public class UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(cascade = CascadeType.ALL)
private UserAddress userAddress;
}
Here:
UserDetailsknows aboutUserAddressUserAddresshas no idea aboutUserDetails
This is the simplest and most commonly used form.
How Foreign Key is Created
Hibernate automatically creates a foreign key column using this rule:
<field_name>_id
So for:
private UserAddress userAddress;
Hibernate creates:
user_address_id
Custom Foreign Key using @JoinColumn
If you want full control:
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private UserAddress userAddress;
Now the foreign key will be:
address_id
Composite Key Reference
If the child entity uses a composite key, you must map all key columns:
@OneToOne
@JoinColumns({
@JoinColumn(name="address_street", referencedColumnName="street"),
@JoinColumn(name="address_pin_code", referencedColumnName="pinCode")
})
private UserAddress userAddress;
Cascade Types – The Most Important Concept
Cascade means:
What happens to the child when parent changes?
| Cascade | Meaning |
|---|---|
| PERSIST | Save child automatically |
| MERGE | Update child automatically |
| REMOVE | Delete child automatically |
| REFRESH | Reload child from DB |
| DETACH | Stop tracking child |
| ALL | All of the above |
Most Used Cascade Combination
In real projects, best balance is:
@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
Why?
- Allows insert and update
- Prevents accidental delete of child
Eager vs Lazy Loading
| Type | Meaning |
|---|---|
| EAGER | Loads child immediately |
| LAZY | Loads child only when accessed |
Default:
- OneToOne → EAGER
- OneToMany → LAZY
You can control it:
@OneToOne(fetch = FetchType.LAZY)
Lazy Loading JSON Problem
When using LAZY, GET APIs often fail with errors like:
could not initialize proxy – no session
Because Jackson tries to serialize an object that is not loaded.
Wrong solution (quick hack)
@JsonIgnore
Correct solution (professional)
Use DTO.
DTO – Best Practice for REST APIs
Never return entities directly.
Instead:
class UserDTO {
private String name;
private String city;
}
Map entity → DTO in service layer.
This avoids:
- Lazy loading errors
- Infinite recursion
- Exposing DB structure
Bidirectional OneToOne
Both entities know each other:
class UserDetails {
@OneToOne
private UserAddress userAddress;
}
class UserAddress {
@OneToOne(mappedBy="userAddress")
private UserDetails userDetails;
}
Still only one foreign key in DB.
Infinite Recursion Problem
Bidirectional mapping causes:
User → Address → User → Address → …
Solutions
Option 1 (old style)
@JsonManagedReference
@JsonBackReference
Option 2 (best modern solution)
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
Jackson tracks objects using ID and avoids looping.
Real-World Best Practices
| Scenario | Recommendation |
|---|---|
| REST APIs | Always use DTO |
| Banking systems | Avoid Cascade.REMOVE |
| High traffic apps | LAZY + DTO |
| CRUD projects | PERSIST + MERGE |
| Microservices | Never expose entities |
Interview One-Liners (Gold)
Never expose JPA entities in REST APIs. Always use DTOs.
Avoid Cascade.REMOVE in production systems.
Bidirectional mapping does not create two foreign keys.
Lazy loading + Jackson = error unless handled properly.