Part 6 – JPA Relationships and Database Joins


Introduction

Relationships are the foundation of every enterprise application.

Consider these examples:

  • Customer → Orders
  • Order → Order Items
  • Employee → Department
  • User → Roles
  • Student → Courses

In relational databases, these relationships are represented using foreign keys.

In JPA, they are represented using annotations.

Poor relationship design often causes:

❌ N+1 queries

❌ Performance issues

❌ Circular references

❌ Excessive SQL

❌ Large object graphs

❌ Memory problems

This article covers:

✅ One-to-One

✅ One-to-Many

✅ Many-to-One

✅ Many-to-Many

✅ Join tables

✅ Cascade operations

✅ Orphan removal

✅ Bidirectional relationships

✅ Fetch types


Sample Database Model

DEPARTMENT
    |
    | 1
    |
    N
EMPLOYEE
    |
    | 1
    |
    N
ADDRESS

USER
    |
    N
    |
    N
ROLE

Relationship Types

RelationshipExample
One-to-OneEmployee → Passport
One-to-ManyDepartment → Employees
Many-to-OneEmployee → Department
Many-to-ManyUser → Roles

One-to-One Relationship

Example:

Employee
      |
      1
      |
      1
Passport

Database

EMPLOYEE
---------
EMP_ID

PASSPORT
---------
PASSPORT_ID
EMP_ID

Employee Entity

@Entity
public class Employee {

    @Id
    private Long id;

    @OneToOne
    @JoinColumn(name = "PASSPORT_ID")
    private Passport passport;
}

Passport Entity

@Entity
public class Passport {

    @Id
    private Long id;
}

One-to-Many

Example:

Department
      |
      1
      |
      N
Employee

Database

EMPLOYEE
---------
EMP_ID
DEPT_ID

Department Entity

@OneToMany(
    mappedBy = "department")
private List<Employee> employees;

Employee Entity

@ManyToOne
@JoinColumn(name = "DEPT_ID")
private Department department;

Many-to-One

This is the most common JPA relationship.

Examples:

  • Employee → Department
  • Order → Customer
  • Invoice → Account

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DEPT_ID")
private Department department;

Why ManyToOne Should Usually Be LAZY

Bad:

@ManyToOne(fetch = FetchType.EAGER)

Loading 100 employees:

Employee
Department
Manager
Location

Many extra queries.


Good:

@ManyToOne(fetch = FetchType.LAZY)

Load only when needed.


Many-to-Many

Example:

User
   N
    \
     USER_ROLE
    /
   N
Role

Join Table

USER_ROLE
----------
USER_ID
ROLE_ID

User Entity

@ManyToMany
@JoinTable(
    name = "USER_ROLE",
    joinColumns =
        @JoinColumn(name="USER_ID"),
    inverseJoinColumns =
        @JoinColumn(name="ROLE_ID")
)
private Set<Role> roles;

Why Many-to-Many Can Be Dangerous

Problems:

  • Complex SQL
  • Large joins
  • Difficult updates

Recommendation:

Create an explicit entity.

UserRole

JoinColumn

@JoinColumn(
    name = "DEPT_ID")

Defines:

  • Foreign key column.
  • Relationship owner.

mappedBy

Example:

@OneToMany(
    mappedBy = "department")

Meaning:

The Employee entity owns the relationship.

Avoid duplicate foreign keys.


Unidirectional Relationship

Employee
    |
Department

Only Employee knows Department.


Bidirectional Relationship

Employee ←→ Department

Both sides know each other.


Example

@OneToMany(
    mappedBy = "department")
private List<Employee> employees;
@ManyToOne
private Department department;

Cascade Operations

TypeDescription
PERSISTSave child
MERGEUpdate child
REMOVEDelete child
ALLEverything

Example

@OneToMany(
    cascade = CascadeType.ALL)
private List<Address> addresses;

Saving Employee:

Employee
Address
Address

Everything persists.


Orphan Removal

@OneToMany(
    orphanRemoval = true)

Removing child:

employee.getAddresses()
        .remove(address);

Database:

Address deleted.

Fetch Types

TypeMeaning
EAGERLoad immediately
LAZYLoad later

Default Fetch Types

RelationshipDefault
OneToManyLAZY
ManyToOneEAGER
OneToOneEAGER
ManyToManyLAZY

Recommended Strategy

RelationshipRecommendation
ManyToOneLAZY
OneToManyLAZY
ManyToManyLAZY
OneToOneLAZY

EAGER Problem

Employee employee =
    repository.findById(1L);

Generated:

SELECT employee

SELECT department

SELECT manager

SELECT location

Many unnecessary queries.


LAZY Loading

employee.getDepartment();

Only then:

SELECT *
FROM DEPARTMENT

N+1 Query Problem

Example:

employees.forEach(
    e -> e.getDepartment());

Queries:

1 Employee query

100 Department queries

Total:

101 queries

Join Fetch Solution

@Query("""
select e
from Employee e
join fetch e.department
""")

One query.


SQL Generated

SELECT *
FROM EMPLOYEE E
JOIN DEPARTMENT D
ON E.DEPT_ID=D.DEPT_ID

Entity Graph

@EntityGraph(
    attributePaths = "department")

Loads associations efficiently.


Cascade Best Practices

RelationshipRecommendation
Parent → ChildCascade
ManyToManyAvoid ALL
Reference DataNo cascade

Equals and HashCode

Avoid:

equals(id)

before persistence.

Recommended:

Business key
or
Generated identifier after persist

JSON Serialization Problem

Bidirectional:

Department
    Employees
        Department
            Employees

Infinite loop.


Jackson Solution

@JsonManagedReference

@JsonBackReference

or:

@JsonIgnore

Recommended Entity

@Entity
public class Employee {

    @Id
    private Long id;

    @ManyToOne(
        fetch = FetchType.LAZY)
    private Department department;
}

Department

@Entity
public class Department {

    @OneToMany(
        mappedBy = "department")
    private List<Employee> employees;
}

Common Mistakes

EAGER Everywhere

Bad.


Cascade ALL on ManyToMany

Dangerous.


Bidirectional Everything

Unnecessary.


Large Object Graphs

Memory issues.


Missing Join Fetch

N+1 queries.


Best Practices

✅ Prefer LAZY.

✅ Use join fetch.

✅ Avoid ManyToMany.

✅ Use explicit join entities.

✅ Keep relationships small.

✅ Use DTOs.

✅ Avoid exposing entities to REST.

✅ Monitor SQL.


Interview Questions

Difference between OneToMany and ManyToOne?

OneToMany is parent side.

ManyToOne owns foreign key.


What is mappedBy?

Indicates relationship owner.


What causes N+1?

Lazy loading in loops.


Why avoid EAGER?

Excessive queries.


What is orphanRemoval?

Deletes removed children.


Why avoid ManyToMany?

Complex SQL and updates.


Summary

This article covered:

✅ OneToOne

✅ OneToMany

✅ ManyToOne

✅ ManyToMany

✅ Join tables

✅ Fetch types

✅ Cascades

✅ Orphan removal

✅ N+1 problems

✅ Join fetch


Next Article

Part 7 – Composite Keys and Embedded IDs

Topics:

  • Composite keys
  • EmbeddedId
  • IdClass
  • Natural keys
  • Surrogate keys
  • Junction entities
  • equals and hashCode
  • UUIDs
  • Shared primary keys
  • Best practices

Leave a Reply

Your email address will not be published. Required fields are marked *