Os 2 melhores opções para corrigir o Hibernate MultipleBagFetchException

Você provavelmente aprendeu que você deve usar FetchType.Preguiçoso para todas as suas associações. Ele garante que o Hibernate inicializa uma associação quando você o usa e não gasta nenhum tempo a obter dados que você não precisa. infelizmente, isto introduz uma nova questão. Agora você precisa usar uma cláusula de JOIN FETCH ou um EntityGraph para obter a associação se você precisar dela. Caso contrário, irá experimentar o n+1 select issue, que causa problemas graves de desempenho ou uma ligeira excepção de inicialização. Se você fizer isso para várias associações, Hibernate pode lançar um MultipleBagFetchException.

neste artigo, vou explicar quando o Hibernate lançar esta excepção e mostrar-lhe as suas 2 melhores opções para corrigi-la. Um deles é um grande ajuste para associações com uma pequena cardinalidade e o outro para associações que contêm muitos elementos. Então, vamos dar uma olhada em ambos, e você escolhe o que se encaixa na sua aplicação.

Cause of the MultipleBagFetchException

como eu expliquei em um artigo anterior sobre o tipo de dados mais eficiente para uma associação de muitos, a nomeação interna do Hibernate dos tipos de coleta é bastante confusa. Hibernar chama-lhe Saco, se os elementos do seu java.util.A lista não é ordenada. Se forem encomendados, chama-se Lista.

assim, dependendo do seu mapeamento, um java.util.A lista pode ser tratada como um saco ou uma lista. Mas não te preocupes, na vida real, isto não é tão confuso como pode parecer. Definir a ordem de uma associação requer uma anotação adicional e é quase sempre uma sobrecarga. É por isso que você deve evitá-lo e por que pelo menos 90% dos mapeamentos da associação que usam um java.util.List e que eu vi em projetos reais não são encomendados. O Hibernate trata-os como um saco.

Aqui está um modelo de domínio simples no qual Hibernate trata as revisões e os autores de um livro como sacos.

@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }

Se você tentar obter vários desses sacos em uma consulta JPQL, você cria um produto 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();

isto pode criar problemas de desempenho. Hibernate também luta para diferenciar entre informações que supostamente devem ser duplicadas e informações que foram duplicadas por causa do produto cartesiano. Por causa disso, Hibernate lança um Multiplebagfetchextion.

java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: 

Fixing the MultipleBagFetchException

você pode encontrar muitas perguntas sobre esta exceção e várias soluções para evitá-la. Mas muitos deles têm efeitos colaterais inesperados. As únicas 2 correções entre as quais você deve escolher são as que eu descreverei nas seções seguintes. Qual deles é o melhor para você depende do tamanho do produto cartesiano de que as consultas podem criar:

  1. Se todas as suas associações conter apenas um pequeno número de elementos, o criado produto cartesiano será relativamente pequeno. Nestas situações, você pode mudar os tipos de atributos que mapeiam suas associações para um java.util.Definir. O Hibernate pode então obter várias associações em uma consulta.
  2. Se pelo menos uma de suas associações contém um monte de elementos, o seu produto cartesiano vai se tornar muito grande para obtê-lo eficientemente em 1 consulta. Você deve, então, usar várias consultas que obter diferentes partes do resultado requerido.

Como sempre, otimizar o desempenho da sua aplicação requer que você escolha entre diferentes compromissos, e não há uma abordagem de tamanho único para todos. O desempenho de cada opção depende do tamanho do produto cartesiano e do número de consultas que você está executando. Para um produto cartesiano relativamente pequeno, obter todas as informações com 1 consulta lhe fornece o melhor desempenho. Se o produto cartesiano atinge um determinado tamanho, você deve dividi-lo em várias consultas.

é por isso que vou mostrar-lhe ambas as opções para que você possa escolher a que se encaixa na sua aplicação.

Opção 1: Usar um conjunto em vez de uma lista

a abordagem mais fácil para corrigir o MultipleBagFetchException é alterar o tipo de atributos que mapeiam as suas associações para muitas num java.util.Definir. Esta é apenas uma pequena mudança no seu mapeamento, e você não precisa mudar o seu código de Negócio.

@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }

