Week 5 Day 4 — Entity Relationships
Goal
Today I want to understand entity relationships in JPA and Spring Data JPA.
Main questions:
- What is an entity relationship?
- What is
@ManyToOne? - What is
@OneToMany? - What is the owning side?
- What does
mappedBymean? - What is lazy loading?
- What is eager loading?
- What is cascade?
- What is orphan removal?
- What is the N+1 query problem?
- How should REST APIs handle entity relationships?
- What are common exam traps?
1. Quick Review from Week 5 Day 3
In Day 3, I learned:
- A transaction is an all-or-nothing unit of work.
@Transactionaldefines a transaction boundary.- Transactions usually belong in the service layer.
- The persistence context tracks managed entities.
- Dirty checking updates managed entities automatically.
- Runtime exceptions roll back by default.
- Checked exceptions do not roll back by default.
readOnly = trueis for read operations.- Lazy loading usually needs an open persistence context.
Memory sentence:
Persistence context tracks managed entities inside a transaction.
Today I learn what happens when entities are connected to other entities.
2. What Is an Entity Relationship?
An entity relationship means:
One entity is connected to another entity.
Example business relationship:
A client has many tasks.
A task belongs to one client.
Java model:
ClientEntity
TaskEntity
Database model:
clients table
tasks table
Relationship:
tasks.client_id references clients.id
Memory sentence:
Entity relationships map Java object connections to database foreign keys.
3. Main Relationship Types
JPA has these relationship annotations:
@OneToOne
@OneToMany
@ManyToOne
@ManyToMany
Common meanings:
| Annotation | Meaning |
|---|---|
@ManyToOne | many child rows belong to one parent |
@OneToMany | one parent has many children |
@OneToOne | one entity connected to one entity |
@ManyToMany | many entities connected to many entities |
Most common in business apps:
@ManyToOne
@OneToMany
Example:
Many tasks belong to one client.
One client has many tasks.
4. Database Foreign Key First
Before Java annotations, understand the database.
Tables:
CREATE TABLE clients (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
client_id BIGINT NOT NULL,
title VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL,
CONSTRAINT fk_tasks_client
FOREIGN KEY (client_id)
REFERENCES clients(id)
);
Important:
The foreign key is in the tasks table.
Why?
Each task belongs to one client.
So each task row stores client_id.
Memory sentence:
In a many-to-one relationship, the many side usually has the foreign key.
5. @ManyToOne
@ManyToOne maps many entities to one entity.
Example:
Many tasks -> one client
Java:
@Entity
@Table(name = "tasks")
public class TaskEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
protected TaskEntity() {
}
public TaskEntity(String title, String status, ClientEntity client) {
this.title = title;
this.status = status;
this.client = client;
}
}
Meaning:
TaskEntity has a many-to-one relationship to ClientEntity.
The foreign key column is client_id.
Memory sentence:
@ManyToOneis usually placed on the child side.
6. @JoinColumn
@JoinColumn defines the foreign key column.
Example:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
Meaning:
tasks.client_id points to clients.id
name = "client_id" means:
foreign key column name in the tasks table
nullable = false means:
a task must have a client
Memory sentence:
@JoinColumntells JPA which foreign key column stores the relationship.
7. @OneToMany
@OneToMany maps one entity to many child entities.
Example:
One client -> many tasks
Java:
@Entity
@Table(name = "clients")
public class ClientEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks = new ArrayList<>();
protected ClientEntity() {
}
public ClientEntity(String name) {
this.name = name;
}
}
Meaning:
ClientEntity has many TaskEntity objects.
TaskEntity.client owns the relationship.
Memory sentence:
@OneToMany(mappedBy = "...")usually points to the field on the many side.
8. Full Relationship Example
Client:
@Entity
@Table(name = "clients")
public class ClientEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks = new ArrayList<>();
protected ClientEntity() {
}
public ClientEntity(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<TaskEntity> getTasks() {
return tasks;
}
}
Task:
@Entity
@Table(name = "tasks")
public class TaskEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
protected TaskEntity() {
}
public TaskEntity(String title, String status, ClientEntity client) {
this.title = title;
this.status = status;
this.client = client;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getStatus() {
return status;
}
public ClientEntity getClient() {
return client;
}
}
9. Owning Side
In JPA, the owning side is the side that controls the database relationship.
Simple definition:
The owning side is the side that owns the foreign key mapping.
In this example:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id")
private ClientEntity client;
TaskEntity is the owning side because:
tasks table has client_id foreign key
Memory sentence:
The owning side is usually the side with
@JoinColumn.
10. Inverse Side
The inverse side is the other side of a bidirectional relationship.
Example:
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks = new ArrayList<>();
This means:
ClientEntity.tasks is not the owning side.
TaskEntity.client owns the relationship.
mappedBy = "client" points to this field:
private ClientEntity client;
inside TaskEntity.
Memory sentence:
mappedBymeans: the other side owns the relationship.
11. mappedBy
Example:
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks;
The value "client" means:
Look at the field named client in TaskEntity.
That field owns the relationship.
Common mistake:
@OneToMany(mappedBy = "client_id")
Wrong.
Why?
mappedBy uses Java field name, not database column name.
Correct:
@OneToMany(mappedBy = "client")
Memory sentence:
mappedByuses the Java field name on the owning side.
12. Bidirectional Relationship
A bidirectional relationship means both entities reference each other.
Client has tasks:
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks;
Task has client:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id")
private ClientEntity client;
This lets Java code navigate:
client.getTasks()
task.getClient()
But bidirectional relationships are more complex.
Memory sentence:
Bidirectional means both sides have references.
13. Unidirectional Relationship
A unidirectional relationship means only one side knows about the other.
Example:
@Entity
public class TaskEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
}
But ClientEntity has no tasks list.
This is simpler.
For many applications, this is enough.
Memory sentence:
Use bidirectional relationships only when you really need navigation from both sides.
14. Keeping Both Sides Consistent
In bidirectional relationships, Java object references must be kept consistent.
Example helper method:
@Entity
@Table(name = "clients")
public class ClientEntity {
@OneToMany(mappedBy = "client", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TaskEntity> tasks = new ArrayList<>();
public void addTask(TaskEntity task) {
tasks.add(task);
task.setClient(this);
}
public void removeTask(TaskEntity task) {
tasks.remove(task);
task.setClient(null);
}
}
Task:
@Entity
@Table(name = "tasks")
public class TaskEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id")
private ClientEntity client;
void setClient(ClientEntity client) {
this.client = client;
}
}
Why helper methods?
They keep both Java sides synchronized.
Memory sentence:
In bidirectional relationships, update both sides in Java.
15. Lazy Loading
Lazy loading means:
Load related data only when it is accessed.
Example:
@ManyToOne(fetch = FetchType.LAZY)
private ClientEntity client;
When loading a task:
TaskEntity task = taskRepository.findById(id).orElseThrow();
JPA may load:
task row
but not immediately load:
client row
Only when code calls:
task.getClient().getName();
then JPA may load the client.
Memory sentence:
Lazy loading delays loading related data until needed.
16. Eager Loading
Eager loading means:
Load related data immediately together with the entity.
Example:
@ManyToOne(fetch = FetchType.EAGER)
private ClientEntity client;
When loading a task, JPA also loads the client.
This can sound convenient, but it can cause performance problems.
Memory sentence:
Eager loading loads related data immediately.
17. Lazy vs Eager
| Topic | Lazy | Eager |
|---|---|---|
| When loaded | when accessed | immediately |
| Performance | often better by default | can load too much |
| Risk | lazy loading exception | N+1 and large queries |
| Control | explicit fetching when needed | harder to control |
| Common recommendation | prefer for associations | use carefully |
Important practical rule:
Prefer LAZY by default.
Fetch explicitly when needed.
18. Default Fetch Types
JPA defaults are important exam traps.
Common defaults:
@ManyToOne -> EAGER by default
@OneToOne -> EAGER by default
@OneToMany -> LAZY by default
@ManyToMany -> LAZY by default
Because @ManyToOne defaults to eager, many developers explicitly write:
@ManyToOne(fetch = FetchType.LAZY)
Memory sentence:
Many-to-one is eager by default, so explicitly set it to lazy in many real projects.
19. LazyInitializationException
Lazy loading needs an open persistence context.
Example problem:
public TaskDto getTask(Long id) {
TaskEntity task = taskRepository.findById(id).orElseThrow();
return new TaskDto(
task.getId(),
task.getTitle(),
task.getClient().getName()
);
}
If this happens outside an active transaction or persistence context, accessing:
task.getClient().getName()
may fail with lazy loading error.
Common name:
LazyInitializationException
Memory sentence:
Lazy relationships must be accessed while the persistence context is open.
20. Good Practice: Map to DTO Inside Transaction
Good service:
@Transactional(readOnly = true)
public TaskDto getTask(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
return new TaskDto(
task.getId(),
task.getTitle(),
task.getStatus(),
task.getClient().getName()
);
}
Why better?
mapping happens while transaction/persistence context is open
DTO is returned to controller
controller does not touch lazy entity relationships
Memory sentence:
Load and map needed data in the service layer.
21. Do Not Return Entities Directly from REST APIs
Bad:
@GetMapping("/api/clients/{id}")
public ClientEntity getClient(@PathVariable Long id) {
return clientRepository.findById(id).orElseThrow();
}
Problems:
lazy loading during JSON serialization
N+1 queries during serialization
infinite recursion
sensitive fields exposed
API coupled to database model
unexpected huge JSON response
Better:
@GetMapping("/api/clients/{id}")
public ClientDto getClient(@PathVariable Long id) {
return clientService.findById(id);
}
Memory sentence:
Return DTOs, not entities.
22. Infinite JSON Recursion
Bidirectional relationship:
ClientEntity -> tasks
TaskEntity -> client
If Jackson serializes entity directly:
client -> tasks -> task -> client -> tasks -> task -> ...
This can cause infinite recursion.
DTOs solve this cleanly.
Example DTO:
public record ClientDto(
Long id,
String name,
List<TaskSummaryDto> tasks
) {
}
public record TaskSummaryDto(
Long id,
String title,
String status
) {
}
No circular reference.
23. Cascade
Cascade means:
Apply an operation from a parent entity to related child entities.
Example:
@OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
private List<TaskEntity> tasks = new ArrayList<>();
If I save a client, JPA may also save its tasks.
Common cascade types:
PERSIST
MERGE
REMOVE
REFRESH
DETACH
ALL
Memory sentence:
Cascade propagates entity operations to related entities.
24. CascadeType.PERSIST
Example:
@OneToMany(mappedBy = "client", cascade = CascadeType.PERSIST)
private List<TaskEntity> tasks = new ArrayList<>();
Meaning:
when persisting client, also persist new tasks
Useful when child lifecycle belongs to parent.
25. CascadeType.REMOVE
Example:
@OneToMany(mappedBy = "client", cascade = CascadeType.REMOVE)
private List<TaskEntity> tasks = new ArrayList<>();
Meaning:
when deleting client, also delete tasks
Danger:
can delete many rows accidentally
Use carefully.
Memory sentence:
Cascade remove is powerful and dangerous.
26. CascadeType.ALL
@OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
private List<TaskEntity> tasks = new ArrayList<>();
Means all cascade operations:
persist
merge
remove
refresh
detach
This is common in examples, but in real projects it must be used carefully.
Memory sentence:
Do not use
CascadeType.ALLautomatically everywhere.
27. Cascade on @ManyToOne Trap
Dangerous:
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "client_id")
private ClientEntity client;
Why dangerous?
Many tasks share one client.
Deleting one task could cascade remove to the client.
That could affect other tasks.
Usually avoid cascade from child to parent.
Better:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
Memory sentence:
Be very careful with cascade on
@ManyToOne.
28. Orphan Removal
Orphan removal means:
If a child entity is removed from the parent collection, delete it from the database.
Example:
@OneToMany(
mappedBy = "client",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<TaskEntity> tasks = new ArrayList<>();
If I do:
client.removeTask(task);
then the task can be deleted from the database.
Memory sentence:
orphanRemoval = truedeletes children removed from the parent collection.
29. Cascade Remove vs Orphan Removal
| Topic | Cascade Remove | Orphan Removal |
|---|---|---|
| Trigger | parent is removed | child removed from parent collection |
| Example | delete client -> delete tasks | remove task from client.tasks -> delete task |
| Use case | child cannot exist without parent | child lifecycle belongs to parent |
| Risk | deletes many children | accidental delete from collection |
Memory sentence:
Cascade remove reacts to parent delete. Orphan removal reacts to child removal from collection.
30. When Is Orphan Removal Good?
Good example:
Order -> OrderItem
Invoice -> InvoiceLine
Client -> ClientContact maybe
If an OrderItem cannot exist without an Order, orphan removal makes sense.
Maybe not good:
Client -> Task
Why?
Maybe tasks should be archived, reassigned, or kept for audit.
Always ask:
Can the child exist without the parent?
Should removing it from collection delete it permanently?
Memory sentence:
Use orphan removal only when child lifecycle truly belongs to parent.
31. N+1 Query Problem
N+1 problem means:
One query loads parent records, then additional queries load related records one by one.
Example:
@Transactional(readOnly = true)
public List<TaskDto> listTasks() {
List<TaskEntity> tasks = taskRepository.findByStatus("OPEN");
return tasks.stream()
.map(task -> new TaskDto(
task.getId(),
task.getTitle(),
task.getClient().getName()
))
.toList();
}
Potential SQL:
select * from tasks where status = 'OPEN'; -- 1 query
select * from clients where id = 1; -- query 1
select * from clients where id = 2; -- query 2
select * from clients where id = 3; -- query 3
...
If there are 100 tasks:
1 query for tasks
100 queries for clients
= 101 queries
This is N+1.
Memory sentence:
N+1 means one main query plus one query per row for related data.
32. Why N+1 Is Bad
N+1 causes:
many database round trips
slow API responses
high database load
performance problems in production
hard-to-see bugs
Small dev database:
looks fine
Production database:
very slow
Memory sentence:
N+1 is a performance bug that often appears only with real data.
33. How to Detect N+1
Enable SQL logs in dev:
spring:
jpa:
show-sql: true
Better logging:
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
Look for:
one query for list
many similar queries for related entity
Memory sentence:
SQL logs reveal N+1 problems.
34. Fix N+1 with Fetch Join
Repository:
@Query("""
select t
from TaskEntity t
join fetch t.client
where t.status = :status
""")
List<TaskEntity> findByStatusWithClient(@Param("status") String status);
Meaning:
Load tasks and clients in one query.
Service:
@Transactional(readOnly = true)
public List<TaskDto> listOpenTasks() {
return taskRepository.findByStatusWithClient("OPEN")
.stream()
.map(task -> new TaskDto(
task.getId(),
task.getTitle(),
task.getClient().getName()
))
.toList();
}
Memory sentence:
Fetch join loads needed relationships together.
35. join vs join fetch
JPQL join:
select t from TaskEntity t join t.client c
This joins for filtering.
JPQL fetch join:
select t from TaskEntity t join fetch t.client
This joins and loads the relationship.
Memory sentence:
joinhelps query.join fetchalso loads the association.
36. Fix N+1 with EntityGraph
Repository:
@EntityGraph(attributePaths = "client")
List<TaskEntity> findByStatus(String status);
Meaning:
When finding tasks by status, also fetch client.
This is another way to define a fetch plan.
Memory sentence:
@EntityGraphtells Spring Data JPA which associations to fetch.
37. Fix N+1 with DTO Projection
Instead of loading full entities:
@Query("""
select new de.klarsync.tasks.TaskListDto(
t.id,
t.title,
t.status,
c.name
)
from TaskEntity t
join t.client c
where t.status = :status
""")
List<TaskListDto> findTaskListDtos(@Param("status") String status);
DTO:
public record TaskListDto(
Long id,
String title,
String status,
String clientName
) {
}
This returns exactly what the API needs.
Memory sentence:
DTO projection can avoid loading unnecessary entity graphs.
38. Fetch Join with Pagination Trap
Be careful with fetch joining collections and pagination.
Example risky query:
@Query("""
select c
from ClientEntity c
join fetch c.tasks
""")
Page<ClientEntity> findAllWithTasks(Pageable pageable);
Problem:
fetch joining collection relationships with pagination can produce wrong or inefficient results
Safer options:
use DTO projection
fetch only many-to-one relationships
use two-step query
use EntityGraph carefully
batch fetching
custom query design
Memory sentence:
Be careful with collection fetch joins and pagination.
39. @ManyToOne Usually Safe to Fetch Join
Fetching many-to-one is usually easier:
@Query("""
select t
from TaskEntity t
join fetch t.client
where t.status = :status
""")
Page<TaskEntity> findByStatusWithClient(
@Param("status") String status,
Pageable pageable
);
Because each task has one client.
But always test generated SQL and performance.
40. Relationship Design for REST APIs
Bad API shape:
{
"id": 1,
"name": "Client A",
"tasks": [
{
"id": 10,
"title": "Task 1",
"client": {
"id": 1,
"name": "Client A",
"tasks": [...]
}
}
]
}
Better DTO shape:
{
"id": 1,
"name": "Client A",
"tasks": [
{
"id": 10,
"title": "Task 1",
"status": "OPEN"
}
]
}
Memory sentence:
API response shape should be designed with DTOs, not automatically generated from entity relationships.
41. Good DTO Examples
Client detail:
public record ClientDetailDto(
Long id,
String name,
List<TaskSummaryDto> tasks
) {
}
Task summary:
public record TaskSummaryDto(
Long id,
String title,
String status
) {
}
Task detail:
public record TaskDetailDto(
Long id,
String title,
String status,
Long clientId,
String clientName
) {
}
Notice:
DTOs include only what the endpoint needs.
42. Example: Create Task for Client
Controller:
@PostMapping("/api/clients/{clientId}/tasks")
public ResponseEntity<TaskDetailDto> createTask(
@PathVariable Long clientId,
@Valid @RequestBody CreateTaskRequest request
) {
TaskDetailDto created = taskService.createTask(clientId, request);
URI location = URI.create("/api/tasks/" + created.id());
return ResponseEntity.created(location).body(created);
}
Service:
@Transactional
public TaskDetailDto createTask(Long clientId, CreateTaskRequest request) {
ClientEntity client = clientRepository.findById(clientId)
.orElseThrow(() -> new ResourceNotFoundException("Client", clientId));
TaskEntity task = new TaskEntity(
request.title(),
"OPEN",
client
);
TaskEntity saved = taskRepository.save(task);
return new TaskDetailDto(
saved.getId(),
saved.getTitle(),
saved.getStatus(),
client.getId(),
client.getName()
);
}
Important:
Find parent entity.
Set parent on child entity.
Save child entity.
Return DTO.
43. Example: List Tasks with Client Name
Repository:
@Query("""
select t
from TaskEntity t
join fetch t.client
where t.status = :status
order by t.createdAt desc
""")
List<TaskEntity> findByStatusWithClient(@Param("status") String status);
Service:
@Transactional(readOnly = true)
public List<TaskDetailDto> listTasks(String status) {
return taskRepository.findByStatusWithClient(status)
.stream()
.map(task -> new TaskDetailDto(
task.getId(),
task.getTitle(),
task.getStatus(),
task.getClient().getId(),
task.getClient().getName()
))
.toList();
}
This avoids N+1 for client names.
44. Example: Client with Task Summaries
Repository:
@Query("""
select c
from ClientEntity c
left join fetch c.tasks
where c.id = :id
""")
Optional<ClientEntity> findByIdWithTasks(@Param("id") Long id);
Service:
@Transactional(readOnly = true)
public ClientDetailDto getClientDetail(Long id) {
ClientEntity client = clientRepository.findByIdWithTasks(id)
.orElseThrow(() -> new ResourceNotFoundException("Client", id));
List<TaskSummaryDto> tasks = client.getTasks()
.stream()
.map(task -> new TaskSummaryDto(
task.getId(),
task.getTitle(),
task.getStatus()
))
.toList();
return new ClientDetailDto(
client.getId(),
client.getName(),
tasks
);
}
This can be fine for one client detail endpoint.
For paginated clients, be more careful with collection fetch joins.
45. @ManyToMany Warning
@ManyToMany exists, but use it carefully.
Example:
User <-> Role
Student <-> Course
In real business apps, a join entity is often better.
Instead of direct many-to-many:
@ManyToMany
private Set<RoleEntity> roles;
Often better:
UserEntity
UserRoleEntity
RoleEntity
Why?
join table may need extra fields
createdAt
assignedBy
status
metadata
better control
Memory sentence:
For complex many-to-many relationships, use a join entity.
46. Entity Equals and HashCode Warning
Entity relationships often use collections.
Be careful with equals() and hashCode().
Bad design can cause:
duplicate collection problems
lazy loading problems
unexpected behavior in Set
issues before ID is generated
Simple beginner rule:
Be careful using entities in HashSet.
Do not include lazy relationships in equals/hashCode.
For now, use List in examples and avoid complicated equality logic until needed.
47. Common Exam Traps
Trap 1
@ManyToOne is usually the owning side.
Trap 2
The owning side usually has @JoinColumn.
Trap 3
mappedBy uses the Java field name, not the database column name.
Trap 4
@ManyToOne is eager by default, so many projects explicitly set it to lazy.
Trap 5
Lazy loading needs an open persistence context.
Trap 6
Do not return entities directly from REST controllers.
Trap 7
Bidirectional relationships can cause infinite JSON recursion.
Trap 8
Cascade does not mean database foreign key cascade only. It is JPA entity operation cascade.
Trap 9
Do not use CascadeType.ALL everywhere automatically.
Trap 10
Be careful with cascade on @ManyToOne.
Trap 11
Orphan removal deletes child rows when removed from parent collection.
Trap 12
N+1 means one main query plus many relationship queries.
Trap 13
Fetch join can fix N+1, but collection fetch joins with pagination are dangerous.
Trap 14
DTO projections can be better than loading large entity graphs.
Trap 15
Use DTOs to design API response shape.
48. Real Exam Question: @ManyToOne
Question:
What does @ManyToOne mean?
Answer:
@ManyToOne means many instances of one entity are associated with one instance of another entity. For example, many tasks belong to one client.
49. Real Exam Question: @OneToMany
Question:
What does @OneToMany mean?
Answer:
@OneToMany means one entity has a collection of many related entities. For example, one client has many tasks.
50. Real Exam Question: Owning Side
Question:
What is the owning side of a JPA relationship?
Answer:
The owning side is the side that controls the database relationship, usually the side with the foreign key and @JoinColumn.
51. Real Exam Question: mappedBy
Question:
What does mappedBy mean?
Answer:
mappedBy marks the inverse side of a bidirectional relationship and points to the Java field name on the owning side.
52. Real Exam Question: Lazy Loading
Question:
What is lazy loading?
Answer:
Lazy loading means related entities are loaded only when they are accessed, not immediately when the parent entity is loaded.
53. Real Exam Question: Eager Loading
Question:
What is eager loading?
Answer:
Eager loading means related entities are loaded immediately together with the owning entity.
54. Real Exam Question: Default Fetch
Question:
What is the default fetch type for @ManyToOne?
Answer:
@ManyToOne is eager by default, so many real projects explicitly configure it as lazy.
55. Real Exam Question: Cascade
Question:
What is cascade?
Answer:
Cascade means that an entity operation, such as persist, merge, or remove, is propagated from one entity to its related entities.
56. Real Exam Question: Orphan Removal
Question:
What does orphanRemoval = true do?
Answer:
It deletes a child entity when it is removed from the parent collection, assuming the relationship is managed correctly.
57. Real Exam Question: N+1
Question:
What is the N+1 query problem?
Answer:
The N+1 problem happens when one query loads a list of entities, and then one additional query is executed for each entity to load a related association.
58. Real Exam Question: Fetch Join
Question:
How can a fetch join help with N+1?
Answer:
A fetch join loads the main entity and the needed association in the same query, reducing many additional relationship queries.
59. Interview Answer
Question:
Explain
@ManyToOneand@OneToMany.
Good answer:
@ManyToOne means many child entities point to one parent entity, such as many tasks belonging to one client. This side usually has the foreign key and @JoinColumn, so it is often the owning side. @OneToMany is the parent-side collection, such as a client having many tasks. In a bidirectional relationship, @OneToMany(mappedBy = "client") points to the client field in the child entity.
60. Interview Answer
Question:
What is the owning side in JPA?
Good answer:
The owning side is the side that controls the relationship in the database. In a typical many-to-one relationship, the many side owns the relationship because it has the foreign key column. For example, TaskEntity has client_id, so TaskEntity.client is the owning side. The other side uses mappedBy.
61. Interview Answer
Question:
What is lazy loading and why can it be dangerous?
Good answer:
Lazy loading means JPA loads related entities only when they are accessed. It is useful because it avoids loading unnecessary data. But it can be dangerous if the persistence context is already closed, which can cause lazy loading exceptions. It can also cause N+1 query problems if code loops through many entities and accesses a lazy association one by one.
62. Interview Answer
Question:
What is the N+1 query problem?
Good answer:
N+1 happens when one query loads a list of entities, and then one extra query is executed for each entity to load related data. For example, one query loads 100 tasks, and then 100 more queries load each task’s client. This creates 101 queries and can seriously hurt performance. It can be fixed with fetch joins, entity graphs, DTO projections, or better query design.
63. Interview Answer
Question:
What is cascade and when should you be careful?
Good answer:
Cascade means that operations on one entity are propagated to related entities. For example, saving a parent can also save its children. It is useful when the child lifecycle belongs to the parent, like order items inside an order. But it is dangerous to use blindly, especially CascadeType.ALL or cascade remove. I avoid cascade from child to parent, such as @ManyToOne(cascade = ALL), because deleting a child could delete a shared parent.
64. Interview Answer
Question:
Why should REST APIs not return JPA entities directly?
Good answer:
Returning entities directly can expose internal database structure, leak sensitive fields, trigger lazy loading during JSON serialization, cause infinite recursion in bidirectional relationships, and create N+1 performance problems. I prefer mapping entities to DTOs inside the service layer and returning DTOs from controllers.
65. Tiny Code Practice
Entity relationship:
@Entity
public class TaskEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private ClientEntity client;
}
@Entity
public class ClientEntity {
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks = new ArrayList<>();
}
Questions:
- Which side owns the relationship?
- What does
client_idmean? - What does
mappedBy = "client"point to? - Is
TaskEntity.clientlazy or eager? - Should this entity be returned directly from REST controller?
Answers:
TaskEntity.clientowns the relationship.- It is the foreign key column in the tasks table.
- It points to the
clientfield inTaskEntity. - Lazy, because
fetch = FetchType.LAZY. - Usually no. Return DTOs.
66. Tiny Bug Practice 1
Problem:
@OneToMany(mappedBy = "client_id")
private List<TaskEntity> tasks;
Question:
What is wrong?
Answer:
mappedBy must use the Java field name on the owning side, not the database column name.
Correct:
@OneToMany(mappedBy = "client")
private List<TaskEntity> tasks;
67. Tiny Bug Practice 2
Problem:
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "client_id")
private ClientEntity client;
Question:
Why is this dangerous?
Answer:
Many tasks can share one client. Cascading all operations from task to client can accidentally update or delete the shared client when operating on a task. Avoid cascade from child to parent unless you have a very specific reason.
68. Tiny Bug Practice 3
Problem:
@GetMapping("/api/clients/{id}")
public ClientEntity getClient(@PathVariable Long id) {
return clientRepository.findById(id).orElseThrow();
}
Question:
What can go wrong?
Answer:
Returning the entity directly can cause lazy loading errors, infinite JSON recursion, N+1 queries during serialization, sensitive data exposure, and tight coupling between API and database model. Return a DTO instead.
Practice Questions and Answers
Question 1
What is an entity relationship?
Answer:
An entity relationship maps a connection between Java entities to a database relationship, usually through a foreign key.
Question 2
What does @ManyToOne mean?
Answer:
@ManyToOne means many instances of one entity are associated with one instance of another entity, such as many tasks belonging to one client.
Question 3
What does @OneToMany mean?
Answer:
@OneToMany means one entity has a collection of many related entities, such as one client having many tasks.
Question 4
Where is the foreign key usually located in a many-to-one relationship?
Answer:
The foreign key is usually located on the many side, such as tasks.client_id.
Question 5
What does @JoinColumn do?
Answer:
@JoinColumn defines the foreign key column used for the relationship.
Question 6
What is the owning side?
Answer:
The owning side is the side that controls the database relationship, usually the side with @JoinColumn.
Question 7
What does mappedBy mean?
Answer:
mappedBy marks the inverse side of a bidirectional relationship and points to the field on the owning side.
Question 8
Does mappedBy use Java field name or database column name?
Answer:
mappedBy uses the Java field name, not the database column name.
Question 9
What is lazy loading?
Answer:
Lazy loading means related data is loaded only when it is accessed.
Question 10
What is eager loading?
Answer:
Eager loading means related data is loaded immediately together with the entity.
Question 11
What is the default fetch type for @ManyToOne?
Answer:
@ManyToOne is eager by default.
Question 12
What can cause LazyInitializationException?
Answer:
It can happen when code accesses a lazy relationship after the persistence context is closed.
Question 13
What is cascade?
Answer:
Cascade means entity operations are propagated from one entity to related entities.
Question 14
Why is CascadeType.ALL dangerous if used everywhere?
Answer:
Because it can propagate persist, merge, remove, refresh, and detach operations unexpectedly, possibly deleting or modifying related data accidentally.
Question 15
What is orphan removal?
Answer:
Orphan removal deletes a child entity when it is removed from the parent collection.
Question 16
What is the difference between cascade remove and orphan removal?
Answer:
Cascade remove deletes children when the parent is deleted. Orphan removal deletes a child when it is removed from the parent collection.
Question 17
What is the N+1 query problem?
Answer:
N+1 is when one query loads a list and then one additional query is executed for each item to load related data.
Question 18
How can fetch join help with N+1?
Answer:
A fetch join loads the main entity and the needed association in the same query, reducing extra queries.
Question 19
Why can bidirectional relationships cause JSON recursion?
Answer:
Because parent references child and child references parent, so JSON serialization can loop endlessly.
Question 20
Why should REST APIs usually return DTOs instead of entities?
Answer:
DTOs avoid exposing internal database structure, prevent lazy loading and recursion problems, control response shape, and protect sensitive fields.
Final Memory Sentences
- Entity relationships map Java object connections to database foreign keys.
@ManyToOneusually belongs on the child side.@OneToManyusually represents a parent collection.- The many side usually owns the foreign key.
- The owning side usually has
@JoinColumn. mappedBypoints to the Java field on the owning side.mappedBydoes not use database column names.- Lazy loading loads related data only when accessed.
- Eager loading loads related data immediately.
@ManyToOneis eager by default.- Prefer lazy associations and explicit fetch plans in many real projects.
- Lazy loading needs an open persistence context.
- Do not return entities directly from REST APIs.
- Bidirectional relationships can cause infinite JSON recursion.
- Cascade propagates entity operations.
- Do not use
CascadeType.ALLblindly. - Be careful with cascade on
@ManyToOne. - Orphan removal deletes children removed from parent collections.
- N+1 means one main query plus many relationship queries.
- Fetch join, entity graph, and DTO projection can help avoid N+1.