JPA Join Fetch es una de las opciones de las que dispone el estándar de JPA a la hora de reducir el número de consultas que se generan contra la base de datos. Algo que es bastante habitual y que degrada el rendimiento sino tenemos cuidado. Vamos a ver un ejemplo ,para ello partiremos de dos clases Java (Experto e Impartición) que están relacionadas (un Experto imparte varias charlas).
El código Java de las clases utilizando anotaciones JPA será :
package com.arquitectajava; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity public class Experto { @Id private String nombre; public Experto() { super(); } @OneToMany(mappedBy = "experto", cascade = CascadeType.PERSIST) private List < Imparticion > imparticiones = new ArrayList < Imparticion > (); public List < Imparticion > getImparticiones() { return imparticiones; } public void setImparticiones(List < Imparticion > imparticiones) { this.imparticiones = imparticiones; } public String getNombre() { return nombre; } public Experto(String nombre) { super(); this.nombre = nombre; } public void setNombre(String nombre) { this.nombre = nombre; } public void addImparticion(Imparticion i) { imparticiones.add(i); } }
package com.arquitectajava; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @Entity public class Imparticion { @Id private int id; private Date fecha; private String titulo; @ManyToOne @JoinColumn(name = "nombreExperto") private Experto experto; public Imparticion(int id, Date fecha, String titulo, Experto experto) { super(); this.id = id; this.fecha = fecha; this.titulo = titulo; this.experto = experto; } public Experto getExperto() { return experto; } public void setExperto(Experto experto) { this.experto = experto; } public int getId() { return id; } public void setId(int id) { this.id = id; } public Date getFecha() { return fecha; } public void setFecha(Date fecha) { this.fecha = fecha; } public String getTitulo() { return titulo; } public void setTitulo(String titulo) { this.titulo = titulo; } }
Vamos a construir el programa principal que nos selecciona los Expertos para luego recorrer sus imparticiones.
package com.arquitectajava; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.persistence.TypedQuery; public class Principal { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("UnidadCharla"); EntityManager em = emf.createEntityManager(); TypedQuery < Experto > consulta = em.createQuery("select e from Experto e", Experto.class); List < Experto > lista = consulta.getResultList(); for (Experto e: lista) { System.out.println(e.getNombre()); List < Imparticion > imparticiones = e.getImparticiones(); for (Imparticion i: imparticiones) { System.out.println(i.getTitulo()); } } em.close(); } }
Si ejecutamos el programa podremos ver el resultado en la consola:
Los datos se imprimen , pero estamos realizando 3 consultas para obtenerlos .En primer lugar se seleccionan todos los Expertos y por cada Experto se realiza una consulta para saber sus imparticiones. Este es el problema clásico de las n+1 Queries. Cuando existan 100 imparticiones , se lanzarán 101 consultas lo cual es un verdadero problema. Vamos a solventar este problema utilizando JPA Join Fetch.
JPA Join Fetch
Para ello nos bastará con modificar la consulta de JPA que estamos utilizando y obligarla a que incluya las imparticiones añadiendo la clausula join fetch dentro de la consulta . Esto obligará a que Hibernate realice un Join en la base de datos. Ojo que hay que añadir una clausula distinct.
package com.arquitectajava; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.persistence.TypedQuery; public class Principal { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("UnidadCharla"); EntityManager em = emf.createEntityManager(); TypedQuery < Experto > consulta = em.createQuery("select distinct e from Experto e join fetch e.imparticiones", Experto.class); List < Experto > lista = consulta.getResultList(); for (Experto e: lista) { System.out.println(e.getNombre()); List < Imparticion > imparticiones = e.getImparticiones(); for (Imparticion i: imparticiones) { System.out.println(i.getTitulo()); } } em.close(); } }
Solución con JPA
Ahora la consulta es “select distinct e from Experto e join fetch e.imparticiones” obligando a JPA a incluir las imparticiones a través de un JOIN.
Ahora una sola consulta es suficiente ya que nos hemos apoyado en JPA Join Fetch y cargará un único grafo de objetos.
Recordemos que aunque los frameworks de persistencia aportan mucho , hay que saber utilizarlos. Un mal uso de estos frameworks puede generar los peores resultados.
Otros artículos relacionados :
Muy bien explicado, a parte era una de las cosas que no sabía. Gracias por el aporte y compartir el conocimiento.
Muchas felicidades también por tu blog,
gracias 🙂
Hay alguna forma de hacer mas de un join fetch en la misma consulta?, ya que cuando lo intento me sale una excepción org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
No deberias tener problemas se pueden realizar multioples fetch joins pero tienes que tener claro que coleccion devuelves
Me encontré con el mismo problema y, después de mucho buscar, encontré que la mejor solución es la que cuentan aquí:
https://stackoverflow.com/questions/30088649/how-to-use-multiple-join-fetch-in-one-jpql-query
de alegro que te fuera útil 🙂
Muchas gracias por este post, me ayudaste con un problema que tenia de un filtrado con jpa ya que no funcionaba pero colocando el join fetch funciono correctamente.
Query anterior:
@Query(“select s from SalesChannel s inner join s.categoryList cat where s.channelId = ?1 and cat.id =?2”)
Query actual:
@Query(“select s from SalesChannel s join fetch s.categoryList cat where s.channelId = ?1 and cat.id =?2”)
me alegro 🙂
Gracias !!
de nada 🙂
Hola amigo solo tengo una pregunta y es porque se debe colocar distinct en la consulta(Si se para que se usa distinct), pero no se porque se debe colocar, ya que imprimo sin el distinct, se imprime el primer pais en mi caso con las ciudades de nuevo y con el distinct se imprime normalmente la cantidad de paises con sus ciudades(La bd la revise y no estan repetidos los paises).
Es por el tema de los joins
Perdonen si interrumpo, pero estoy analizando la lógica para implementar comentarios y respuestas en un proyecto personal que tengo, así que no piensen que estoy loco por comentar otro tema así de la nada
[…] de imparticiones. Hubiera sido mejor utilizar un Join. Esto se soluciona habitualmente usando un fetchJoin que nos permite realizar un Join con JPA. Sin embargo nos queda un problema. ¿Cuantas consultas […]
[…] de imparticiones. Hubiera sido mejor utilizar un Join. Esto se soluciona habitualmente usando un fetchJoin que nos permite realizar un Join con JPA. Sin embargo nos queda un problema. ¿Cuantas consultas […]
TypeQuery y List llevan Notificaciones como tipo, pero no reconoce la página los menor y mayor 🙁
Perdón que no salieron los carácteres, me refiero a un Vector de objetos o un Vector de array de Objects en los join. Decir que se está usando Toplink.
No creo que haya solución sencilla , JPA 1 se quedaba muy corto 🙁 optaría por ver si el API directa de TopLink es más potente .En tal caso la usaría y encapsularía las llamadas dentro de las clases repositorio devolviendo List de objetos
Hola Cecilio, pues me han dado libertad, así que he cambiado la persistencia a EclipseLink 2. Sin embargo hay algo de aún no sé si estoy haciendo mal. Si hago una consulta como la tuya va perfecto y trabajo todo con objetos, sin embargo, en la consulta, si en lugar de hacer un “SELECT u FROM Usuarios u…” me traigo sólo varias columnas “SELECT u.Id, u.alias FROM Usuarios u…” me lo devuelve como un Vector de Object[]. Supongo que como son solo varios atributos no le hace el casting. ¿Cuál sería la mejor forma? Aquí como lo tengo actualmente: /**… Read more »
Sino recuerdo mal tienes la opcion de select new y pasar los parametros , echa un veo a : http://stackoverflow.com/questions/2355728/jpql-create-new-object-in-select-statement-avoid-or-embrace
Hola Cecilio, está muy bien el uso de TypeQuery pero sólo está disponible en JPA 2, mi problema esque el proyecto actual usa JPA 1 y me devuelve por defecto un Vector o Vector cuando hago joins.
¿qué me recomiendas? Gracias.