Skip to content

Relational Mappings

The Professional’s Guide to Spring Data JPA: Relational Mappings

Section titled “The Professional’s Guide to Spring Data JPA: Relational Mappings”

Objective: To achieve a deep, foundational understanding of how to map relationships between entities in JPA, focusing on the underlying database principles, common pitfalls like the N+1 problem, and production-grade best practices.


Module : Relational Mapping - Connecting Your Entities

Section titled “Module : Relational Mapping - Connecting Your Entities”

In a real application, your objects are not islands. A Customer has Orders. A Product has a Category. Relational mapping is the process of teaching JPA how to manage these connections.

The Golden Rule: The “Owning Side” and Foreign Keys

Section titled “The Golden Rule: The “Owning Side” and Foreign Keys”

Before we look at any annotations, we must understand one database truth: a relationship between two tables is maintained by a foreign key column in one of the tables.

  • The Owning Side: In JPA, the entity that corresponds to the table holding the foreign key column is called the “owning side” of the relationship. This is where you will define the physical mapping using the @JoinColumn annotation.
  • The Inverse Side (or Non-Owning Side): The other entity in the relationship is the “inverse side.” It uses the mappedBy attribute to tell JPA, “I am not responsible for this relationship. The mapping is already defined by the ‘owning’ side.”

Remembering this rule will solve 90% of your mapping confusion.


The Concept: A one-to-one relationship means one record in a table is associated with exactly one record in another table.

The Database Reality: The user_profiles table will contain a foreign key user_id that points back to the users table. Therefore, the UserProfile entity is the owning side.

JPA Implementation (Bidirectional - Best Practice):

// === The Inverse (Non-Owning) Side ===
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// A User has one UserProfile.
// 'mappedBy = "user"': Tells Hibernate "The mapping for this relationship is
// defined on the 'user' field in the UserProfile class."
// cascade = CascadeType.ALL: If I save a User, save its profile too. If I delete
// a User, delete its profile. (Domino effect)
// orphanRemoval = true: If I set user.setUserProfile(null), the old profile
// is an orphan and should be deleted.
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private UserProfile userProfile;
// Helper method to keep both sides in sync
public void setUserProfile(UserProfile profile) {
this.userProfile = profile;
profile.setUser(this);
}
}
// === The Owning Side ===
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// A UserProfile is associated with one User.
// fetch = FetchType.LAZY: Don't load the User object until I explicitly call getuser().
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false) // Defines the foreign key column in this table.
private User user;
}

@OneToMany & @ManyToOne: One Customer has many Orders

Section titled “@OneToMany & @ManyToOne: One Customer has many Orders”

The Concept: The most common relationship. One Customer can have multiple Orders, but each Order belongs to only one Customer.

The Database Reality: The orders table must contain a customer_id foreign key column. There is no other logical way to know which customer an order belongs to. Therefore, the Order entity is the owning side.

JPA Implementation (Bidirectional - The Standard Pattern):

// === The Inverse (Non-Owning) "One" Side ===
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// A Customer can have many Orders.
// 'mappedBy = "customer"': The mapping is defined by the 'customer' field in the Order class.
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// Helper methods for synchronization
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this);
}
}
// === The Owning "Many" Side ===
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Many Orders belong to one Customer. This is the owner.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false) // Defines the foreign key.
private Customer customer;
}

Sensi’s Tip: In a @OneToMany relationship, the @ManyToOne side is almost always the owner and responsible for the @JoinColumn.


@ManyToMany: Many Students can enroll in many Courses

Section titled “@ManyToMany: Many Students can enroll in many Courses”

The Concept: A Student can be in many Courses, and a Course can have many Students.

The Database Reality: This is impossible to model with a single foreign key. You must create a third table, called a Join Table (e.g., student_courses), that has foreign keys to both the students table and the courses table.

JPA Implementation (The “Easy” Way - Not Recommended for Production):

@Entity
public class Student {
@Id private Long id;
@ManyToMany
@JoinTable(
name = "student_courses", // Name of the join table
joinColumns = @JoinColumn(name = "student_id"), // FK to this entity's table
inverseJoinColumns = @JoinColumn(name = "course_id") // FK to the other entity's table
)
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@Id private Long id;
@ManyToMany(mappedBy = "courses") // Use mappedBy on the inverse side
private Set<Student> students = new HashSet<>();
}

