Efficiently loading the JPA entities can have a major impact on the application performance. It is especially important when loading multiple related entities, as the number of SQL queries can quickly grow.
For example, when we need to retrieve multiple users and their address, we can easily fall into the "N+1" query problem: one query to load all the users, then one query per user to load their address. In that case the performance can be terrible. It can get even worse if multiple relations are loaded: 1+N+M+... queries.
In this article, I will show several standard JPA solutions that help load multiple related entities with a single
queries: JPQL JOIN FETCH
, Criteria fetch, and entity graphs.
Let's consider a simple JPA model with several entities. A car has a maker and an owner. In addition, an owner has an address.
For exemple the Car
entity is:
@Entity
public class Car {
// Primary key, business attributes
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "maker_id")
private Maker maker;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id")
private Owner owner;
// Constructors, getters, setters, etc
}
As a reminder, it is usually recommended to use lazy relations for performance reasons, in order to prevent additional and inefficient SQL queries. I will explain why in a future dedicated article.
In the next sections, we will use different solutions to search the cars by color, and load all the related entities in a single SQL query: the cars, their maker, their owner and the owner's address.
JOIN FETCH
is an easy-to-use solution to selectively load multiple entities in a single query with a JPQL expression.
For example, in the Spring Data JPA repository of Car
, to efficiently load the cars of a given color:
@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
// Returns the cars + their maker + their owner + their owner address,
// using 1 query instead of 1+N+M+P
@Query("FROM Car c JOIN FETCH c.maker JOIN FETCH c.owner o JOIN FETCH o.address WHERE c.color = :color")
List<Car> findWithRelationsByColor(String color);
}
JOIN FETCH
is used three times, one for each relation we want to efficiently load.
The owner fetch is named o
in order to fetch the owner address with o.address
.
The generated query is:
select c.id,c.color,c.name,m.id,m.name,o.id,o.name,a.id,a.name from car c left join maker m on m.id=c.maker_id left join owner o on o.id=c.owner_id left join address a on a.id=o.address_id where c.color=?
Note that there are actually two variants: JOIN FETCH
and LEFT JOIN FETCH
.
The difference is the same in JPQL than in SQL.
Simply said, if the relation is optional and you want to load the parent entity even if the child entity is null,
then use LEFT JOIN FETCH
instead of JOIN FETCH
.
When using the JPA Criteria API, we can also load multiple entities in a single query. The next example performs the same query as the previous section but using the Criteria API:
/**
* Returns the cars + their maker + their owner + their owner address,
* using 1 query instead of 1+N+M+P
*/
public List<Car> findWithRelationsByColor(String color) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> query = cb.createQuery(Car.class);
Root<Car> root = query.from(Car.class);
// Load the makers in the same query
root.fetch("maker");
// Load the owners and their address in the same query
root.fetch("owner").fetch("address");
query.select(root).where(cb.equal(root.get("color"), color));
return entityManager.createQuery(query).getResultList();
}
The fetch
method is also called three times, one for each relation we want to efficiently load.
Notice that the address
fetch is performed on the owner
fetch result.
The generated query is:
select c.id,c.color,c.name,m.id,m.name,o.id,o.name,a.id,a.name from car c left join maker m on m.id=c.maker_id left join owner o on o.id=c.owner_id left join address a on a.id=o.address_id where c.color=?
The fetch
method takes an optional JoinType
second parameter to specify the type of join.
By default, a JoinType.JOIN
fetch is performed, similar to the JPQL JOIN FETCH
from the previous section.
But we can also use fetch("maker", JoinType.LEFT)
to perform the equivalent of a JPQL LEFT JOIN FETCH
.
JPA entity graphs support a declarative approach to define the relations to load. There are two variants, the regular entity graphs described in this section and the named entity graphs described in the next.
The regular entity graphs use annotations on the query method to define the relations to load. The next example performs the same query as the previous sections but using an entity graph:
@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
// Returns the cars + their maker + their owner + their owner address,
// using 1 query instead of 1+N+M+P
@EntityGraph(attributePaths = {"maker", "owner", "owner.address"})
@Query("FROM Car c WHERE c.color = :color")
List<Car> findWithRelationsByColor(String color);
}
The attributePaths
attribute of the @EntityGraph
annotation specifies the relations to load.
A dot notation is used to navigate the relations.
The generated query is:
select c.id,c.color,c.name,m.id,m.name,o.id,o.name,a.id,a.name from car c left join maker m on m.id=c.maker_id left join owner o on o.id=c.owner_id left join address a on a.id=o.address_id where c.color=?
The @EntityGraph
annotation supports an optional type
attribute to specify the loading strategy for the
relations not declared in the graph:
EntityGraphType.LOAD
: relations not declared in the graph will be lazily or eagerly loaded based on their configuration.EntityGraphType.FETCH
: this is the default, relations not declared in the graph will be lazily loaded. This is powerful as it is one of the few JPA solutions to selectively un-eager a relation.Regular entity graphs are convenient when you want to define the relations to load directly in the query method. But sometimes you prefer to reuse the same entity graph in multiple query methods. That's where named entity graphs come into play. First define the named entity graphs in entity classes than use them query methods.
For the same cars search as before, the entity class is:
@Entity
@NamedEntityGraph(
name = "car-full",
attributeNodes = {
@NamedAttributeNode("maker"),
@NamedAttributeNode(value = "owner", subgraph = "owner-address"),
},
subgraphs = {
@NamedSubgraph(name = "owner-address", attributeNodes = @NamedAttributeNode("address"))
}
)
public class Car {
// Atttributes, constructors, getters, setters, etc.
}
The @NamedEntityGraph
annotation defines an entity graph with a name and the concerned relations.
Subgraphs are used to navigate the relations, in our case the address of the owner.
Then the persistence method in the repository interface references the graph from its name:
@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
// Returns the cars + their maker + their owner + their owner address,
// using 1 query instead of 1+N+M+P
@EntityGraph("car-full")
@Query("FROM Car c WHERE c.color = :color")
List<Car> findWithRelationsByColor(String color);
}
The generated query is:
select c.id,c.color,c.name,m.id,m.name,o.id,o.name,a.id,a.name from car c left join maker m on m.id=c.maker_id left join owner o on o.id=c.owner_id left join address a on a.id=o.address_id where c.color=?
Once again, we can use the type
attribute of @EntityGraph
to refine the loading strategy.
The previous solutions work very well for "ToOne" relations (i.e. @OneToOne
and @ManyToOne
).
But what about "ToMany" relations (i.e. @OneToMany
and @ManyToMany
)?
They are lazy by default, so are loaded by additional queries unless specific measures are taken. Fortunately the previous solutions still apply to load them in a single query.
There are some limitations though.
First, it uses a cartesian product of the tables. This means that executing a single request may not always improve the performances. Sometimes using a dedicated query to load a single "ToMany" relation may be more efficient. Try both and pick the one that's better for your case.
Second, we can do so for at most one "ToMany" relation when they are mapped using a List
.
If you try to load, in a single query, multiple "ToMany" relations mapped with a List
,
you will get an org.hibernate.loader.MultipleBagFetchException
.
Switching from a List
to a Set
(e.g. @OneToMany(...) Set<Item> items
) fixes this issue but,
because of the cartesian product, it further multiplies the number of returned rows in the SQL result set,
meaning that performances may actually degrade.
In this article I showed several solutions with JPA to load multiple related entities using a single query:
JPQL (LEFT) JOIN FETCH
, Criteria fetch and (named) entity graphs.
This can bring major performance improvements to your applications.
© 2007-2025 Florian Beaufumé