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