Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 34

JPA Entity Relationships

Recommended resources

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, a User has one UserProfile.
  • One-to-Many (@OneToMany): One entity instance can be related to many instances of another entity. For example, a Category can have many Products.
  • 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: many Products belong to one Category.
  • Many-to-Many (@ManyToMany): Many entity instances can be related to many instances of another entity. For example, a Student can enroll in many Courses, and a Course can have many Students.

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 many Product entities can be associated with one Category entity.
  • @JoinColumn(name = "category_id"): This is the key annotation for the owning side. It specifies that the products table should have a column named category_id which will store the ID of the related Category.

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 one Category can be associated with many Product entities.
  • 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 the category field within the Product class." This prevents JPA from creating a redundant join table and correctly uses the foreign key we defined in Product.

This image perfectly illustrates the result of our mapping:

One-to-Many/Many-to-One Bidirectional Relationship in JPA
This diagram shows how our `Category` (EntityA) and `Product` (EntityB) classes are mapped. The `Product` entity holds the `@ManyToOne` annotation and corresponds to the database table with the foreign key, making it the owning side. The `Category` entity uses `@OneToMany` with `mappedBy` and does not have the foreign key.

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 @JoinColumn to specify the foreign key.

  • Inverse Side (e.g., UserProfile):

    @OneToOne(mappedBy = "userProfile")
    private User user;
    

    And just like @OneToMany, we use mappedBy to 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 @JoinTable annotation 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, mappedBy points 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 a Category with a new Product in its set, the Product will 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 calling category.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:

  1. The Book entity is the owning side, so its table gets the foreign key. We use @ManyToOne and @JoinColumn(name = "author_id") to define this.
  2. The Author entity is the inverse side. We use @OneToMany and mappedBy = "author" to point to the author field in the Book class, which contains the mapping configuration.
  3. We've also added fetch = FetchType.LAZY as a good practice. cascade = CascadeType.ALL on the Author'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 mappedBy attribute to point back to the field on the owning side.
  • It's a best practice to use FetchType.LAZY to 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.