JPA Relationships
Most applications have related entities — books belong to authors, orders contain items. JPA maps these relationships to foreign keys.
@ManyToOne / @OneToMany — The Most Common
@Entity
@Table(name = "authors")
public class Author {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>();
// Helper methods
public void addBook(Book book) {
books.add(book);
book.setAuthor(this);
}
public void removeBook(Book book) {
books.remove(book);
book.setAuthor(null);
}
}
@Entity
@Table(name = "books")
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private Author author;
}
| Side | Annotation | Has FK? |
|---|---|---|
| Many (Book) | @ManyToOne |
✅ author_id |
| One (Author) | @OneToMany(mappedBy) |
❌ inverse side |
mappedBy = "author"means: “Look at theauthorfield inBookfor the FK.”
@ManyToMany
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "book_categories",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
private Set<Category> categories = new HashSet<>();
}
@Entity
public class Category {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "categories")
private Set<Book> books = new HashSet<>();
}
Creates a join table: book_categories(book_id, category_id).
@OneToOne
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "detail_id")
private BookDetail detail;
}
@Entity
public class BookDetail {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 5000)
private String summary;
private int pageCount;
private String language;
}
LAZY vs EAGER Loading
| Fetch Type | Behavior | Default For |
|---|---|---|
LAZY |
Loaded on access (separate query) | @OneToMany, @ManyToMany |
EAGER |
Loaded immediately (JOIN) | @ManyToOne, @OneToOne |
@ManyToOne(fetch = FetchType.LAZY) // ✅ Use LAZY
private Author author;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // default
private List<Book> books;
Rule: Always use
LAZY. Fetch eagerly only when you know you need the data.
The N+1 Problem
// Loads all authors (1 query)
List<Author> authors = authorRepository.findAll();
// Accessing books triggers 1 query PER author = N+1 queries!
for (Author author : authors) {
System.out.println(author.getName() + ": " + author.getBooks().size());
}
-- 1 query for authors
SELECT * FROM authors;
-- N queries for books (one per author!)
SELECT * FROM books WHERE author_id = 1;
SELECT * FROM books WHERE author_id = 2;
SELECT * FROM books WHERE author_id = 3;
... (100 authors = 100 extra queries)
Solution: JOIN FETCH
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
-- Single query!
SELECT a.*, b.* FROM authors a JOIN books b ON a.id = b.author_id;
Solution: @EntityGraph
@EntityGraph(attributePaths = {"books"})
List<Author> findAll();
Cascade Types
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books;
| Cascade | Effect |
|---|---|
PERSIST |
Saving author saves books too |
MERGE |
Updating author updates books |
REMOVE |
Deleting author deletes books |
ALL |
All of the above |
orphanRemoval = true — if a book is removed from the list, it’s deleted from DB.
Key Takeaways
@ManyToOneon the FK side,@OneToMany(mappedBy)on the inverse side.- Always use
LAZYloading —EAGERcauses surprise queries. - The N+1 problem is the #1 JPA performance issue — use
JOIN FETCHor@EntityGraph. - Use
CascadeType.ALL+orphanRemovalfor parent-child relationships. - Use helper methods (
addBook,removeBook) to keep both sides in sync.
What’s Next?
Next: Database Migrations — Flyway and Liquibase for production-grade schema management.