El problema de las n+1 Queries es uno de los problemas más habituales cuando trabajamos con Domain Driven Design y frameworks de Persistencia . ¿Que es una n+1 Query y como nos podemos encontrar en una situación límite con ellas?. Vamos a explicarlo paso a paso . En primer lugar el problema aparece normalmente cuando trabajamos con algún framework de Persistencia . Puede ser Hibernate o puede ser cualquier otro como EntityFramework o Larabel dependiendo de la plataforma y hace referencia a estos frameworks abordan los problemas relacionados con consultas que incluyen relaciones . Vamos a construir un ejemplo sencillo .[ihc-hide-content ihc_mb_type=”show” ihc_mb_who=”4″ ihc_mb_template=”1″ ]
Para ello partiremos de dos clases . La clase Libro y la clase Capítulo. Se trata de dos clases que van a estar relacionadas con JPA.
package com.arquitecturajava.data1; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.Table; @Entity @Table(name="Libros") public class Libro { @Id private String isbn; private String titulo; private String autor; private double precio; private Date fecha; @OneToMany(mappedBy = "libro") private List<Capitulo> capitulos= new ArrayList<Capitulo>(); public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public String getTitulo() { return titulo; } public void setTitulo(String titulo) { this.titulo = titulo; } public String getAutor() { return autor; } public void setAutor(String autor) { this.autor = autor; } public double getPrecio() { return precio; } public void setPrecio(double precio) { this.precio = precio; } public Date getFecha() { return fecha; } public void setFecha(Date fecha) { this.fecha = fecha; } public Libro() { super(); } public Libro(String isbn) { super(); this.isbn = isbn; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((isbn == null) ? 0 : isbn.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Libro other = (Libro) obj; if (isbn == null) { if (other.isbn != null) return false; } else if (!isbn.equals(other.isbn)) return false; return true; } public List<Capitulo> getCapitulos() { return capitulos; } public void setCapitulos(List<Capitulo> capitulos) { this.capitulos = capitulos; } public Libro(String isbn, String titulo, String autor, double precio, Date fecha) { super(); this.isbn = isbn; this.titulo = titulo; this.autor = autor; this.precio = precio; this.fecha = fecha; } }
package com.arquitecturajava.data1; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; @Entity @Table(name="capitulos") public class Capitulo { @Id private String titulo; private int paginas; @ManyToOne @JoinColumn(name="libros_isbn") private Libro libro; public Libro getLibro() { return libro; } public void setLibro(Libro libro) { this.libro = libro; } public String getTitulo() { return titulo; } public void setTitulo(String titulo) { this.titulo = titulo; } public int getPaginas() { return paginas; } public void setPaginas(int paginas) { this.paginas = paginas; } public Capitulo(String titulo, int paginas) { super(); this.titulo = titulo; this.paginas = paginas; } public Capitulo() { super(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((titulo == null) ? 0 : titulo.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Capitulo other = (Capitulo) obj; if (titulo == null) { if (other.titulo != null) return false; } else if (!titulo.equals(other.titulo)) return false; return true; } }
Como podemos observar se trata de una relación 1 a n . Un libro contiene n capítulos :
Hasta aquí todo muy muy correcto. Podemos construir una pequeña prueba unitaria que nos solicite los libros y nos imprima los isbn y títulos por la consola. En este caso lo voy a realizar con Spring Boot por comodidad ya que me permite de una forma relativamente sencilla cargar datos :
package com.arquitecturajava.data1; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; @SpringBootTest @Sql({ "/schema.sql", "/data.sql" }) class DataPruebas { @PersistenceContext private EntityManager em; @Test @Transactional public void testLibros() { List<Libro> libros= em.createQuery("select l from Libro l", Libro.class).getResultList(); for (Libro l : libros) { System.out.println(l.getIsbn()); } } }
Realmente no estoy ejecutando ninguna prueba unitaria en sí sino simplemente solicitando la información de los libros una vez realizada la consulta . Esta información la podremos ver pasar por la consola
Junto con esa información tenemos también acceso a las consultas que Hibernate generó para obtener los datos . En este caso todo es muy normal ya que realiza un select clásico solicittando la información requerida:
n+1 Queries
Todo ha funcionado correctamente . ¿Ahora bien que es lo que sucede si yo intento recorrer los objetos de otra forma y deseo acceder a información de los capítulos? .Vamos a verlo en acción , para ello la modificación de nuestro código es muy puntual:
package com.arquitecturajava.data1; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.transaction.Transactional; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; @SpringBootTest @Sql({ "/schema.sql", "/data.sql" }) class DataPruebas { @PersistenceContext private EntityManager em; @Test @Transactional public void test() { List<Libro> libros= em.createQuery("select l from Libro l", Libro.class).getResultList(); for (Libro l : libros) { System.out.print(l.getIsbn()); System.out.println(l.getTitulo()); for(Capitulo c: l.getCapitulos()) { System.out.println(c.getTitulo()); System.out.println(c.getPaginas()); } } } }
El código se ejecuta y nos muestra la información de cada uno de los libros con sus capítulos por la consola :
Normalmente . la gente no le da muchas más vueltas y sigue trabajando . Lamentablemente dentro de este código hay un problema importante de n+1 Queries ya que si nos fijamos Hibernate ha ejecutado las siguientes Queries simplemente para traer la información del primer Libro y sus Capítulos.
select libro0_.isbn as isbn1_1_, libro0_.autor as autor2_1_, libro0_.fecha as fecha3_1_, libro0_.precio as precio4_1_, libro0_.titulo as titulo5_1_ from Libros libro0_
select capitulos0_.libros_isbn as libros_i3_0_0_, capitulos0_.titulo as titulo1_0_0_, capitulos0_.titulo as titulo1_0_1_, capitulos0_.libros_isbn as libros_i3_0_1_, capitulos0_.paginas as paginas2_0_1_ from capitulos capitulos0_ where capitulos0_.libros_isbn=? 2021-04-04 18:51:07.679 TRACE 33449 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [1]
Aquí podemos ver como consulta primero por todos los Libros y por cada Libro realiza una consulta adicional para traerse sus capítulos . En principio no parece un problema en sí pero la realidad es que es el comienzo del fin de tu aplicación . Si este tipo de problemas no los atajas en un primer momento y te das cuenta de que tu framework de persistencia no esta realizando las consultas de forma apropiada para traerte la información . Nos encontraremos en un futuro con consultas que se traen de golpe un listado de 2000 libros y por cada libro realiza una consulta adicional solicitando sus capítulos. Por lo tanto tendremos 2001 consultas solo para traernos los Libros con sus capítulos. Cuando multipliquemos esto por el número de usuarios concurrentes nos encontraremos con una situación inabordable a nivel de rendimiento y tendremos que vernos obligados a revisar todos y cada una de las consultas. ¿Qué es lo que ha pasado exactamente ? . Lo que ha pasado es que “sabemos poco” sobre cómo utilizar el framework y lo hemos usado de forma incorrecta . Generando por cada listado de libros una consulta adicional por los capítulos de cada Libro.
La primera consulta que busca los libros , es en principio correcta pero las consultas Hijas que realiza el framework para poder acceder al contenido de los capítulos es el principio del fin.
Solucionando el problema
¿Como podemos solventar el problema de las n+1 Queries? . Depende mucho del framework y depende mucho de la situación de cada uno . Pero lo importante es revisar la documentación del framework y orientarle a que no realize este tipo de consultas sino que haga una única consulta de golpe. En este caso nos valdría con una consulta de Fetch Join entre Libros y Capítulos:
List<Libro> libros= em.createQuery("select l from Libro l join Fetch l.capitulos", Libro.class).getResultList();
De esta forma únicamente generaremos una consulta :
El resultado en la consola de la consulta de fetch es :
select libro0_.isbn as isbn1_1_0_, capitulos1_.titulo as titulo1_0_1_, libro0_.autor as autor2_1_0_, libro0_.fecha as fecha3_1_0_, libro0_.precio as precio4_1_0_, libro0_.titulo as titulo5_1_0_, capitulos1_.libros_isbn as libros_i3_0_1_, capitulos1_.paginas as paginas2_0_1_, capitulos1_.libros_isbn as libros_i3_0_0__, capitulos1_.titulo as titulo1_0_0__ from Libros libro0_ inner join capitulos capitulos1_ on libro0_.isbn=capitulos1_.libros_isbn
Acabamos de solventar nuestro problema de n+1 queries
Otros artículos relacionados
- JPA Lazy fetching proxies y rendimiento
- JPA Orphan Removal y como usarlo
- JPA Single Table Inheritance
[/ihc-hide-content]