JPA
Hibernate
mappedBy
Java
ORM

Can someone explain mappedBy in JPA and Hibernate?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

In JPA and Hibernate, mappedBy declares the inverse (non-owning) side of a bidirectional relationship. It tells the persistence provider "the foreign key lives on the other entity, in the field named by this string." Without mappedBy, Hibernate assumes both sides own the relationship and creates a redundant join table. The entity that does not have mappedBy (the one with @JoinColumn) is the owning side and is the only side that controls inserts and updates to the foreign key column.

The Owning Side vs. the Inverse Side

Every bidirectional relationship in JPA has exactly two sides:

Owning side: the entity whose table contains the foreign key column. This side has @JoinColumn (or is the default owner in @ManyToMany). Changes to this side's collection or reference are written to the database.

Inverse side: the entity that declares mappedBy. This side is read-only from the persistence provider's perspective. Setting or adding entities on the inverse side alone does not produce SQL INSERT or UPDATE statements for the foreign key.

This distinction is the single most important concept for understanding mappedBy. If you set both sides correctly, Hibernate generates efficient SQL. If you only set the inverse side, the relationship is silently not persisted.

One-to-Many / Many-to-One

This is the most common relationship type. The "many" side always owns the relationship because that is where the foreign key naturally lives.

java
1@Entity
2public class Department {
3    @Id
4    @GeneratedValue(strategy = GenerationType.IDENTITY)
5    private Long id;
6
7    private String name;
8
9    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
10    private List<Employee> employees = new ArrayList<>();
11
12    // Helper method to maintain both sides
13    public void addEmployee(Employee employee) {
14        employees.add(employee);
15        employee.setDepartment(this);
16    }
17
18    public void removeEmployee(Employee employee) {
19        employees.remove(employee);
20        employee.setDepartment(null);
21    }
22}
23
24@Entity
25public class Employee {
26    @Id
27    @GeneratedValue(strategy = GenerationType.IDENTITY)
28    private Long id;
29
30    private String name;
31
32    @ManyToOne(fetch = FetchType.LAZY)
33    @JoinColumn(name = "department_id")
34    private Department department;
35}

The mappedBy = "department" string refers to the department field in the Employee class. Hibernate reads this to understand that Employee.department_id is the foreign key column and Department.employees is a mirror that does not produce its own SQL.

What SQL Hibernate Generates

sql
1CREATE TABLE department (
2    id BIGINT AUTO_INCREMENT PRIMARY KEY,
3    name VARCHAR(255)
4);
5
6CREATE TABLE employee (
7    id BIGINT AUTO_INCREMENT PRIMARY KEY,
8    name VARCHAR(255),
9    department_id BIGINT,
10    FOREIGN KEY (department_id) REFERENCES department(id)
11);

No join table is created. The foreign key lives on the employee table, exactly where you want it.

What Happens Without mappedBy

If you remove mappedBy from @OneToMany:

java
// BAD: missing mappedBy
@OneToMany(cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();

Hibernate treats the Department.employees collection as a unidirectional @OneToMany and creates a join table:

sql
1CREATE TABLE department_employees (
2    department_id BIGINT,
3    employees_id BIGINT,
4    FOREIGN KEY (department_id) REFERENCES department(id),
5    FOREIGN KEY (employees_id) REFERENCES employee(id)
6);

This join table is almost never what you want for a one-to-many relationship. It adds unnecessary storage, complicates queries, and degrades performance.

One-to-One

In a one-to-one relationship, either side can own the relationship. The side with @JoinColumn is the owner.

java
1@Entity
2public class User {
3    @Id
4    @GeneratedValue(strategy = GenerationType.IDENTITY)
5    private Long id;
6
7    private String username;
8
9    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
10    private UserProfile profile;
11}
12
13@Entity
14public class UserProfile {
15    @Id
16    @GeneratedValue(strategy = GenerationType.IDENTITY)
17    private Long id;
18
19    private String bio;
20
21    @OneToOne(fetch = FetchType.LAZY)
22    @JoinColumn(name = "user_id", unique = true)
23    private User user;
24}

The foreign key user_id lives in the user_profile table. User.profile is the inverse side.

Lazy Loading Caveat for @OneToOne

On the inverse side (User.profile), Hibernate cannot determine whether the associated entity is null or not without querying the database. This means FetchType.LAZY on the inverse @OneToOne side is often not honored. Hibernate issues an eager query to check if a profile exists. The owning side (UserProfile.user) supports lazy loading correctly because Hibernate can check the user_id column value.

If lazy loading on the inverse side is critical for performance, consider using @MapsId to share the primary key:

java
1@Entity
2public class UserProfile {
3    @Id
4    private Long id;  // Same as User.id
5
6    @OneToOne(fetch = FetchType.LAZY)
7    @MapsId
8    @JoinColumn(name = "id")
9    private User user;
10}

Many-to-Many

In a many-to-many relationship, a join table is always required. One side must declare mappedBy to prevent Hibernate from creating two join tables.

java
1@Entity
2public class Student {
3    @Id
4    @GeneratedValue(strategy = GenerationType.IDENTITY)
5    private Long id;
6
7    @ManyToMany
8    @JoinTable(
9        name = "student_course",
10        joinColumns = @JoinColumn(name = "student_id"),
11        inverseJoinColumns = @JoinColumn(name = "course_id")
12    )
13    private Set<Course> courses = new HashSet<>();
14}
15
16@Entity
17public class Course {
18    @Id
19    @GeneratedValue(strategy = GenerationType.IDENTITY)
20    private Long id;
21
22    @ManyToMany(mappedBy = "courses")
23    private Set<Student> students = new HashSet<>();
24}

Student is the owning side. Adding or removing entries in student.getCourses() triggers inserts/deletes in the student_course table. Modifying course.getStudents() alone does nothing in the database.

The mappedBy String Must Match the Field Name Exactly

The mappedBy value is a string that must exactly match the Java field name on the owning side. It is case-sensitive and refers to the Java property, not the database column.

java
1// Owning side
2@ManyToOne
3@JoinColumn(name = "dept_id")  // database column name
4private Department department;  // Java field name
5
6// Inverse side
7@OneToMany(mappedBy = "department")  // must match Java field name, NOT "dept_id"
8private List<Employee> employees;

If the string is wrong, Hibernate throws a MappingException at startup:

 
org.hibernate.AnnotationException: mappedBy reference an unknown target entity property:
Employee.dept in Department.employees

Synchronizing Both Sides

Because Hibernate only persists changes from the owning side, you must set both sides when working within the same persistence context (same transaction or same session). Otherwise the in-memory object graph is inconsistent.

java
1// WRONG: only setting the inverse side
2department.getEmployees().add(employee);
3// employee.department is still null -> no FK update in database
4
5// CORRECT: set the owning side (and optionally the inverse for in-memory consistency)
6employee.setDepartment(department);
7department.getEmployees().add(employee);

The recommended pattern is to use helper methods on the parent entity that maintain both sides, as shown in the Department.addEmployee() example above.

Quick Reference Table

RelationshipOwning side (has @JoinColumn)Inverse side (has mappedBy)Join table created
@OneToMany / @ManyToOne@ManyToOne side@OneToMany sideNo (FK on "many" table)
@OneToOneSide with @JoinColumnOther sideNo (FK on owner's table)
@ManyToManySide with @JoinTableOther sideYes (one join table)
@OneToMany without mappedByBoth sides treated as ownersN/AYes (unintended join table)

Common Pitfalls

Only modifying the inverse side. This is the most frequent mistake. Adding an entity to the mappedBy collection without setting the owning side's reference means the foreign key is never written. The entity appears in memory but is not persisted. Always set the owning side.

Typo in the mappedBy string. The value must match the Java field name exactly, including case. A mismatch causes a startup MappingException. This error is easy to introduce during refactoring when you rename a field but forget to update the mappedBy string. IDEs like IntelliJ flag this, but not all refactoring tools catch it.

Missing mappedBy on @OneToMany. Without mappedBy, Hibernate creates an unnecessary join table. This is valid JPA behavior (it models a unidirectional @OneToMany), but it is almost never the intended design for a bidirectional relationship.

N+1 query problem with eager fetching. Using FetchType.EAGER on the inverse side (the default for @ManyToOne and @OneToOne) loads associated entities immediately with each query. For collections, this produces N+1 queries. Use FetchType.LAZY and fetch explicitly with JOIN FETCH or @EntityGraph when you need the data.

Circular toString() and equals(). Bidirectional relationships create circular references. If both entities include the other in toString(), equals(), or hashCode(), you get a StackOverflowError. Exclude the inverse side from these methods or use only the entity's own fields (typically the primary key).

Summary

mappedBy declares the inverse side of a bidirectional JPA relationship. The owning side (the one without mappedBy, holding @JoinColumn or @JoinTable) controls all foreign key inserts and updates. The inverse side is read-only from the persistence provider's perspective. Always set the owning side when persisting relationships, use helper methods to synchronize both sides for in-memory consistency, and make sure the mappedBy string exactly matches the Java field name on the owning entity. For @OneToMany, omitting mappedBy causes Hibernate to generate an unnecessary join table.


Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.