Hello! Welcome back to our journey with Spring Data JPA.
In the last lesson, we laid the groundwork by defining standalone JPA entities. You learned how to map a Java class to a database table using essential annotations like @Entity, @Table, @Id, and @GeneratedValue. This is a fantastic start, but real-world data is rarely isolated. Think of a blog: a User writes many Posts, and a Post has many Comments.
Today, we'll build on that foundation to connect our entities, creating a rich and realistic data model. Our learning outcome is to map entity relationships using @OneToMany, @ManyToOne, @OneToOne, and @ManyToMany. By the end of this lesson, you will be able to represent the complex connections between different pieces of data in your application.
1. Relationships: The Heart of a Data Model
In database terms, relationships define how different tables are linked. JPA brings this concept directly into our Java code, allowing us to navigate between connected objects seamlessly. There are four main types of relationships we will cover:
- One-to-One (
@OneToOne): One entity instance is related to exactly one instance of another entity. For example, aUserhas oneUserProfile. - One-to-Many (
@OneToMany): One entity instance can be related to many instances of another entity. For example, aCategorycan have manyProducts. - Many-to-One (
@ManyToOne): Many entity instances can be related to one instance of another entity. This is the other side of a one-to-many relationship: manyProductsbelong to oneCategory. - Many-to-Many (
@ManyToMany): Many entity instances can be related to many instances of another entity. For example, aStudentcan enroll in manyCourses, and aCoursecan have manyStudents.
A crucial concept for all these relationships is the owning side. In a relationship between two entities, one is designated as the "owner." The owner is responsible for managing the link in the database. In physical terms, the database table corresponding to the owning entity is the one that holds the foreign key column that links to the other table.
2. The Most Common Pair: @OneToMany and @ManyToOne
Let's dive into the most frequent relationship you'll encounter: a bidirectional one-to-many/many-to-one association. We'll model a simple e-commerce scenario: a Category can contain multiple Products, and each Product belongs to a single Category.
The "Many" Side: @ManyToOne (The Owning Side)
We start with the Product entity. Since many products can belong to one category, this is the "many" side. We will make this the owning side of the relationship, which is the standard practice. This means the products table will contain a foreign key column (category_id) that references the categories table.
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// ... other product fields
@ManyToOne
@JoinColumn(name = "category_id") // Defines the foreign key column
private Category category;
// Constructors, Getters, Setters...
}
@ManyToOne: This annotation declares that manyProductentities can be associated with oneCategoryentity.@JoinColumn(name = "category_id"): This is the key annotation for the owning side. It specifies that theproductstable should have a column namedcategory_idwhich will store the ID of the relatedCategory.
The "One" Side: @OneToMany (The Inverse Side)
Now let's look at the Category entity. This is the "one" side of the relationship. It will hold a collection of all the products that belong to it.
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "category") // Points to the field that owns the relationship
private Set<Product> products = new HashSet<>();
// Constructors, Getters, Setters...
}
@OneToMany: This annotation declares that oneCategorycan be associated with manyProductentities.mappedBy = "category": This is the most important part. It tells JPA, "This side of the relationship is the inverse (non-owning) side. The actual mapping configuration (the@JoinColumn) is defined on thecategoryfield within theProductclass." This prevents JPA from creating a redundant join table and correctly uses the foreign key we defined inProduct.
This image perfectly illustrates the result of our mapping:

