
Por qué Entity Graphs
El problema empieza siempre igual. Una entidad Departamento tiene una colección @OneToMany de Empleado con FetchType.LAZY (la opción correcta por defecto). El servicio carga los departamentos para serializarlos en una respuesta REST que incluye el nombre de cada empleado.
@Entity
public class Departamento {
@Id @GeneratedValue
private Long id;
private String nombre;
@OneToMany(mappedBy = "departamento", fetch = FetchType.LAZY)
private List<Empleado> empleados = new ArrayList<>();
}
List<Departamento> departamentos = repository.findAll();
return departamentos.stream()
.map(d -> new DepartamentoConEmpleadosDto(
d.getId(),
d.getNombre(),
d.getEmpleados().stream().map(Empleado::getNombre).toList()))
.toList();
Hibernate ejecuta una query para findAll y, al iterar, una query adicional por cada departamento para resolver getEmpleados(). Diez departamentos generan once queries. Cien departamentos, ciento una. Es el clásico problema N+1 que tumba aplicaciones en producción.
La solución intuitiva es marcar la asociación como EAGER, pero eso afecta a todas las queries de la entidad y prohíbe optimizaciones cuando no se necesitan los empleados. El JOIN FETCH en JPQL funciona, pero rompe la paginación de Spring Data y se vuelve verboso si hay varias asociaciones.
Un Entity Graph declara para una query concreta qué asociaciones traer en el mismo SELECT, dejando intactas las anotaciones de la entidad. Es la herramienta idiomática para resolver N+1 sin perder flexibilidad.
NamedEntityGraph: declarativo y reutilizable
Cuando un patrón de carga es estable y se reutiliza en varios sitios, lo idiomático es declararlo en la entidad con @NamedEntityGraph:
@Entity
@NamedEntityGraph(
name = "Departamento.conEmpleados",
attributeNodes = @NamedAttributeNode("empleados")
)
@NamedEntityGraph(
name = "Departamento.conEmpleadosYRoles",
attributeNodes = {
@NamedAttributeNode(value = "empleados", subgraph = "empleados.roles")
},
subgraphs = @NamedSubgraph(
name = "empleados.roles",
attributeNodes = @NamedAttributeNode("roles")
)
)
public class Departamento {
@Id @GeneratedValue
private Long id;
private String nombre;
@OneToMany(mappedBy = "departamento", fetch = FetchType.LAZY)
private List<Empleado> empleados = new ArrayList<>();
}
Cada @NamedEntityGraph recibe un nombre único y describe qué atributos cargar. El segundo grafo es jerárquico: carga los empleados y, para cada empleado, sus roles. La sintaxis es algo verbosa, pero queda en la entidad como contrato.
Dynamic EntityGraph: construido en runtime
Cuando el grafo depende de parámetros (un caso de uso decide qué relaciones traer), se construye de forma programática con EntityManager.createEntityGraph:
@Service
@RequiredArgsConstructor
public class DepartamentoService {
private final EntityManager em;
public List<Departamento> buscarConGrafoDinamico(boolean conRoles) {
var grafo = em.createEntityGraph(Departamento.class);
var empleadosNode = grafo.addAttributeNodes("empleados");
if (conRoles) {
var empleadosSubgraph = grafo.addSubgraph("empleados");
empleadosSubgraph.addAttributeNodes("roles");
}
return em.createQuery(
"select d from Departamento d", Departamento.class)
.setHint("jakarta.persistence.loadgraph", grafo)
.getResultList();
}
}
A partir de Hibernate 7 también se puede usar la API fluida EntityGraph.builder():
EntityGraph<Departamento> grafo = EntityGraph.builder(Departamento.class)
.addAttributeNode("empleados")
.addSubgraph("empleados", sg -> sg.addAttributeNode("roles"))
.build(em);
El grafo dinámico es la opción cuando un endpoint admite query params del tipo ?include=empleados,roles y hay que componer el SELECT en función de ellos.
Spring Data JPA: la anotación @EntityGraph
En la práctica del 95% de los casos no se toca el EntityManager directamente. Spring Data integra Entity Graphs con la anotación @EntityGraph en cualquier método del repositorio:
public interface DepartamentoRepository extends JpaRepository<Departamento, Long> {
// 1. Por nombre del NamedEntityGraph
@EntityGraph(value = "Departamento.conEmpleados")
List<Departamento> findAll();
// 2. Ad hoc, sin declarar NamedEntityGraph
@EntityGraph(attributePaths = {"empleados"})
Optional<Departamento> findById(Long id);
// 3. Sobre un derived query
@EntityGraph(attributePaths = {"empleados", "empleados.roles"})
List<Departamento> findByActivoTrue();
// 4. Sobre @Query JPQL
@EntityGraph(attributePaths = {"empleados"})
@Query("select d from Departamento d where d.sede = :sede")
List<Departamento> buscarPorSede(@Param("sede") String sede);
// 5. Combinado con paginación
@EntityGraph(attributePaths = {"empleados"})
Page<Departamento> findByActivoTrue(Pageable pageable);
}
La opción 2 (attributePaths) es la más usada en proyectos modernos: declara el grafo donde se necesita, sin contaminar la entidad con anotaciones. La opción 5 funciona porque Spring Data ejecuta dos queries (count y data) y aplica el grafo sólo a la segunda.
El uso de
attributePathsdirectamente en el método del repositorio es el patrón idiomático en Spring Boot 4. Reserva@NamedEntityGraphpara grafos complejos que se reutilizan en al menos tres puntos del código.
LOAD frente a FETCH: dos semánticas distintas
JPA define dos hints para aplicar un grafo, con semánticas opuestas:
// LOAD: el grafo añade asociaciones a las que ya define la entidad
em.createQuery(...).setHint("jakarta.persistence.loadgraph", grafo);
// FETCH: el grafo es la única fuente de verdad (todo lo no listado es LAZY)
em.createQuery(...).setHint("jakarta.persistence.fetchgraph", grafo);
| Hint | Atributos en el grafo | Atributos NO en el grafo |
|------|-----------------------|--------------------------|
| loadgraph | EAGER en esta query | Respetan FetchType declarado en la entidad |
| fetchgraph | EAGER en esta query | LAZY siempre, ignorando FetchType.EAGER |
Spring Data usa loadgraph por defecto al usar @EntityGraph. Es el comportamiento más predecible: lo que listas se carga ansioso, lo que no listas mantiene su configuración original.
fetchgraph es útil cuando una entidad legada tiene asociaciones marcadas EAGER (mala práctica habitual) y se quiere romper esa carga ansiosa puntualmente. Permite forzar LAZY en todo lo no listado:
@EntityGraph(value = "Departamento.minimal", type = EntityGraph.EntityGraphType.FETCH)
List<Departamento> findAll();
graph TD
A[Query con EntityGraph] --> B{type}
B -->|LOAD - default| C[Grafo lista atributos EAGER<br/>Resto respeta FetchType]
B -->|FETCH| D[Grafo lista atributos EAGER<br/>Resto LAZY incluso si era EAGER]
C --> E[Predecible si la entidad ya está bien diseñada]
D --> F[Útil para limitar EAGER de entidades legadas]
Casos de uso típicos
Listado con detalle de hijos
El caso más común: listar departamentos junto con sus empleados, sin disparar N+1.
@EntityGraph(attributePaths = {"empleados"})
List<Departamento> findAll();
Hibernate genera un único SELECT con LEFT JOIN a empleado. Se elimina N+1.
Detalle profundo con dos niveles de asociación
Cargar un departamento, sus empleados y los roles de cada empleado en una sola operación:
@EntityGraph(attributePaths = {"empleados", "empleados.roles"})
Optional<Departamento> findById(Long id);
La notación con punto navega por subgrafos. Hibernate emite una query con dos JOIN adicionales.
Múltiples asociaciones del mismo nivel
Una entidad Empleado con departamento y proyectos:
@EntityGraph(attributePaths = {"departamento", "proyectos"})
Optional<Empleado> findByEmail(String email);
Aquí aparece la limitación clave de los Entity Graphs (y de JOIN FETCH): puede haber como máximo una colección (@OneToMany o @ManyToMany) traída por query si se quiere evitar el producto cartesiano. Si proyectos y otra colección se cargan a la vez, Hibernate lanza un MultipleBagFetchException o degrada a fetch separado en memoria.
Solución: dividir en dos queries o usar Set en lugar de List para una de las colecciones.
@OneToMany(mappedBy = "empleado", fetch = FetchType.LAZY)
private Set<Proyecto> proyectos = new HashSet<>();
Set permite a Hibernate combinar varios fetches sin lanzar la excepción, a costa de no garantizar el orden.
Comparativa con JOIN FETCH
JOIN FETCH en JPQL es la alternativa clásica:
@Query("""
select d from Departamento d
left join fetch d.empleados e
where d.activo = true
""")
List<Departamento> buscarActivosConEmpleados();
| Aspecto | Entity Graph (@EntityGraph) | JOIN FETCH (JPQL) |
|---------|-------------------------------|-------------------|
| Reutilizable en varios métodos | Sí (NamedEntityGraph) | No |
| Ad hoc por método | Sí (attributePaths) | Sí |
| Funciona con Pageable | Sí (Spring Data ejecuta dos queries) | No (warning HHH000104, paginación en memoria) |
| Funciona con métodos derivados | Sí | No (requiere @Query) |
| Aplica DISTINCT automático | Sí | No (manual: select distinct d) |
| Soporta múltiples colecciones | No (mismo límite) | No (mismo límite) |
Para listados paginados con asociaciones, siempre Entity Graph. JOIN FETCH solo cuando ya tienes una
@QueryJPQL compleja y añadir el fetch es una línea más.
Anti-patrones
Aplicar Entity Graph "por si acaso"
Cargar todas las asociaciones porque "puede que se usen luego" multiplica el coste de la query. Si un endpoint solo necesita el nombre del departamento, no añadas attributePaths = {"empleados"} por si acaso. Diseña el grafo para el caso de uso concreto.
Múltiples colecciones en el mismo grafo
@EntityGraph(attributePaths = {"empleados", "proyectos"}) // PELIGRO
List<Departamento> findAll();
Si ambas son List, Hibernate lanza MultipleBagFetchException. Si son Set, hace producto cartesiano (cada empleado se duplica por cada proyecto). Soluciones:
- Dividir en dos consultas: una con
empleados, otra conproyectos, combinar en memoria. - Usar
@BatchSizeodefault_batch_fetch_sizeglobal para una de las dos. - Cargar la segunda colección con un grafo distinto en un método aparte.
Combinar Entity Graph con DTO projection
@EntityGraph(attributePaths = {"empleados"})
@Query("select new com.empresa.DepartamentoDto(d.id, d.nombre) from Departamento d")
List<DepartamentoDto> findAllDto();
Esto no funciona: las DTO projections devuelven valores escalares, no entidades, así que el grafo se ignora. Si necesitas un DTO con asociaciones, usa una proyección de tupla con multiselect o devuelve la entidad y mapea fuera.
NamedEntityGraph para un único uso
Crear @NamedEntityGraph para un grafo que solo se usa en un sitio añade ruido a la entidad. Usa attributePaths ad hoc en el repositorio: la entidad queda limpia.
Buenas prácticas
- Asociaciones LAZY por defecto. Nunca
EAGERen@OneToManyni@ManyToMany. El grafo decide la carga ansiosa por query. - Un grafo por caso de uso. No reutilices "el grafo gordo" para todo. Cada endpoint declara lo que necesita.
attributePathspara grafos simples y locales.@NamedEntityGraphsolo si el grafo se reutiliza en tres o más puntos.- Una sola colección por grafo. Para múltiples colecciones, usar
@BatchSizeo varias queries. - Mide el resultado. Activa
spring.jpa.show-sql=trueodatasource-proxypara verificar que las queries esperadas son las que se ejecutan. - Combina con paginación con confianza. Spring Data hace dos queries: count sin grafo, data con grafo. Sin warnings, sin paginación en memoria.
Entity Graphs son la herramienta correcta para declarar la carga ansiosa por query sin acoplar la entidad. En proyectos Spring Boot 4 modernos, casi todo el N+1 se resuelve con
@EntityGraph(attributePaths = ...). JOIN FETCH y@BatchSizecubren los huecos restantes. Diseña tus entidades con LAZY siempre y deja que cada caso de uso pida lo que necesita.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Boot
Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Diseñar NamedEntityGraph en entidades para escenarios estables de carga. Construir EntityGraph dinámicos con EntityManager.createEntityGraph en runtime. Aplicar @EntityGraph en repositorios Spring Data y métodos derivados. Diferenciar javax.persistence.loadgraph y javax.persistence.fetchgraph. Detectar cuándo un Entity Graph supera a JOIN FETCH y cuándo no.