Interview Gold: Why you should AVOID @ManyToMany The “easy” way has a huge flaw: the join table is hidden from you. What if you need to store extra information about the relationship, like the enrollment_date or the student’s grade in that course? @ManyToMany cannot do this.

The Professional Solution (The “Join Entity” Pattern): You manually create an entity for the join table. This is the best practice.

// The new "Join Entity"
@Entity
public class Enrollment {
@Id @GeneratedValue private Long id;
@ManyToOne @JoinColumn(name = "student_id")
private Student student;
@ManyToOne @JoinColumn(name = "course_id")
private Course course;
private LocalDateTime enrollmentDate;
private String grade;
}
// Student and Course now have a @OneToMany to the Enrollment entity
@Entity
public class Student {
@OneToMany(mappedBy = "student")
private Set<Enrollment> enrollments;
}
@Entity
public class Course {
@OneToMany(mappedby = "course")
private Set<Enrollment> enrollments;
}

Cascading & Orphan Removal - The Domino Effects

Section titled “Cascading & Orphan Removal - The Domino Effects”
  • Cascading (cascade = ...): “When I perform an operation on the parent entity, automatically apply the same operation to its related child entities.”

    • CascadeType.PERSIST: If I save a new Customer, also save its new Orders.
    • CascadeType.MERGE: If I update a detached Customer, also update its Orders.
    • CascadeType.REMOVE: If I delete a Customer, also delete all their Orders.
    • CascadeType.ALL: A shortcut for all of the above and more. Convenient, but can have unintended consequences if used carelessly.
  • Orphan Removal (orphanRemoval = true): This is a Hibernate-specific feature that is subtler than cascading. It applies only to the child collection.

    • The Rule: “If a child entity is removed from its parent’s collection, that child is now an ‘orphan’ and should be deleted from the database.”
    • Example:
      Customer customer = customerRepository.findById(1L).get();
      // The customer has 3 orders.
      customer.getOrders().remove(0); // We remove one order from the Java list.
      customerRepository.save(customer); // On save, Hibernate sees the orphan and issues a DELETE statement for that order.

Fetch Strategies: The #1 Performance Topic

Section titled “Fetch Strategies: The #1 Performance Topic”

Fetch strategies determine when JPA loads related entities from the database.

  • FetchType.LAZY (The Professional’s Choice):

    • Analogy: On-demand loading.
    • How it works: When you load a Customer, JPA does not load their Orders. The orders collection will be a special proxy object. Only when you explicitly call customer.getOrders().size() or iterate over the list does Hibernate issue a second query to fetch the orders.
    • Default for: @OneToMany, @ManyToMany.
  • FetchType.EAGER (The Dangerous Default):

    • Analogy: Pre-loading everything.
    • How it works: When you load a Customer, JPA immediately loads its Orders in the same query (or a subsequent one).
    • Default for: @OneToOne, @ManyToOne.

Interview Gold: The N+1 Select Problem This is the most common performance disaster caused by incorrect fetching.

The Scenario:

// Let's assume Post has a @OneToMany relationship to Comment (which is LAZY by default)
List<Post> posts = postRepository.findAll(); // 1 query: "SELECT * FROM posts"
// Now, in your code, you loop through the posts to display them
for (Post post : posts) {
// The first time you access .getComments() for each post, a NEW query is fired!
System.out.println(post.getTitle() + " has " + post.getComments().size() + " comments.");
}

The Result: If you have N posts, this code will execute 1 + N SQL queries. One for the posts, and N more for the comments. This is incredibly inefficient.

The Solution: Use a JOIN FETCH in a custom query to tell JPA to load everything in a single, efficient query.

@Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments WHERE p.author = :author")
List<Post> findAllByAuthorWithComments(String author); // This will only execute 1 query.

Self-Referencing Relationships - Hierarchies

Section titled “Self-Referencing Relationships - Hierarchies”

Concept: When an entity has a relationship to itself. Use Case: An employee-manager hierarchy, a category tree with sub-categories.

@Entity
public class Employee {
@Id private Long id;
// An Employee can have one manager. Many employees can report to the same manager.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "manager_id")
private Employee manager;
// An Employee (as a manager) can have many subordinates.
@OneToMany(mappedBy = "manager")
private List<Employee> subordinates = new ArrayList<>();
}