Correctly Retrieve Data from Child Tables in Spring Boot with JPA and Hibernate

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 @ManyToOne side 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 FETCH on 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 CaseRecommended Approach
REST APIRepository method on child
UI ViewEntity Graph
ReportingDTO projection
Simple CRUDLazy loading
Bulk dataChild 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

This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back