¿Cuál es la solución para el problema de N + 1 en JPA e Hibernate?

Entiendo que el problema N + 1 es donde se ejecuta una consulta para obtener N registros y N consultas para recuperar algunos registros relacionales.

Pero, ¿cómo se puede evitar en Hibernate?

Supongamos que tenemos un fabricante de clase con una relación de muchos a uno con Contact.

Resolvemos este problema asegurándonos de que la consulta inicial obtenga todos los datos necesarios para cargar los objetos que necesitamos en su estado inicializado apropiadamente. Una forma de hacerlo es usar una combinación de búsqueda de HQL. Usamos el HQL

"from Manufacturer manufacturer join fetch manufacturer.contact contact" 

con la statement de búsqueda. Esto resulta en una unión interna:

 select MANUFACTURER.id from manufacturer and contact ... from MANUFACTURER inner join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id 

Usando una consulta Criteria podemos obtener el mismo resultado de

 Criteria criteria = session.createCriteria(Manufacturer.class); criteria.setFetchMode("contact", FetchMode.EAGER); 

que crea el SQL:

 select MANUFACTURER.id from MANUFACTURER left outer join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id where 1=1 

en ambos casos, nuestra consulta devuelve una lista de objetos del fabricante con el contacto inicializado. Solo se debe ejecutar una consulta para devolver toda la información de contacto y del fabricante requerida

para más información aquí hay un enlace al problema y la solución

La solución nativa para 1 + N en Hibernate, se llama:

20.1.5. Utilizando la recuperación por lotes

Al utilizar la recuperación por lotes, Hibernate puede cargar varios proxies sin inicializar si se accede a un proxy. La búsqueda por lotes es una optimización de la estrategia de búsqueda de selección perezosa. Hay dos formas en que podemos configurar la recuperación por lotes: en el nivel de clase 1) y en el nivel de colección 2).

Verifique estas preguntas y respuestas:

  • @BatchSize pero muchos viajes de ida y vuelta en el caso @ManyToOne
  • Evitar n + 1 búsqueda con ganas de asociación de elementos de colección de elementos secundarios

Con las anotaciones podemos hacerlo así:

Un nivel de class :

 @Entity @BatchSize(size=25) @Table(... public class MyEntity implements java.io.Serializable {... 

Un nivel de collection :

 @OneToMany(fetch = FetchType.LAZY...) @BatchSize(size=25) public Set getMyColl() 

La carga diferida y la recuperación de lotes juntas representan la optimización, que:

  • no requiere ninguna búsqueda explícita en nuestras consultas
  • se aplicará a cualquier cantidad de referencias que se toquen (perezosamente) después de que se cargue la entidad raíz (mientras que los efectos de extracción explícitos solo se nombran en la consulta)
  • resolverá el problema 1 + N con colecciones (porque solo se pudo obtener una colección con la consulta raíz) sin necesidad de un procesamiento posterior Para obtener valores raíz DISTINCT (marque: Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct )

El problema

El problema de la consulta N + 1 ocurre cuando se olvida de buscar una asociación y luego necesita acceder a ella.

Por ejemplo, supongamos que tenemos la siguiente consulta JPA:

 List comments = entityManager.createQuery( "select pc " + "from PostComment pc " + "where pc.review = :review", PostComment.class) .setParameter("review", review) .getResultList(); 

Ahora, si iteramos las entidades PostComment y atravesamos la asociación de post :

 for(PostComment comment : comments) { LOGGER.info("The post title is '{}'", comment.getPost().getTitle()); } 

Hibernate generará las siguientes declaraciones SQL:

 SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_ FROM post_comment pc WHERE pc.review = 'Excellent!' INFO - Loaded 3 comments SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_ FROM post pc WHERE pc.id = 1 INFO - The post title is 'Post nr. 1' SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_ FROM post pc WHERE pc.id = 2 INFO - The post title is 'Post nr. 2' SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_ FROM post pc WHERE pc.id = 3 INFO - The post title is 'Post nr. 3' 

Así es como se genera el problema de consulta N + 1.

Debido a que la asociación de post no se inicializa al recuperar las entidades PostComment , Hibernate debe buscar la entidad Post con una consulta secundaria, y para N entidades PostComment , se ejecutarán N más consultas (de ahí el problema de consulta N + 1).

La solución

Lo primero que debe hacer para solucionar este problema es agregar un registro y supervisión de SQL adecuados . Sin iniciar sesión, no notará el problema de consulta N + 1 mientras desarrolla una determinada función.

En segundo lugar, para solucionarlo, solo puede UNIRSE A FETCH la relación que causa este problema:

 List comments = entityManager.createQuery( "select pc " + "from PostComment pc " + "join fetch pc.post p " + "where pc.review = :review", PostComment.class) .setParameter("review", review) .getResultList(); 

Si necesita recuperar varias asociaciones secundarias, es mejor buscar una colección en la consulta inicial y la segunda con una consulta SQL secundaria.

Este problema es mejor atrapado por las pruebas de integración. Puede usar una statement JUnit automática para validar el recuento esperado de sentencias SQL generadas . El proyecto db-unit ya proporciona esta funcionalidad, y es de código abierto y la dependencia está disponible en Maven Central.