For a deeper dive into the mechanics of bidirectional relationships and the concept of the "owning side," the following article is an excellent resource.
Spring Data JPA: One-to-Many Relationship
The article "Spring Data JPA: One-to-Many Relationship" from CodingNomads provides a detailed explanation of how to handle these relationships, including the crucial concepts of directionality and the 'owning side'.
Please read the section titled "Bidirectional One-to-Many / Many-to-One Relationships". Focus on understanding the roles of the @ManyToOne side (the owning side) and the @OneToMany side (the inverse side), and how the mappedBy attribute connects them. Pay attention to the practical advice about persisting changes from the owning side.
3. Implementing the Other Relationship Types
Now that you've mastered the most common relationship type, let's quickly cover the others. They follow very similar principles. We'll use a single, comprehensive article to walk through the remaining types.
Entity Relationships and Fetching Strategies
The CodeSignal article "Entity Relationships and Fetching Strategies" provides a great overview of all four relationship types with clear examples and diagrams.
Please read the sections "Types of Relationships", "Understanding One-to-One Relationship", and "Understanding Many-to-Many Relationship". Focus on how each relationship type is implemented with its specific annotations (@OneToOne, @ManyToMany, @JoinTable).
Let's summarize the key points from the reading.
One-to-One (@OneToOne)
Imagine a User and a UserProfile. A user has only one profile, and a profile belongs to only one user.
-
Owning Side (e.g.,
User):@OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "profile_id", referencedColumnName = "id") private UserProfile userProfile;Similar to
@ManyToOne, we use@JoinColumnto specify the foreign key. -
Inverse Side (e.g.,
UserProfile):@OneToOne(mappedBy = "userProfile") private User user;And just like
@OneToMany, we usemappedByto point back to the owner.
Many-to-Many (@ManyToMany)
Consider Student and Course. A student can take many courses, and a course has many students. In a relational database, this requires an intermediate join table (or linking table) to store pairs of student_id and course_id. JPA handles this with the @JoinTable annotation.
-
Owning Side (e.g.,
Student):@ManyToMany @JoinTable( name = "student_courses", // Name of the join table joinColumns = @JoinColumn(name = "student_id"), // FK for the owning side (Student) inverseJoinColumns = @JoinColumn(name = "course_id") // FK for the other side (Course) ) private Set<Course> courses;The
@JoinTableannotation defines everything JPA needs to manage the relationship in this intermediate table. -
Inverse Side (e.g.,
Course):@ManyToMany(mappedBy = "courses") private Set<Student> students;Again,
mappedBypoints back to the field on the owning side.
4. A Quick Note on Cascading and Fetching
You may have noticed cascade and fetch attributes in the code examples. These are important for performance and behavior:
cascade = CascadeType.ALL: This is a convenience setting. It means that if you perform an operation (like saving or deleting) on a parent entity, that operation is "cascaded" to its related child entities. For example, if you save aCategorywith a newProductin its set, theProductwill also be saved automatically.fetch: This controls when related entities are loaded from the database.FetchType.LAZY(default for@...ToMany): Related entities are loaded only when you explicitly access them (e.g., by callingcategory.getProducts()). This is generally the best for performance.FetchType.EAGER(default for@...ToOne): Related entities are loaded immediately along with the parent entity. This can be convenient but can lead to performance issues if it loads too much data unnecessarily.
As a best practice, prefer FetchType.LAZY for all your relationships to start with, and only switch to EAGER if you have a specific, proven need.
Test your understanding!
Let's model a relationship between an Author and a Book. An Author can write many Books, and each Book is written by one Author.
Your task is to fill in the annotations for the fields below to create a correct bidirectional one-to-many relationship. Make the Book entity the owning side.
// In the Author class
// ...
// --- field to fill in ---
private Set<Book> books = new HashSet<>();
// In the Book class
// ...
// --- field to fill in ---
private Author author;
Show answer
Here is the correct implementation:
// In the Author class
// This is the inverse side ("one")
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Book> books = new HashSet<>();
// In the Book class
// This is the owning side ("many")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
Explanation:
- The
Bookentity is the owning side, so its table gets the foreign key. We use@ManyToOneand@JoinColumn(name = "author_id")to define this. - The
Authorentity is the inverse side. We use@OneToManyandmappedBy = "author"to point to theauthorfield in theBookclass, which contains the mapping configuration. - We've also added
fetch = FetchType.LAZYas a good practice.cascade = CascadeType.ALLon theAuthor's side is a common choice to manage the lifecycle of books along with their author.
Conclusion
Excellent work! You have now learned how to create a connected, relational data model using JPA. This is a huge step towards building sophisticated backend applications where data integrity and relationships are key.
Key Takeaways:
- JPA supports four relationship types:
@OneToOne,@OneToMany,@ManyToOne, and@ManyToMany. - Every relationship has an owning side, which manages the foreign key (or join table) in the database.
- The owning side uses
@JoinColumn(for foreign keys) or@JoinTable(for many-to-many). - The inverse (non-owning) side uses the
mappedByattribute to point back to the field on the owning side. - It's a best practice to use
FetchType.LAZYto optimize performance by avoiding unnecessary data loading.
What's Next?
We've meticulously designed our data model with entities and relationships. But how do we actually save, retrieve, update, and delete these entities? In our next lesson, we will explore JpaRepository, Spring Data's powerful interface that provides all of these database operations out-of-the-box, bringing our data model to life.