Retrieving data from child tables in a Spring Boot application using JPA and Hibernate is a common taskβbut one that often leads to performance issues, unexpected null values, or LazyInitializationException errors if not handled correctly.
This article explains best practices for retrieving child entities, compares different fetching strategies, and shows when to use single queries, multiple queries, DTOs, and repository methodsβall in an anonymized, production-ready context.
Understanding ParentβChild Relationships in JPA
In JPA, child tables are usually modeled using relationships such as:
@OneToMany@ManyToOne@ManyToMany
A typical parentβchild mapping looks like this:
@Entity
public class ParentEntity {
@Id
private Long id;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<ChildEntity> children;
}
@Entity
public class ChildEntity {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private ParentEntity parent;
}
π Important: The
@ManyToOneside is the owning side and controls the foreign key.
The Core Question: How Should Child Data Be Retrieved?
There is no single correct answer. The correct approach depends on:
- How much data you need
- How often the relationship is accessed
- Performance requirements
- API vs UI use cases
Letβs explore the recommended strategies.
1. Retrieving Child Data via the Parent Entity
When to use
- You already need the parent
- The number of children is reasonable
- You are inside a transactional context
Example
@Transactional
public ParentEntity getParent(Long id) {
return parentRepository.findById(id).orElseThrow();
}
List<ChildEntity> children = parent.getChildren();
Pros
β Clean domain model
β Natural object navigation
β Easy to implement
Cons
β Risk of N+1 queries
β Lazy loading requires an open transaction
2. Fetching Child Entities Directly (Recommended for APIs)
When to use
- You only need child data
- You want predictable SQL
- You want better performance control
Spring Data JPA Repository
List<ChildEntity> findByParentId(Long parentId);
Generated SQL (simplified)
SELECT * FROM child_entity WHERE parent_id = ?
Pros
β Simple and efficient
β No lazy loading issues
β Ideal for REST endpoints
Cons
β Parent entity not automatically loaded
3. Using JPQL with JOIN FETCH (Single Query)
When to use
- You know exactly what associations you need
- One or two collections only
- Read-only scenarios
Example
@Query("""
SELECT p
FROM ParentEntity p
LEFT JOIN FETCH p.children
WHERE p.id = :id
""")
Optional<ParentEntity> findWithChildren(Long id);
Pros
β Single SQL query
β No lazy loading problems
Cons
β Cartesian product risk
β Poor scalability with multiple @OneToMany
β Not suitable for pagination
β οΈ Avoid
JOIN FETCHon multiple collections in one query.
4. Entity Graphs (Best Balance for Complex Models)
When to use
- You want flexible fetch strategies
- You want to keep repositories clean
- You need dynamic control over loading
Entity Graph Definition
@EntityGraph(attributePaths = {"children"})
Optional<ParentEntity> findById(Long id);
Pros
β Cleaner than JOIN FETCH
β Configurable per use case
β Safer with large models
Cons
β Still loads full entities
β Requires understanding of graphs
5. DTO Projections (Best Performance for Read Operations)
When to use
- API responses
- Large datasets
- Reporting or dashboards
Example
@Query("""
SELECT new com.example.dto.ChildDto(
c.id, c.value
)
FROM ChildEntity c
WHERE c.parent.id = :parentId
""")
List<ChildDto> findChildDtos(Long parentId);
Pros
β Fastest approach
β Minimal memory usage
β No entity side effects
Cons
β Not suitable for updates
β More code to maintain
Common Mistakes to Avoid
β Fetching everything eagerly
@OneToMany(fetch = FetchType.EAGER)
This often causes:
- Huge SQL joins
- Memory issues
- Poor scalability
β Prefer LAZY + explicit fetching.
β Not setting both sides of the relationship
child.setParent(parent);
parent.getChildren().add(child);
Failing to do this often results in null foreign keys.
β Accessing lazy collections outside transactions
LazyInitializationException
β Use @Transactional
β Or fetch explicitly via repository methods
Recommended Strategy (Production-Proven)
| Use Case | Recommended Approach |
|---|---|
| REST API | Repository method on child |
| UI View | Entity Graph |
| Reporting | DTO projection |
| Simple CRUD | Lazy loading |
| Bulk data | Child table queries |
Final Recommendation
Do not load everything by default. Load only what you need, when you need it.
A well-designed Spring Boot + JPA application:
- Defaults to LAZY loading
- Uses repository queries for child tables
- Uses DTOs for APIs
- Uses Entity Graphs for controlled eager loading
This approach ensures:
β Better performance
β Predictable SQL
β Cleaner architecture
β Fewer production issues


