@OneToOne
@OneToOne
A one-to-one association is the simplest relationship in a relational model: one row in table A corresponds to exactly one row in table B. In JPA / Hibernate the @OneToOne annotation maps this relationship to Java objects. Knowing where the foreign key lives, and which side owns it, determines how the DDL is generated, how inserts work, and how the association is loaded — so let us walk through every meaningful variant.
The Domain: User and UserProfile
A classic example is a User with a corresponding UserProfile that holds extended bio data. Each user has at most one profile, and each profile belongs to exactly one user.
Variant 1 — Foreign Key on the Owning Side
The most common layout stores the foreign key in the child table (user_profiles). The entity that physically holds the foreign-key column is called the owning side. The other entity is the inverse side (covered in detail in Lesson 5).
Hibernate generates a user_id column with a UNIQUE constraint in user_profiles. The unique = true attribute on @JoinColumn is what tells the database to enforce the "at most one" cardinality — without it you would have a many-to-one at the DB level.
User, mappedBy = "user" tells Hibernate: "the real foreign-key column lives over there in UserProfile.user". Only one side can own the association; forgetting this causes Hibernate to create two foreign-key columns — one per entity — which is almost always a bug.
Variant 2 — Shared Primary Key
An alternative schema uses the same primary key value for both tables. The child row reuses the parent's PK as its own PK. This is efficient (no extra FK index) and semantically clean when the child cannot exist independently. Use @MapsId to wire this up:
Persisting a OneToOne Association
Because User owns the cascade policy (CascadeType.ALL), persisting the parent automatically persists the child:
profile.setUser(user) means the FK column stays NULL. Setting only the owning side is correct for persistence, but setting both keeps your first-level cache consistent and prevents confusing NullPointerExceptions when you later traverse user.getProfile() within the same transaction.
Fetch Type Choices
JPA's default for @OneToOne is EAGER, which means every time you load a User, Hibernate immediately joins and loads the UserProfile. In most applications that is wasteful when the profile is not needed.
- EAGER (default) — joined in the same SELECT. Simple, but loads data you may not need.
- LAZY — the profile is loaded only when you call
user.getProfile(). Almost always the right choice; requires the session to still be open at access time.
To enable LAZY on the inverse side Hibernate needs to generate a proxy sub-class. This works reliably with byte-code enhancement (enabled by default in Spring Boot 3 via the Hibernate Enhancer plugin) or with the @LazyToOne(LazyToOneOption.NO_PROXY) hint on older setups.
Reading with Spring Data JPA
The repository looks the same as any other:
Because the profile is LAZY, accessing it inside a @Transactional service method works transparently. Accessing it outside the transaction (in a controller, after the session is closed) throws a LazyInitializationException — the standard solution is to load what you need inside the service or use a DTO projection.
Summary
A @OneToOne association maps two entities that share a one-to-one cardinality. The owning side holds the @JoinColumn (and the real FK column); the inverse side uses mappedBy. For a shared-PK layout use @MapsId. Always declare fetch = LAZY to avoid loading related data you do not need, set both ends of the relationship in memory when persisting, and add unique = true to @JoinColumn so the database enforces the cardinality constraint. The next lesson extends this to the far more common one-to-many and many-to-one relationships.