How to fetch children in JPA with association?
This guide explains how to effectively fetch child entities in JPA when working with associated parent-child relationships, covering different strategies and best practices.
1. Understanding JPA Associations
In JPA, relationships between entities are defined using annotations like @OneToMany, @ManyToOne, @OneToOne, and @ManyToMany. For parent-child relationships, @OneToMany on the parent and @ManyToOne on the child are most common. Let's consider a simple Parent-Child example.
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<Child> getChildren() { return children; }
public void setChildren(List<Child> children) { this.children = children; }
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Parent getParent() { return parent; }
public void setParent(Parent parent) { this.parent = parent; }
}
2. Fetching Strategies: EAGER vs. LAZY
JPA associations have a fetch type, which dictates when the associated data is loaded from the database. * FetchType.LAZY (Default for @OneToMany, @ManyToMany): The associated entities are not loaded immediately when the parent entity is loaded. They are loaded only when they are first accessed. This prevents loading unnecessary data, improving performance. * FetchType.EAGER (Default for @OneToOne, @ManyToOne): The associated entities are loaded immediately along with the parent entity. While convenient, EAGER fetching can lead to performance issues (N+1 queries, large memory consumption) if not used carefully, especially with collections.
It's generally recommended to stick with LAZY fetching for collections and explicitly fetch children only when needed, or use more optimized fetching strategies.
3. Recommended Approaches for Fetching Children
3.1. Lazy Loading (Default Behavior)
When you load a parent entity with its children marked as FetchType.LAZY, the children will only be fetched when you try to access the getChildren() method. This must happen within an active JPA session (e.g., within a transaction).
// Inside a transactional service method or main application context
@Transactional
public Parent getParentWithChildren(Long parentId) {
Parent parent = entityManager.find(Parent.class, parentId);
if (parent != null) {
// Accessing children here triggers lazy loading within the same transaction
parent.getChildren().size(); // Forces initialization
}
return parent;
}
If you try to access parent.getChildren() outside of an active session, you'll encounter a LazyInitializationException.
3.2. Using JOIN FETCH with JPQL/Criteria API
JOIN FETCH is the most common and recommended way to eagerly load associations efficiently. It instructs JPA to fetch the associated entities along with the root entity in a single SQL query, avoiding the N+1 problem.
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import java.util.List;
@Transactional
public class ParentRepository {
@PersistenceContext
private EntityManager entityManager;
public Parent findParentWithChildrenJoinFetch(Long parentId) {
return entityManager.createQuery(
"SELECT p FROM Parent p JOIN FETCH p.children WHERE p.id = :parentId", Parent.class)
.setParameter("parentId", parentId)
.getSingleResult();
}
public List<Parent> findAllParentsWithChildrenJoinFetch() {
return entityManager.createQuery(
"SELECT DISTINCT p FROM Parent p JOIN FETCH p.children", Parent.class)
.getResultList();
}
}
The DISTINCT keyword is often used with JOIN FETCH for @OneToMany relationships to prevent duplicate parent entities in the result set when multiple children exist for a single parent.
3.3. Entity Graphs
Entity Graphs provide a flexible way to define fetch plans declaratively, either directly on the entity using @NamedEntityGraph or dynamically. They allow you to specify which associations should be fetched eagerly for a particular query, overriding the default fetch types.
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@NamedEntityGraph(
name = "parent-with-children-graph",
attributeNodes = @NamedAttributeNode("children")
)
public class Parent {
// ... (same fields as before)
}
// In your repository or service
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import java.util.HashMap;
import java.util.Map;
@Transactional
public class ParentRepository {
@PersistenceContext
private EntityManager entityManager;
public Parent findParentWithChildrenUsingGraph(Long parentId) {
EntityGraph<?> entityGraph = entityManager.getEntityGraph("parent-with-children-graph");
Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.fetchgraph", entityGraph);
return entityManager.find(Parent.class, parentId, properties);
}
}
Entity Graphs are particularly useful for scenarios where you need different fetching behaviors for the same entity in various contexts without altering the default FetchType in the entity mapping.