One-to-One Mapping
Intermediate@OneToOne maps two entities with a shared or foreign-key strategy; use @MapsId to share the primary key between parent and child for an efficient 1:1 relationship.
Overview
@OneToOne maps two entities where each instance of A is associated with exactly one instance of B. The common strategies are: (1) Foreign key — child table has a unique FK column pointing to the parent PK; (2) Shared primary key (@MapsId) — child shares the same PK value as the parent, eliminating the extra FK column and making joins trivial; (3) Join table — a separate association table (rare for 1:1). Always make @OneToOne relationships LAZY (fetch = FetchType.LAZY) to avoid loading the associated entity on every fetch of the owner.
Foreign-Key Strategy
The child entity holds a foreign key column referencing the parent PK. @JoinColumn specifies the FK column name. Make the relationship bidirectional with mappedBy on the owning side to avoid extra joins.
// Parent entity — User
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
// Bidirectional — mappedBy points to the field in UserProfile that owns the FK
@OneToOne(mappedBy = "user",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true)
private UserProfile profile;
}
// Child entity — UserProfile owns the FK
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", unique = true) // FK column in user_profiles
private User user;
private String bio;
private String avatarUrl;
}
// Create with cascade
User user = new User("alice@example.com");
UserProfile profile = new UserProfile("Java developer", "/avatars/alice.jpg");
profile.setUser(user);
user.setProfile(profile);
userRepository.save(user); // cascades to UserProfileShared Primary Key (@MapsId)
@MapsId makes the child entity share the parent's PK — no extra FK column needed. The child table's PK IS the FK to the parent. This is the most efficient 1:1 strategy.
// Parent
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "order", fetch = FetchType.LAZY,
cascade = CascadeType.ALL, orphanRemoval = true)
private OrderDetail detail;
}
// Child — PK shared with Order.id
@Entity
@Table(name = "order_details")
public class OrderDetail {
@Id // PK
private Long id; // same value as order.id
@MapsId // tells Hibernate: id = order.id
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id") // FK column is also the PK column
private Order order;
private String shippingAddress;
private String trackingNumber;
}
// Table structure:
// orders: id | status | total
// order_details: id | shipping_address | tracking_number
// ↑ PK and FK — same column, joins are trivially efficient
// Create
Order order = new Order(OrderStatus.PLACED, total);
OrderDetail detail = new OrderDetail(address, tracking);
detail.setOrder(order);
order.setDetail(detail);
orderRepository.save(order);Lazy Loading Gotcha & Optional Associations
@OneToOne with LAZY fetch only works correctly when the association is non-null AND the child side owns the FK. On the non-owning (mappedBy) side, Hibernate must issue a SQL query to check existence even for LAZY — this is the "proxy problem".
// Problem: Hibernate cannot lazily proxy the non-owning side
// because it doesn't know if the associated entity exists without a query
// User → UserProfile (non-owning, mappedBy side)
// Even with LAZY, loading a User issues a second query for UserProfile
// to determine: does a profile exist for this user?
// Solution 1: @LazyToOne (Hibernate-specific, advanced bytecode instrumentation)
// Solution 2: Always access via the OWNING side (UserProfile → User is truly lazy)
// Solution 3: Use Optional<UserProfile> and check existence in the service layer
@Service
public class UserService {
public Optional<UserProfile> getProfile(Long userId) {
// Query UserProfile by userId directly — avoids the proxy issue
return userProfileRepository.findByUserId(userId);
}
}
// Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
Optional<UserProfile> findByUserId(Long userId);
}
// Rule of thumb:
// ✓ Owning side (has @JoinColumn) — LAZY works correctly
// ✗ Non-owning side (mappedBy) — LAZY may be proxied incorrectly
// ✓ Use @MapsId (child IS the owning side) to get correct lazy loading on both sidesKey Points to Remember
- 1@OneToOne with FK: child has a unique FK column; @JoinColumn names the column.
- 2@MapsId shares the parent PK as the child PK — no extra column, most efficient strategy.
- 3Always declare @OneToOne with fetch = FetchType.LAZY to avoid unintended joins.
- 4The non-owning side (mappedBy) cannot be truly lazy without bytecode instrumentation.
- 5Use cascade = CascadeType.ALL + orphanRemoval = true to manage child lifecycle via parent.
- 6Query via the owning side (FK holder) for correct lazy loading behaviour.
Interview Questions
Sign in to ask AriaWhat is the difference between FK-based and shared-PK @OneToOne strategies?
What does @MapsId do and why is it preferred for 1:1 relationships?
Why does @OneToOne with LAZY fetch not work correctly on the non-owning side?
What is the difference between cascade = ALL and orphanRemoval = true?
How would you query a @OneToOne relationship without the N+1 problem?
Ask Aria about One-to-One Mapping
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.