Probablemente aprendiste que deberías usar FetchType.PEREZOSO para todas sus asociaciones. Garantiza que Hibernate inicializa una asociación cuando la usas y no pasa tiempo obteniendo datos que no necesitas.
Desafortunadamente, esto introduce un nuevo problema. Ahora necesita usar una cláusula de búsqueda de UNIÓN o un grafo de entidad para obtener la asociación si la necesita. De lo contrario, experimentará el problema de selección n+1, que causa problemas de rendimiento graves o una excepción de inicialización de Lazy. Si lo hace para varias asociaciones, Hibernar puede generar una excepción de MultipleBagFetchException.
En este artículo, explicaré cuándo Hibernate lanza esta excepción y le mostraré sus 2 mejores opciones para solucionarla. Uno de ellos es ideal para asociaciones con una pequeña cardinalidad y el otro para asociaciones que contienen muchos elementos. Por lo tanto, echemos un vistazo a ambos y elija el que se ajuste a su aplicación.
Causa de la excepción Multiplebagfetch
Como expliqué en un artículo anterior sobre el tipo de datos más eficiente para una asociación a muchos, la nomenclatura interna de Hibernate de los tipos de colección es bastante confusa. Hibernate lo llama una Bolsa, si los elementos en su java.útil.La lista no está ordenada. Si se ordenan, se llama Lista.
Por lo tanto, dependiendo de su asignación, un java.útil.La lista se puede tratar como una Bolsa o una Lista. Pero no te preocupes, en la vida real, esto no es tan confuso como podría parecer. Definir el orden de una asociación requiere una anotación adicional y casi siempre es una sobrecarga. Es por eso que debe evitarlo y por eso al menos el 90% de las asignaciones de asociación que usan java.útil.Lista y que he visto en proyectos reales están desordenadas. Hibernate los trata como a una bolsa.
Aquí hay un modelo de dominio simple en el que Hibernate trata a las Reseñas y a los Autores de un Libro como Bolsas.
@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }
Si intenta obtener varias de estas bolsas en una consulta JPQL, crea un producto cartesiano.
TypedQuery<Book> q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b "+ "JOIN FETCH b.authors a "+ "JOIN FETCH b.reviews r "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);List<Book> b = q.getResultList();
Esto puede crear problemas de rendimiento. Hibernar también lucha para diferenciar entre la información que se supone que debe duplicarse y la información que se duplicó debido al producto cartesiano. Debido a eso, Hibernate lanza una excepción de bolsa múltiple.
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
Arreglando la excepción MultipleBagFetchException
Puede encontrar muchas preguntas sobre esta excepción y varias soluciones para evitarla. Pero muchos de ellos tienen efectos secundarios inesperados. Las únicas 2 correcciones entre las que debe elegir son las que describiré en las siguientes secciones. Cuál de ellos es el mejor para ti depende del tamaño del producto cartesiano que puedan crear tus consultas:
- Si todas tus asociaciones solo contienen un pequeño número de elementos, el producto cartesiano creado será relativamente pequeño. En estas situaciones, puede cambiar los tipos de atributos que asignan sus asociaciones a un java.útil.Establecer. Hibernate puede obtener varias asociaciones en 1 consulta.
- Si al menos una de sus asociaciones contiene muchos elementos, su producto cartesiano se volverá demasiado grande para obtenerlo de manera eficiente en 1 consulta. A continuación, debe usar varias consultas que obtengan diferentes partes del resultado requerido.
Como siempre, optimizar el rendimiento de su aplicación requiere que elija entre diferentes ventajas y desventajas, y no hay un enfoque único para todos. El rendimiento de cada opción depende del tamaño del producto cartesiano y del número de consultas que esté ejecutando. Para un producto cartesiano relativamente pequeño, obtener toda la información con 1 consulta le proporciona el mejor rendimiento. Si el producto cartesiano alcanza un cierto tamaño, es mejor dividirlo en varias consultas.
Es por eso que le mostraré ambas opciones para que pueda elegir la que se ajuste a su aplicación.
Opción 1: Utilice un Conjunto en lugar de una Lista
El enfoque más sencillo para corregir la excepción MultipleBagFetchException es cambiar el tipo de atributos que asignan sus asociaciones a-muchos a un java.útil.Establecer. Esto es solo un pequeño cambio en su mapeo, y no necesita cambiar su código de negocio.
@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }
Como se explicó anteriormente, si ahora realiza la misma consulta que le mostré antes para obtener el Libro con todos sus Autores y Reseñas, su conjunto de resultados contendrá un producto cartesiano. El tamaño de ese producto depende del número de Libros que seleccione y del número de Autores y Reseñas asociados.
TypedQuery<Book> q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b "+ "JOIN FETCH b.authors a "+ "JOIN FETCH b.reviews r "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);List<Book> b = q.getResultList();
Aquí puede ver la consulta SQL generada. Para obtener todas las asociaciones solicitadas, Hibernate debe seleccionar todas las columnas asignadas por estas entidades. En combinación con el producto cartesiano creado por las 3 uniones INTERNAS, esto puede convertirse en un problema de rendimiento.
19:46:20,785 DEBUG - select book0_.id as id1_1_0_, author2_.id as id1_0_1_, reviews3_.id as id1_4_2_, book0_.publisherid as publishe5_1_0_, book0_.publishingDate as publishi2_1_0_, book0_.title as title3_1_0_, book0_.version as version4_1_0_, author2_.firstName as firstNam2_0_1_, author2_.lastName as lastName3_0_1_, author2_.version as version4_0_1_, authors1_.bookId as bookId1_2_0__, authors1_.authorId as authorId2_2_0__, reviews3_.bookid as bookid3_4_2_, reviews3_.comment as comment2_4_2_, reviews3_.bookid as bookid3_4_1__, reviews3_.id as id1_4_1__ from Book book0_ inner join BookAuthor authors1_ on book0_.id=authors1_.bookId inner join Author author2_ on authors1_.authorId=author2_.id inner join Review reviews3_ on book0_.id=reviews3_.bookid where book0_.id=1
Siempre que escriba una consulta de este tipo, también debe tener en cuenta que Hibernar no oculta que el conjunto de resultados contiene un producto. Esta consulta devuelve cada libro varias veces. El número de referencias al mismo objeto de libro se calcula por el número de autores multiplicado por el número de Reseñas. Puede evitar esto agregando la palabra clave DISTINCT a su cláusula select y configurando la hibernación de sugerencia de consulta.consulta.Pasaddistinct a través de false.
Consideraciones de rendimiento
En este ejemplo, mi consulta solo selecciona 1 Libro, y la mayoría de los libros han sido escritos por 1 a 3 Autores. Por lo tanto, incluso si la base de datos contiene varias reseñas de este Libro, el producto cartesiano seguirá siendo relativamente pequeño.
En base a estas suposiciones, podría ser más rápido aceptar la ineficiencia del producto cartesiano para reducir el número de consultas. Esto podría cambiar si su producto cartesiano se hace más grande porque selecciona una gran cantidad de Libros o si su libro promedio ha sido escrito por unas pocas docenas de autores.
Opción 2: Dividirlo en múltiples consultas
Obtener productos cartesianos enormes en 1 consulta es ineficiente. Requiere una gran cantidad de recursos en su base de datos y genera una carga innecesaria en su red. Hibernate y su controlador JDBC también necesitan gastar más recursos para manejar el resultado de la consulta.
Puede evitar esto realizando múltiples consultas que obtienen diferentes partes del gráfico de entidades requerido. En el ejemplo de este post, buscaría los Libros con todos sus Autores en 1 consulta y los Libros con todas sus Reseñas en una 2a consulta. Si su gráfico de entidades requeridas es más complejo, es posible que necesite usar más consultas o buscar más asociaciones con cada una de ellas.
TypedQuery<Book> q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b JOIN FETCH b.authors a "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);List<Book> books = q.getResultList();log.info(books.get(0));q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b "+ "JOIN FETCH b.reviews r "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);books = q.getResultList();log.info(books.get(0));log.info("Authors: "+books.get(0).getAuthors().size());log.info("Reviews: "+books.get(0).getReviews().size());
Como expliqué en el post de la semana pasada, Hibernate garantiza que dentro de cada Sesión, solo haya 1 objeto de entidad que represente un registro específico en la base de datos. Puede usarlo para resolver referencias de claves foráneas de manera eficiente o para permitir que Hibernar combine los resultados de múltiples consultas.
Si echa un vistazo a la siguiente salida de registro, puede ver que las Listas devueltas por ambas consultas contienen exactamente el mismo objeto. En ambos casos, los objetos de libro tienen la referencia @1f.
Cuando Hibernate procesó el resultado de la 2a consulta, comprobó para cada registro si la caché de 1er nivel ya contenía un objeto para esa entidad de libro. Luego reutilizó ese objeto y agregó la revisión devuelta a la asociación asignada.
19:52:10,600 DEBUG - select book0_.id as id1_1_0_, author2_.id as id1_0_1_, book0_.publisherid as publishe5_1_0_, book0_.publishingDate as publishi2_1_0_, book0_.title as title3_1_0_, book0_.version as version4_1_0_, author2_.firstName as firstNam2_0_1_, author2_.lastName as lastName3_0_1_, author2_.version as version4_0_1_, authors1_.bookId as bookId1_2_0__, authors1_.authorId as authorId2_2_0__ from Book book0_ inner join BookAuthor authors1_ on book0_.id=authors1_.bookId inner join Author author2_ on authors1_.authorId=author2_.id where book0_.id=119:52:10,633 INFO - 19:52:10,645 DEBUG - select book0_.id as id1_1_0_, reviews1_.id as id1_4_1_, book0_.publisherid as publishe5_1_0_, book0_.publishingDate as publishi2_1_0_, book0_.title as title3_1_0_, book0_.version as version4_1_0_, reviews1_.bookid as bookid3_4_1_, reviews1_.comment as comment2_4_1_, reviews1_.bookid as bookid3_4_0__, reviews1_.id as id1_4_0__ from Book book0_ inner join Review reviews1_ on book0_.id=reviews1_.bookid where book0_.id=119:52:10,648 INFO - 19:52:10,648 INFO - Authors: 219:52:10,648 INFO - Reviews: 2
Consideraciones de rendimiento
Si utiliza varias consultas para obtener el gráfico de entidades requerido, evitará la creación de un producto cartesiano enorme. Esto reduce la carga en todos los sistemas involucrados y facilita garantizar un buen rendimiento para todas las consultas.
Pero esto no significa necesariamente que este enfoque sea más rápido que la opción 1. Ahora realiza más consultas que antes. Cada uno de ellos requiere un recorrido de ida y vuelta de la base de datos y crea cierta sobrecarga de administración en la base de datos, por ejemplo, para crear un plan de ejecución. Debido a esto, esta opción solo es más rápida que la opción 1, si el tamaño del producto cartesiano crea una sobrecarga mayor que la ejecución de múltiples consultas.
Conclusión
Como ha visto en este artículo, puede resolver la excepción MultipleBagFetchException de Hibernate de 2 maneras:
- Puede cambiar el tipo de datos del atributo que asigna las asociaciones y recuperar toda la información en 1 consulta. El resultado de esa consulta es un producto cartesiano. Mientras este producto no sea demasiado grande, este enfoque es simple y eficiente.
- Puede usar varias consultas para obtener el gráfico de entidades requerido. Esto evita un producto cartesiano enorme y es el mejor enfoque si necesita obtener una gran cantidad de datos.