El problema de Hibernate N+1 ocurre cuando usa FetchType.LAZY
para sus asociaciones de entidades. Si realiza una consulta para seleccionar n entidades y si intenta llamar a cualquier método de acceso de la asociación diferida de su entidad, Hibernate realizará n consultas adicionales para cargar objetos obtenidos de forma diferida.
Por ejemplo, tenemos la siguiente entidad Autor con una colección de libros de uno a varios:
public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String fullName; @OneToMany(fetch = FetchType.LAZY) private Set<Book> books; }
Intentemos cargar todos los autores e imprimir el nombre de cada autor con el tamaño de su colección de libros:
entityManager.createQuery("select a from Author a", Author.class) .getResultList() .forEach(a -> System.out.printf("%s had written %d books\n", a.getFullName(), a.getBooks().size()));
La primera consulta que Hibernate generará es para seleccionar todos los autores:
SELECT author0_.id AS id1_0_, author0_.fullName AS fullname2_0_ FROM authors author0_;
Después de eso, cuando llamamos al método size()
en la colección de libros, esta asociación debe inicializarse, por lo que Hibernate realizará una consulta adicional:
SELECT books0_.author_id AS author_i4_1_0_, books0_.id AS id1_1_0_, books0_.id AS id1_1_1_, books0_.author_id AS author_i4_1_1_, books0_.title AS title2_1_1_, books0_.year AS year3_1_1_ FROM books books0_ WHERE books0_.author_id=?;
Esta consulta se llamará n-veces por cada autor cuando imprimamos la cantidad de libros además de la primera consulta. Por lo tanto, el número total de consultas será igual a N+1.
entityManager.createQuery("select a from Author a left join fetch a.books", Author.class);
SELECT author0_.id AS id1_0_0_, books1_.id AS id1_1_1_, author0_.fullName AS fullname2_0_0_, books1_.author_id AS author_i4_1_1_, books1_.title AS title2_1_1_, books1_.year AS year3_1_1_, books1_.author_id AS author_i4_1_0__, books1_.id AS id1_1_0__ FROM authors author0_ LEFT OUTER JOIN books books1_ ON author0_.id=books1_.author_id;
Esta consulta funciona bien, pero tiene un problema: no nos permite usar la paginación porque el límite no se aplicará a los autores. Si especifica query.setMaxResults(n)
, Hibernate obtendrá todas las filas existentes y realizará la paginación en la memoria, lo que aumentará significativamente el consumo de memoria.
@BatchSize
en la asociación perezosa: public class Author { … @OneToMany(fetch = FetchType.LAZY, mappedBy = "author") @BatchSize(size = 10) private Set<Book> books; }
Hibernate creará la primera consulta para recuperar todos los autores:
SELECT author0_.id AS id1_0_, author0_.fullName AS fullname2_0_ FROM authors author0_;
En este caso, podemos realizar fácilmente la paginación de los autores. Luego, cuando llamamos al método size()
en la colección de libros, Hibernate realizará esta consulta:
/* load one-to-many Author.books */ SELECT books0_.author_id AS author_i4_1_1_, books0_.id AS id1_1_1_, books0_.id AS id1_1_0_, books0_.author_id AS author_i4_1_0_, books0_.title AS title2_1_0_, books0_.year AS year3_1_0_ FROM books books0_ WHERE books0_.author_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ? /*batch size*/);
Esta consulta se llamará N/M veces, donde N es la cantidad de autores y M es el tamaño del lote especificado. Totalmente lo llamaremos consultas N/M+1.
Hibernate brinda esta oportunidad configurando @Fetch(FetchMode.SUBSELECT)
en la asociación perezosa:
public class Author { … @OneToMany(fetch = FetchType.LAZY, mappedBy = "author") @Fetch(FetchMode.SUBSELECT) private Set<Book> books; }
La primera consulta cargará todos los autores:
SELECT author0_.id AS id1_0_, author0_.fullName AS fullname2_0_ FROM authors author0_;
La segunda consulta obtendrá libros mediante la subconsulta de autores:
SELECT books0_.author_id AS author_i4_1_1_, books0_.id AS id1_1_1_, books0_.id AS id1_1_0_, books0_.author_id AS author_i4_1_0_, books0_.title AS title2_1_0_, books0_.year AS year3_1_0_ FROM books books0_ WHERE books0_.author_id in (SELECT author0_.id FROM authors author0_);
Si observa detenidamente la condición IN, verá que el código dentro de la subconsulta casi repite la primera consulta. Puede ralentizar el rendimiento si tenemos que ejecutar una consulta muy compleja dos veces. Para acelerar este caso, podemos filtrar y paginar a los autores recuperando sus ID en la primera consulta. Luego podemos pasar estos identificadores directamente a la subconsulta de la segunda consulta:
List<Integer> authorIds = em.createQuery("select a.id from Author a", Integer.class) .setFirstResult(5) .setMaxResults(10) .getResultList(); List<Author> resultList = entityManager.createQuery("select a from Author a" + " left join fetch a.books" + " where a.id in :authorIds", Author.class) .setParameter("authorIds", authorIds) .getResultList();