Solución:
Considerando que tenemos las siguientes entidades:
Y quieres ir a buscar a un padre Post
entidades junto con todos los asociados comments
y tags
colecciones.
Si está usando más de una JOIN FETCH
directivas:
List<Post> posts = entityManager.createQuery("""
select p
from Post p
left join fetch p.comments
left join fetch p.tags
where p.id between :minId and :maxId
""", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();
Hibernate arrojará el MultipleBagFetchException
:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags [
com.vladmihalcea.book.hpjp.hibernate.fetching.Post.comments,
com.vladmihalcea.book.hpjp.hibernate.fetching.Post.tags
]
La razón por la que Hibernate lanza esta excepción es que no permite buscar más de una bolsa porque eso generaría un producto cartesiano.
La peor “solución” que otros podrían intentar venderle
Ahora, encontrará muchas respuestas, publicaciones de blog, videos u otros recursos que le indican que use un Set
en lugar de un List
para tus colecciones.
Ese es un consejo terrible. ¡No hagas eso!
Utilizando Sets
en lugar de Lists
hará el MultipleBagFetchException
desaparecerá, pero el Producto cartesiano seguirá estando allí, lo que en realidad es aún peor, ya que descubrirá el problema de rendimiento mucho después de haber aplicado esta “solución”.
La solucion adecuada
Puedes hacer el siguiente truco:
List<Post> posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.comments
where p.id between :minId and :maxId
""", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
posts = entityManager.createQuery("""
select distinct p
from Post p
left join fetch p.tags t
where p in :posts
""", Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
En la primera consulta JPQL,
distinct
NO va a la sentencia SQL. Es por eso que configuramos elPASS_DISTINCT_THROUGH
Sugerencia de consulta JPA parafalse
.DISTINCT tiene dos significados en JPQL, y aquí, lo necesitamos para deduplicar las referencias de objetos Java devueltas por
getResultList
en el lado de Java, no en el lado de SQL.
Siempre que obtenga como máximo una colección usando JOIN FETCH
, estarás bien.
Al utilizar varias consultas, evitará el Producto cartesiano desde cualquier otra colección, pero la primera se obtiene mediante una consulta secundaria.
Evite siempre el FetchType.EAGER
estrategia
Si está usando el FetchType.EAGER
estrategia en el momento del mapeo para @OneToMany
o @ManyToMany
asociaciones, entonces fácilmente podría terminar con una MultipleBagFetchException
.
Es mejor cambiar de FetchType.EAGER
para Fetchype.LAZY
ya que la búsqueda ansiosa es una idea terrible que puede conducir a problemas críticos de rendimiento de la aplicación.
Conclusión
Evitar FetchType.EAGER
y no cambies de List
para Set
solo porque hacerlo hará que Hibernate oculte el MultipleBagFetchException
debajo de la alfombra. Obtenga solo una colección a la vez, y estará bien.
Siempre que lo haga con la misma cantidad de consultas que tiene colecciones para inicializar, está bien. Simplemente no inicialice las colecciones en un bucle, ya que eso activará N+1
problemas de consulta, que también son perjudiciales para el rendimiento.
A continuación, se muestra un ejemplo práctico de combinación compleja y construcción múltiple:
String query_findByProductDepartmentHospital = "select location from ProductInstallLocation location "
+ " join location.product prod " + " join location.department dep "
+ " join location.department.hospital hos " + " where prod.name = :product "
+ " and dep.name.name = :department " + " and hos.name = :hospital ";
@Query(query_findByProductDepartmentHospital)
ProductInstallLocation findByProductDepartmentHospital(@Param("product") String productName,@Param("department") String departName, @Param("hospital") String hospitalName);