JPA (Java Persistence API)
The Java Persistence API (JPA) is a Java specification that defines a standard way to manage relational data in Java applications using Object Relational Mapping (ORM).
It provides a set of interfaces and annotations that allow developers to map Java objects to database tables, perform CRUD operations, and manage persistence without writing large amounts of SQL.
Key points:
- JPA is a specification, not an implementation
- It standardizes how Java applications interact with relational databases
- It uses annotations and configuration to map objects to tables
- It simplifies database operations through entity management and persistence context
Hibernate
Hibernate is a popular open-source ORM framework that provides the implementation of the JPA specification.
It allows developers to interact with databases using Java objects instead of writing complex SQL queries, making the application code loosely coupled with the underlying database.
Key points:
- Hibernate is a JPA implementation
- Provides powerful ORM capabilities
- Handles CRUD operations automatically
- Supports caching, lazy loading, and transaction management
- Reduces boilerplate JDBC code
Relationship Between JPA and Hibernate
- JPA → Specification (standard API)
- Hibernate → Implementation of that specification
In practice:
- Developers write code using JPA annotations and interfaces
- Hibernate executes the actual database operations
Relationship Mapping in JPA
Key Best Practices
-
Always use
FetchType.LAZYon relationships — avoids N+1 query problem -
mappedByon the non-owning side (the side without the FK column) - Cascade carefully — only cascade from parent to child (Order → OrderItem), not upward
-
@JoinColumnexplicitly names your FK column for clarity -
Snapshot prices in
OrderItem.unitPrice— never rely on currentProduct.price - Use
Setinstead ofListfor@ManyToManyto avoid duplicate join queries
One-to-One Relationship Mapping in JPA
A One-to-One relationship occurs when one entity is associated with exactly one instance of another entity.
One-to-One: Unidirectional vs Bidirectional
Unidirectional
- Only one entity has a reference to the other. Navigation works in one direction only.
Bidirectional
- Both entities have a reference to each other. Navigation works in both directions.
How to Identify Owner, FK Side, and Child?
- The entity that CANNOT exist without the other = child = owns the FK
- The entity that EXISTS independently = parent = no FK
Questions to Ask
- WHO cannot exist without the other? → that's the CHILD → holds the FK
- WHO exists independently? → that's the PARENT → has mappedBy
- WHO makes sense to delete first? → that's the CHILD → cascade from parent
JPA Annotation Summary
| Annotation | Side | FK |
|---|---|---|
@JoinColumn |
Owning / Child | FK lives here |
mappedBy |
Inverse / Parent | no FK |
@JoinTable |
@ManyToMany owner |
junction table |
cascade |
Parent → Child | delete parent = delete child |
Child = cannot exist without parent = has
@JoinColumn(FK).
Parent = exists independently = hasmappedBy(no FK).
Example:
- Person → Passport
- User → Profile
- Order → Invoice
Example:
The User entity is the parent, while the UserProfile is the child association because the Foreign Key is located in the 'user_profile' database table.
This relationship is mapped as follows:
UserProfile.java — OWNING side (holds the FK) @JoinColumn annotation is used in the owning side - the child entity that contains the foreign key points to the primary key of the parent entity.
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String phone;
@Column(nullable = false)
private String address;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", unique = true)
private User user;
}
The user_profile table contains a Primary Key (PK) column (e.g. id) and a Foreign Key (FK) column (e.g. user_id).
User.java — the INVERSE side
@Getter
@Setter
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled = true;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private UserProfile profile;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"}))
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
private Set<Role> roles = new HashSet<>();
public void addRole(Role role) {
if (role != null) {
this.roles.add(role);
}
}
public void removeRole(Role role) {
this.roles.remove(role);
}
public boolean hasRole(Role role) {
return this.roles.contains(role);
}
public void setProfile(UserProfile profile) {
this.profile = profile;
if (profile != null) {
profile.setUser(this);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return username != null && username.equals(user.getUsername());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
@ElementCollection stores simple values (Strings) in a separate table — not a full entity relationship, so there's no "other side" to sync.
Specifying FetchType.LAZY for the non-owning side of the @OneToOne association will not affect the loading. On the inverse side (mappedBy), Hibernate doesn't know if the associated entity exists or not without hitting the DB. which defeats the purpose of LAZY. So it just loads eagerly regardless of what you specify.
On the owning side, this problem doesn't exist — the FK column is right there in the same row, so Hibernate knows immediately if it's null or not, and can safely create a proxy.
What's Happening
Query 1: select from users ← your actual request
Query 2: select from user_profile ← YOU DIDN'T ASK FOR THIS
Query 3: select from user_roles ← expected (EAGER @ElementCollection)
Query 2 is Hibernate silently probing — "does a profile exist for this user?" — because profile is on the inverse side and has no FK column to check.
Query 3 (user_roles) firing is expected and correct — @ElementCollection(fetch = FetchType.EAGER) is explicitly eager. If you don't need roles on every fetch, change it:
@ElementCollection(fetch = FetchType.LAZY) // load roles only when needed
Since I am using Spring Security, keeping it EAGER — UserDetails needs roles immediately on authentication.
Option 1: @LazyToOne — Bytecode Instrumentation (True Lazy)
// User.java
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY) // forces true lazy via bytecode
private UserProfile profile;
Requires adding the Hibernate bytecode enhancer to your build:
How it works:
- Without enhancement → Hibernate probes DB to check if profile is null or not
- With enhancement → Hibernate injects interceptor into bytecode, no probe needed
Option 2 — JOIN FETCH Query
- Load both entities together in a single SQL query — profile is already in memory, no probe needed.
@Query("SELECT u FROM User u LEFT JOIN FETCH u.profile")
List<User> findAllWithProfile();
turns into
SELECT
u1_0.id,
u1_0.enabled,
u1_0.password,
u1_0.username,
p1_0.id,
p1_0.address,
p1_0.phone
FROM users u1_0
LEFT JOIN user_profile p1_0
ON u1_0.id = p1_0.user_id
How it works:
- Without JOIN FETCH → select from users + select from user_profile (2 queries)
- With JOIN FETCH → select from users LEFT JOIN user_profile (1 query)
Option 3: @MapsId — Shared Primary Key (Best & Simplest)
Since both entities share the same PK, Hibernate already knows the profile ID without probing.
Map a @OneToOne Relationship Using @MapsId
The best way to map a @OneToOne relationship in Hibernate is by using @MapsId, which allows the child entity to share the same primary key as the parent entity.
In this approach:
- The child table uses the parent’s primary key as its own primary key.
- This avoids creating an additional foreign key column.
- It also simplifies fetching, since the child entity can always be retrieved using the parent’s identifier.
For example, in a User and UserProfile relationship:
- Each User has exactly one UserProfile
- The UserProfile shares the same primary key as the User
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile {
@Id //no @GeneratedValue — inherited from User
private Long id;
@Column(nullable = false)
private String phone;
@Column(nullable = false)
private String address;
@OneToOne(fetch = FetchType.LAZY)
@MapsId // tells Hibernate: this entity's PK = users.id
@JoinColumn(name = "id")
private User user;
So in summary
- The id column in UserProfile serves as both PK and FK — its value is copied directly from User.id.
Key Differences Per Entity
User |
UserProfile |
|
|---|---|---|
@GeneratedValue |
yes — generates own PK | no — copies from User
|
| Owns the relationship | no profile field |
yes — has @MapsId
|
| Probe query on fetch | gone — no inverse field | — |
| Save strategy | userRepository.save(user) |
userProfileRepository.save(profile) |
Summary
-
Usergenerates its own PK and knows nothing aboutUserProfile.UserProfileborrowsUser's PK via@MapsId— clean, no extra column, no probe query. - Removing profile from User.java made it unidirectional.
- Bidirectional = convenience of user.getProfile() but costs an extra query. Unidirectional = no extra query but you lose navigation from User side. Bytecode enhancement is the only way to have both.
- For most apps — skip the plugin, go unidirectional, and use JOIN FETCH when you need both. You get zero runtime cost, zero probe query, and full control over when profile loads.
Why @MapsId Solves It
Before (@JoinColumn on profile) |
After (@MapsId) |
|
|---|---|---|
| Profile FK | user_profile.user_id |
user_profile.id = users.id
|
| Hibernate knows ID? | Must probe DB | Already has it |
Extra query on getUser? |
Always fires | Only when accessed |
| DB schema | Extra user_id column |
Shared PK, cleaner |
Advantages of Using @MapsId
- No extra foreign key column
- Better database normalization**
- More efficient joins
- No need for a bidirectional association
- The
UserProfilecan always be fetched using theUserID
Downsides of Using @MapsId
-
Tight Coupling Between Entities
- The child entity (
UserProfile) cannot exist without the parent (User) because it shares the same primary key. - This makes the relationship very tightly coupled.
- The child entity (
-
Insert Order Dependency
- The parent entity must be persisted first.
- Only then can the child entity be created because it needs the parent's ID.
Example flow:
save(User) save(UserProfile) -
Less Flexibility
- If in the future the relationship changes from one-to-one → one-to-many, the schema must be redesigned.
- A separate foreign key mapping would be easier to extend.
-
Harder to Manage Independent Lifecycle
- Since the child shares the same primary key, managing it independently becomes difficult.
- For example:
- Deleting
Userautomatically invalidatesUserProfile.
- Deleting
-
Not Suitable for Optional Relationships
- If the relationship is optional,
@MapsIdmay not be ideal. - Sometimes a
Usermight exist without aUserProfile.
- If the relationship is optional,
-
More Complex for Beginners
-
Developers unfamiliar with JPA may find:
@MapsId- shared primary key
-
entity lifecycle
slightly harder to understand compared to simple FK mapping.
-
-
Migration / Schema Changes Are Harder
- If you later need to add a separate primary key to the child table, it requires database migration and entity refactoring.
When @MapsId Is Ideal
Use it when:
the child entity is just an extension of the parent entity and shares the same lifecycle.
- The relationship is strictly one-to-one
- The child cannot exist without the parent
- The child is more like an extension of the parent
Top comments (0)