como explicado anteriormente, se você agora realizar a mesma consulta que eu mostrei antes para obter o livro com todos os seus autores e opiniões, seu conjunto de resultados conterá um produto cartesiano. O tamanho desse produto depende do número de livros que você selecionar e do número de Autores Associados e comentários.

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();

Aqui você pode ver a consulta SQL gerada. Para obter todas as associações solicitadas, o Hibernate tem de seleccionar todas as colunas mapeadas por estas entidades. Em combinação com o produto cartesiano criado pelas três juntas internas, isso pode se tornar um problema de desempenho.

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

Sempre que você escrever uma consulta, você também precisa ter em mente que o Hibernate não esconde que o conjunto de resultados contém um produto. Esta consulta retorna cada livro várias vezes. O número de referências ao mesmo objeto de livro é calculado pelo número de autores multiplicado pelo número de revisões. Pode evitar isso adicionando a palavra-chave distinta à sua cláusula de selecção e definindo a hibernação da dica da consulta.consulta.passdistinct through to false.

considerações de desempenho

neste exemplo, minha consulta seleciona apenas 1 livro, e a maioria dos livros foram escritos por 1-3 autores. Assim, mesmo que o banco de dados contenha várias resenhas para este livro, O produto cartesiano ainda será relativamente pequeno. com base nestes pressupostos, pode ser mais rápido aceitar a ineficiência do produto cartesiano para reduzir o número de consultas. Isto pode mudar se o seu produto cartesiano se torna maior porque você seleciona um grande número de livros ou se o seu livro médio foi escrito por algumas dezenas de autores.

Opção 2: dividi – lo em várias consultas

obter produtos cartesianos enormes em uma consulta é ineficiente. Ele requer um monte de recursos em sua base de dados e coloca carga desnecessária em sua rede. Hibernar e seu driver JDBC também precisam gastar mais recursos para lidar com o resultado da consulta.

Você pode evitar isso, realizando várias consultas que obtêm diferentes partes do gráfico requerido De entidades. No exemplo deste post, eu iria buscar os livros com todos os seus autores em 1 consulta e os livros com todas as suas opiniões em uma segunda consulta. Se o seu gráfico de entidades requeridas for mais complexo, poderá precisar de usar mais consultas ou obter mais Associações com cada uma delas.

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 expliquei no post da semana passada, Hibernate garante que dentro de cada sessão, há apenas 1 objeto entidade que representa um registro específico na base de dados. Você pode usar isso para resolver referências chave estrangeiras de forma eficiente ou para deixar hibernar mesclar os resultados de várias consultas.

Se você der uma olhada na saída de log seguinte, você pode ver que as listas retornadas por ambas as consultas contêm exatamente o mesmo objeto. Em ambos os casos, os objetos do livro têm a referência @1f.

Quando hibernar processou o resultado da segunda consulta, ele verificou para cada registro se a cache de 1º nível já continha um objeto para essa entidade do livro. Ele então reutilizou esse objeto e adicionou a revisão retornada para a associação mapeada.

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

considerações de desempenho

Se usar várias consultas para obter o gráfico necessário de entidades, evita a criação de um enorme produto cartesiano. Isso reduz a carga em todos os sistemas envolvidos e torna mais fácil garantir um bom desempenho para todas as consultas.

mas isso não significa necessariamente que esta abordagem é mais rápida do que a opção 1. Agora, faz mais perguntas do que antes. Cada um deles requer um roundtrip de banco de dados e cria algumas despesas de gestão no banco de dados, por exemplo, para criar um plano de execução. Devido a isso, esta opção é apenas mais rápida do que a opção 1, Se o tamanho do produto cartesiano cria uma sobrecarga maior do que a execução de múltiplas consultas.

Conclusão

Como vocês viram neste artigo, você pode resolver o MultipleBagFetchException de 2 formas:

  • Você pode alterar o tipo de dados do atributo que mapeia as associações e obter todas as informações em 1 de consulta. O resultado dessa consulta é um produto cartesiano. Desde que este produto não fique muito grande, esta abordagem é simples e eficiente.
  • pode usar várias consultas para obter o gráfico necessário de entidades. Isso evita um enorme produto cartesiano e é a melhor abordagem se você precisa obter uma enorme quantidade de dados.

Deixe uma resposta

O seu endereço de email não será publicado.