Data Persistence Basics: Relationship Behavior 🔗
The Friendship Analogy
Imagine you have friends. Some friendships go both ways — you know them, and they know you. Other friendships are one-way — maybe you follow a celebrity who doesn’t know you exist!
Database relationships work exactly the same way. Let’s explore how entities (like tables) become friends in Jakarta EE!
🔄 Bidirectional Relationships
What Is It?
A bidirectional relationship means BOTH sides know about each other.
Think of it like best friends:
- You can call your best friend
- Your best friend can call you back
- Both of you have each other’s phone number!
Simple Example
@Entity
public class Author {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "author")
private List<Book> books;
}
@Entity
public class Book {
@Id
private Long id;
private String title;
@ManyToOne
private Author author;
}
What’s happening here?
Authorknows about theirbooksBookknows about itsauthor- Both sides can find each other!
The Golden Rule 🌟
One side must be the “owner” of the relationship. The owner is the side WITHOUT mappedBy.
graph TD A["Author"] -->|knows about| B["Books"] B -->|knows about| A B -.->|Owner - has FK| DB["#40;Database#41;"]
➡️ Unidirectional Relationships
What Is It?
A unidirectional relationship means only ONE side knows about the other.
Think of it like following a celebrity:
- You know who Taylor Swift is
- Taylor Swift doesn’t know you exist
- Only you have her “phone number” (not the other way around!)
Simple Example
@Entity
public class Student {
@Id
private Long id;
private String name;
@ManyToOne
private School school;
}
@Entity
public class School {
@Id
private Long id;
private String name;
// No reference to students!
}
What’s happening here?
Studentknows whichSchoolthey attendSchoolhas NO IDEA which students are enrolled- Information flows in ONE direction only
graph LR S["Student"] -->|knows about| SC["School"] SC -.->|doesn't know| S
When to Use Each?
| Use This | When… |
|---|---|
| Bidirectional | You need to navigate both ways |
| Unidirectional | You only query from one direction |
🌊 Cascade Operations
What Is It?
Cascade means “when something happens to me, the same thing happens to my friends!”
Think of it like dominoes:
- You push one domino
- All connected dominoes fall too!
The Cascade Types
| Type | What It Does |
|---|---|
PERSIST |
Save me → Save my friends too |
REMOVE |
Delete me → Delete my friends too |
MERGE |
Update me → Update my friends too |
REFRESH |
Reload me → Reload my friends too |
ALL |
Everything above! |
Simple Example
@Entity
public class Order {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private List<OrderItem> items;
}
What’s happening here?
- When you save an
Order, allOrderItemsget saved automatically! - When you delete an
Order, allOrderItemsvanish too!
graph TD A["Save Order"] -->|CASCADE| B["Save Item 1"] A -->|CASCADE| C["Save Item 2"] A -->|CASCADE| D["Save Item 3"]
⚠️ Be Careful!
CascadeType.REMOVE can be dangerous! Deleting a parent might delete things you wanted to keep!
🎒 Fetch Types
What Is It?
Fetch type decides WHEN to load related data.
Think of it like packing for a trip:
- EAGER: Pack EVERYTHING now, just in case!
- LAZY: Pack only what you need, grab more later if needed
The Two Types
// EAGER: Load books immediately with author
@OneToMany(fetch = FetchType.EAGER)
private List<Book> books;
// LAZY: Load books only when accessed
@OneToMany(fetch = FetchType.LAZY)
private List<Book> books;
Visual Comparison
graph TD subgraph EAGER E1["Load Author"] --> E2["Load ALL Books"] E2 --> E3["Return Everything"] end subgraph LAZY L1["Load Author"] --> L2["Return Author Only"] L2 -.->|Later, if needed| L3["Load Books"] end
Default Fetch Types
| Relationship | Default |
|---|---|
@OneToOne |
EAGER |
@ManyToOne |
EAGER |
@OneToMany |
LAZY |
@ManyToMany |
LAZY |
😱 Lazy Loading Pitfalls
The Session Closed Problem
The #1 mistake with lazy loading!
The Problem:
// Inside a transaction
Author author = authorRepo.findById(1L);
// Transaction ends here...
// Outside transaction - BOOM! 💥
author.getBooks(); // LazyInitializationException!
Why does this happen?
- LAZY means “I’ll load later”
- But “later” needs a database connection
- The connection closed when the transaction ended!
Think of it like this:
You order pizza for “later delivery”. But when “later” arrives, the pizza shop is CLOSED! 🍕❌
Solutions
1. Fetch eagerly when you need it:
@Query("SELECT a FROM Author a " +
"JOIN FETCH a.books WHERE a.id = :id")
Author findWithBooks(@Param("id") Long id);
2. Keep the session open:
@Transactional
public void processAuthor(Long id) {
Author author = authorRepo.findById(id);
// Access lazy collections INSIDE transaction
author.getBooks().size(); // Works!
}
🐌 The N+1 Query Problem
What Is It?
The N+1 problem is when you accidentally make WAY more database calls than needed!
The Story
Imagine you’re a teacher with 30 students. You want to know each student’s grade.
Bad Way (N+1):
- Ask the office for the list of 30 students (1 query)
- Walk to each student’s desk, one by one (30 queries!)
- Total: 31 queries! 😰
Good Way:
- Ask the office for all students WITH their grades (1 query)
- Total: 1 query! 😊
Code Example
The Problem:
List<Author> authors = authorRepo.findAll();
// 1 query for authors
for (Author author : authors) {
author.getBooks(); // 1 query PER author!
}
// If 100 authors = 101 queries! 💀
The Solution - JOIN FETCH:
@Query("SELECT a FROM Author a " +
"JOIN FETCH a.books")
List<Author> findAllWithBooks();
// Just 1 query! 🚀
Visual of N+1
graph TD A["Query: Get 5 Authors"] --> B["Author 1"] A --> C["Author 2"] A --> D["Author 3"] A --> E["Author 4"] A --> F["Author 5"] B --> B1["Query: Get Books"] C --> C1["Query: Get Books"] D --> D1["Query: Get Books"] E --> E1["Query: Get Books"] F --> F1["Query: Get Books"] style A fill:#ff6b6b style B1 fill:#ffd93d style C1 fill:#ffd93d style D1 fill:#ffd93d style E1 fill:#ffd93d style F1 fill:#ffd93d
6 queries instead of 1! That’s the N+1 problem.
🎯 Quick Summary
| Concept | One-Line Summary |
|---|---|
| Bidirectional | Both sides know each other |
| Unidirectional | Only one side knows |
| Cascade | Actions ripple to related entities |
| Fetch EAGER | Load everything now |
| Fetch LAZY | Load later when needed |
| Lazy Pitfall | Session closes before you access data |
| N+1 Problem | Too many queries, use JOIN FETCH |
🌟 You’ve Got This!
Remember:
- Relationships are like friendships — some go both ways, some don’t
- Cascades are like dominoes — push one, others follow
- Fetch types are like packing — eager packs everything, lazy packs light
- Always keep your session open for lazy loading
- Use JOIN FETCH to avoid the N+1 trap
Now you understand how entities relate to each other in Jakarta EE! 🎉
