Mucha gente desconoce el concepto de JPA Entity Graph , y como nos pueden ayudar a mejorar el rendimiento de las consultas de JPA que creamos. Para entender como funcionan hay que recordar algunas cosas de JPA. En primer lugar que todas las consultas que realizamos oneToMany son lazy feching , es decir los datos se cargan según los vamos solicitando generándose en muchas ocasiones las indeseadas n+1 queries.
JPA OneToMany
Para entender mejor el concepto ,vamos a construir un ejemplo usando dos clases: Experto e Impartición. Ambas clases están relacionadas a través de una relación @oneToMany. Un Experto es capaz de realizar varias imparticiones.
Vamos a mostrar el contenido de ambas clases:
package com.arquitecturajava; import java.util.ArrayList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.NamedAttributeNode; import javax.persistence.NamedEntityGraph; import javax.persistence.OneToMany; @Entity public class Experto { @Id private String nombre; @OneToMany(mappedBy="experto") 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); } public Experto() { super(); } }
package com.arquitecturajava; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedAttributeNode; import javax.persistence.NamedEntityGraph; @Entity public class Imparticion { @Id private int id; private Date fecha; private String titulo; @ManyToOne @JoinColumn(name="nombre_experto") private Experto experto; public Imparticion() { super(); } 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 crear un programa main y solicitamos una lista de los Expertos:
package com.arquitecturajava; 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()); for (Imparticion i : e.getImparticiones()) { System.out.println(i.getTitulo()); } } em.close(); } }
Una vez solicitada la lista accedemos al conjunto de imparticiones que cada Experto tiene. El resultado lo podemos ver por la consola.
Lamentablemente nos encontramos ante una situación en la que se producen n+1 Queries. En este caso tres consultas una para obtener los expertos y otras dos para cada grupo 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 diferentes haremos sobre estas dos tablas?
La respuesta es que muchas. En la mayoría de estas consultas necesitaremos que se realice el mismo join.
Para evitar estas situaciones se han creado los JPA Entity Graph que nos permiten definir un grafo de componentes y atributos que cargar en una query.
Usando JPA Entity Graph
Para ello necesitaremos añadir la anotación de NamedEntityGraph en la clase Experto.
</pre> @Entity @NamedEntityGraph( name = "ExpertoConImparticiones", attributeNodes = { @NamedAttributeNode("imparticiones"), }) public class Experto { <pre>
En segundo lugar debemos modificar el programa principal para que cuando la consulta se realice se apoye en un EntityGraph.
import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.persistence.TypedQuery; public class Principal2 { 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", Experto.class); consulta.setHint("javax.persistence.loadgraph", em.getEntityGraph("ExpertoConImparticiones")); List<Experto> lista = consulta.getResultList(); for (Experto e : lista) { System.out.println(e.getNombre()); for (Imparticion i : e.getImparticiones()) { System.out.println(i.getTitulo()); } } em.close(); } }
Una vez hecho esto la consulta se ejecutará como un join.
Así podremos reutilizar los JPA Entity Graph según nuestras necesidades.
Otros artículos relacionados : ORM.XML , Introducción a JPA , JPA NamedQueries
Estimados, con esta nueva clase Entity Graph, es posible hacer consultas de 3 niveles(Table maestra, tabla detalle y tabla subdetalle)? Se lograria evitar el fetch bags exception?
Me parece que no , salvo que montes un entity graph dinamico
Muy bueno el articulo y el ejemplo, gracias!
De nada 🙂
Buenas,
Entonces el Entity Graph te permite darle un comportamiento EAGER a una relación LAZY? Porque si se declara como EAGER fetch la relación siempre se realiza el JOIN. La idea con esto es decidir cuándo realizar un EAGER?
Si te permite cargar los datos previamente ,especificando el tipo de grafo
Muchas gracias por tus artículos.
Con EclipseLink en vez de Hibernate no logro que funcione. He leído que hay un bug en algunas versiones pero que está corregido la última versión y sin embargo no logro hacer que me funcionen los entity graph. Me salen las mismas consultas tanto si lo pongo como si no (con el fetch join si me funciona). ¿Sabes si hay alguna otra cuestión que tenga que tener en cuenta?
Muchas gracias. Saludos.
no sabría decirte 🙁 tienes JPA 2.1 verdad?
Si… Voy a intentarlo usando Hibernate a ver…
[…] que comienza a trabajar con JPA. ¿Cómo funciona un JPA Proxy? .Vamos a apoyarnos en el ejemplo del artículo anterior y hacer una pequeña modificación . Recordemos que partimos de dos clases Experto e Imparticion […]
Gracias por el artículo.
Una duda: en la primera parte donde el for intenta iterar la lista del lado Many, no deberia estar vacia? Si las relaciones OneToMany son lazy como se explica antes.
Saludos
Hola Franco, que me corrijan si me equivoco, pero que sea LAZY quiere decir que, de primeras, no rellenará las listas que tenga mapeadas con oneToMany en este caso. Es decir, que el objeto Experto que obtienes de la query, para que sea más ligero y tener a LAZY su relación con Imparticion, no tiene rellena su lista de imparticiones asociada hasta que explicitamente la pidas, como es el caso del for. Como en el for, sobre uno de los objetos experto, solicitas con el getImparticiones su lista, es cuando ejecuta la query bajo demanda. Si Experto hubiese sido desconectada… Read more »
Si una relación Lazy se carga cuando solicitas acceso a esa colección de forma expresa
Lo mejor de este blog son los ejemplo tan ilustrados que se muestran. No solamente se centra en explicar sino que expone la motivación que ha llevado a crear tal función, en este caso las entityGraph.
Gracias Cecilio!
De nada , me alegro que sea útil 🙂
Muchas gracias creo que esto me podria ayudar
Me alegro que te sea útil 🙂
Al final no pude integrarlo ya que tenemos JPA 2.0 , creo que es importante recalcar que solo esta disponible apartir de JPA 2.1 :'(
Gracias por el aporte , es cierto q los grafos son relativamente nuevos 🙂