Entity Relationships in JPA
Model a real relationship between two entities using @ManyToOne and @OneToMany, with a Student-and-Course example and the foreign key it generates.
Introduction
Real applications are rarely made of isolated tables. A student enrolls in a course; an order has line items; a blog post has comments. JPA lets you model these connections directly in your Java classes using relationship annotations, and Hibernate translates them into foreign keys and joins automatically. This lesson builds a Student-and-Course relationship: each course can have many students, and each student belongs to one course.
- The main relationship types JPA supports.
- How @ManyToOne models the 'many' side of a relationship and owns the foreign key.
- How @OneToMany models the 'one' side, mapped back to the owning side.
- How to avoid infinite loops when serializing related entities to JSON.
Relationship Types in JPA
JPA supports four relationship annotations, each describing how many rows on one side relate to how many rows on the other.
| Annotation | Meaning | Example |
|---|---|---|
| @OneToOne | One row relates to exactly one row on the other side | A User has one Profile |
| @OneToMany | One row relates to many rows on the other side | A Course has many Students |
| @ManyToOne | Many rows relate to one row on the other side | Many Students belong to one Course |
| @ManyToMany | Many rows relate to many rows on the other side | Students enroll in many Courses, each Course has many Students |
@OneToMany and @ManyToOne are almost always used together, as two sides of the same relationship. This lesson focuses on that pair, since it's the one you'll reach for most often.
The Owning Side: @ManyToOne
In a Student-Course relationship, Student is the 'many' side — many students can belong to the same course — so Student holds the foreign key column in the database, and @ManyToOne goes on its Course field. This is called the owning side of the relationship.
@Entitypublic class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name;
@ManyToOne @JoinColumn(name = "course_id") private Course course;
// getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public Course getCourse() { return course; } public void setCourse(Course course) { this.course = course; }}@JoinColumn(name = "course_id") tells Hibernate exactly what to name the foreign key column on the student table. Without it, Hibernate picks a default name automatically, but being explicit keeps your schema predictable.
The Inverse Side: @OneToMany
Course is the 'one' side, so it gets a collection of students annotated with @OneToMany. The mappedBy attribute is essential here — it tells Hibernate that this side does not own the relationship, and points to the field name on Student that already defines the foreign key.
@Entitypublic class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String title;
@OneToMany(mappedBy = "course") private List<Student> students = new ArrayList<>();
// getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; }
public String getTitle() { return title; } public void setTitle(String title) { this.title = title; }
public List<Student> getStudents() { return students; } public void setStudents(List<Student> students) { this.students = students; }}Click Run to see what this code prints.
Fetch Types
Every relationship has a fetch type controlling when the related data actually loads from the database. @ManyToOne defaults to eager loading (fetched immediately with the owning entity); @OneToMany defaults to lazy loading (fetched only when explicitly accessed).
@ManyToOne(fetch = FetchType.LAZY)@JoinColumn(name = "course_id")private Course course;It's common practice to explicitly set fetch = FetchType.LAZY on @ManyToOne relationships too, overriding the eager default. This avoids loading related entities you don't actually need for a given request, which keeps queries smaller and faster.
Avoiding Infinite JSON Recursion
If you return a Course directly from a controller, Jackson tries to serialize its list of Student objects — and each Student tries to serialize its Course back — producing an infinite loop and a StackOverflowError. The @JsonIgnore annotation on one side breaks the cycle.
@OneToMany(mappedBy = "course")@com.fasterxml.jackson.annotation.JsonIgnoreprivate List<Student> students = new ArrayList<>();@JsonIgnore works, but many real projects avoid returning JPA entities directly from controllers altogether, using separate DTO (Data Transfer Object) classes instead. That sidesteps recursion entirely and keeps your API response shape independent of your database schema.
Common Mistakes
- Putting mappedBy on the wrong side — it belongs on the @OneToMany (non-owning) side, referencing the field name on the owning entity.
- Forgetting @JsonIgnore or a DTO layer and hitting a StackOverflowError from infinite recursion when serializing related entities.
- Assuming @OneToMany is eager by default — it is lazy, and accessing the collection outside an active transaction throws a LazyInitializationException.
- Not initializing collection fields (private List<Student> students = new ArrayList<>();), leading to NullPointerException before the entity is ever persisted.
Best Practices
- Always put the foreign key (@ManyToOne) on the 'many' side, and mappedBy on the 'one' side.
- Explicitly set fetch = FetchType.LAZY on @ManyToOne relationships to avoid unnecessary eager loading.
- Break JSON recursion with @JsonIgnore for a quick fix, or a DTO layer for a cleaner long-term solution.
- Initialize collection fields at declaration time to avoid null checks scattered throughout your code.
Frequently Asked Questions
The side that holds the foreign key column in the database — in a @ManyToOne/@OneToMany pair, that's always the @ManyToOne side. The owning side is what Hibernate actually uses to persist and update the relationship.
It tells Hibernate that this side of the relationship is just a read-only view, mirroring a relationship that's really defined and persisted by the field named in mappedBy on the other entity.
Lazy-loaded data is only fetched when accessed, and that fetch needs an open Hibernate session/transaction. If the transaction has already closed — for example, after the repository call returns — trying to access the collection throws a LazyInitializationException.
That depends on your domain: if a student can only be in one course at a time, @ManyToOne/@OneToMany is correct. If a student can enroll in many courses simultaneously, @ManyToMany with a join table is the right model.
Key Takeaways
- @ManyToOne goes on the 'many' side and owns the foreign key column.
- @OneToMany goes on the 'one' side and uses mappedBy to point back at the owning field.
- @ManyToOne is eager by default and @OneToMany is lazy by default — many teams override @ManyToOne to lazy as well.
- Returning entities with bidirectional relationships directly from a controller risks infinite JSON recursion unless you use @JsonIgnore or DTOs.
Summary
You can now model real relationships between entities and understand how Hibernate turns them into foreign keys and joins. Next, you'll add validation to your API so invalid data — missing names, malformed emails — gets rejected automatically before it ever reaches your database.