Em muitos casos há a necessidade do uso de mais de uma lista de objetos dentro de uma classe, como por exemplo, um Produto que possui uma lista de cores e uma lista de movimentação. No Hibernate, trabalhar com esse tipo de objeto pode ser um problema devido a um erro relacionado ao uso de duas ou mais listas (“cannot simultaneously fetch multiple bags”). Não se preocupe, vamos explicar mais à frente do que se trata, com detalhes.

O uso de duas ou mais listas sendo carregadas simultaneamente (Eager Loading) em um mesmo mapeamento não é permitido no Hibernate, até certo ponto. Sendo assim, neste artigo explicaremos o porquê este erro ocorre e como corrigi-lo de forma adequada.

Como ocorre o erro de “Multiple Bags”

Primeiro é importante entender que o termo “bag” é uma associação à java.util.List or java.util.Collection. Vamos abaixo explicar como o erro ocorre através de um exemplo simples, assim fica fácil de assimilar o problema.

Digamos que temos uma classe chamada Parent e 2 subclasses chamadas de Child1 e Child2. Ambas as subclasses Child1 e Child2 tem um bag da classe Parent, assim como mostra o código da Listagem 1.

Listagem 1. Mapeamento da classe Parent


  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.EAGER)
  List<Child1> child1s = new LinkedList<Child1>();
  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.EAGER)
  List<Child2> child2s= new LinkedList<Child2>();  

Perceba que na Listagem 1 ambos os Lists possuem a “FetchType.EAGER” o que explicitamente diz que ambos dever ser carregados juntamente com sua classe Parent. Ao realizar uma busca do nosso objeto Parent no banco de dados, usando o Hibernate, teremos a saída da Listagem 2 do SQL gerado.

Listagem 2. SQL Gerado pelo Hibernate


  select
  parent0_.id as p_id, parent0_.name as p_name, child1s1_.parent_id as c1_p_id,
  child1s1_.id as c1_id, child1s1_.id as c1_id_1, child1s1_.parent_id as
  c1_p_id_1, child1s1_.value as c1_val,child2s2_.parent_id as c2_p_id,
  child2s2_.id as c2_id, child2s2_.id as c2_id_,
  child2s2_.parent_id as c2_p_id_1, child2s2_.value as c2_val
  from
  PARENT parent0_ left outer join
          CHILD1 child1s1_ on parent0_.id=child1s1_.parent_id left outer join
          CHILD2 child2s2_ on parent0_.id=child2s2_.parent_id
  where
  parent0_.id=?  

Nessa listagem foram realizadas pequenas modificações apenas para tornar o código mais legível. Vamos entender como funciona o relacionamento entre Parent, Child1 e Child2.

Na Figura 1 temos a tabela Parent com um registro.

Figura 1. Tabela Parent

Temos agora, na Figura 2, a tabela Child1 com dois registros fazendo referência (chave estrangeira) para a tabela Parent no registro 122.

Figura 2. Tabela Child1

Por fim e não menos importante, temos a Tabela 3 também fazendo referência a tabela Parent, no registro 122.

Figura 3. Tabela Child2

Agora, executamos o SQL mostrado na Listagem 2 e teremos o resultado da Figura 4.

Figura 4. Resultado da Listagem 2

Vamos entender o que ocorreu na figura acima: você deve ter percebido que o Hibernate gerou um erro na tabela mostrada, pois o CHILD2-1 está se repetindo duas vezes, sendo uma na primeira linha e outra na segunda linha. Isso ocorre porque o Hibernate não consegue “ver” que o Child2, na verdade tem apenas uma linha, isso porque a associação da tabela Parent com Child1 e Chidl2 tendem a fazer isso. Veremos nas próximas seções como corrigir esse problema.

Solução do Problema

Dado o problema apresentado neste artigo, vamos agora trabalhar para resolver este problema.

Existem pelo menos três estratégias para a solução deste problema, são elas:

  • Solução 1 – Usar LAZY Loading em todas as listas, assim evita-se o processo de “confusão” e erro do Hibernate, ou manter sempre uma lista apenas com Eager Loading, deixando todas as outras com azy. Veja como ficaria o exemplo da Listagem 3 aplicado a esta solução.

Listagem 3. Solução 1


  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.LAZY)
  List<Child1> child1s = new LinkedList<Child1>();
  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.LAZY)
  List<Child2> child2s= new LinkedList<Child2>();  

O problema da Solução acima é que nunca poderemos carregar as duas listas ao mesmo tempo, sempre teremos que fazer uma por uma. Ao tentar realizar um HQL realizando JOIN FETCH em ambas as listas o problema voltará a ocorrer.

  • Solução 2 – Mudar a Bag (List/Collection) para Set é uma outra solução viável e muito utilizada e simples. O conceito é trocar o tipo do seu objeto de List/Collection para o tipo Set. Você deve-se perguntar sempre: Eu preciso mesmo utilizar um List? Porque não utilizar Set? Na maioria dos casos o List é utilizado apenas porque estamos acostumados à fazê-lo. Observe a Listagem 4.

Listagem 4. Solução 2


  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.EAGER)
  Set<Child1> child1s = new HashSet<Child1>();
  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.EAGER)
  Set<Child2> child2s= new HashSet<Child2>();  
  • Solução 3 – Adicionar o @IndexColumn a sua bag. Iremos ver com mais detalhes na seção abaixo apenas para essa solução. Veja na Listagem 5 como ficará nossa solução após finalizada.

Listagem 5. Solução 3


  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.EAGER)
  @IndexColumn(name="INDEX_COL")
  List<Child1> child1s = new LinkedList<Child1>();  

Usando @IndexColumn para usar Multiple Bags

O uso da anotação @IndexColumn diz ao Hibernate se o elemento já foi ou não carregado, sendo assim evita-se a duplicidade como mostrado nas seções anteriores.

Porém fique muito atento, pois o @IndexColumn não é gerenciado pelo Hibernate no “lado mais fraco do relacionamento”, ou seja, o lado inverso. O lado mais fraco do relacionamento é especificado com o atributo “mappedBy” e como nossa Classe Parent tem um “mappedBy='parent'” significa que ela é o lado mais fraco do relacionamento, então devemos retirar esse mappedBy para que o @IndexColumn funcione como desejamos. Veja como ficaria na Listagem 6.

Listagem 6. Usando @IndexColumn sem mappedBy


  @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) //mappedBy removido
  @IndexColumn(name="INDEX_COL")
  List<Child> childs = new LinkedList<Child>();  

Acontece que ao usar o mappedBy e tornando o Parent o lado mais fraco da associação, o Hibernate não gerencia o index de forma automática, que é exatamente o que desejamos em nosso caso.

Certo, mas então você deve se perguntar: “Se eu não posso mudar a forma como meus objetos se relacionam, eu prefiro gerenciar de forma manual o index do que perder o meu mappedBy”.

No caso onde é necessário o uso do mappedBy juntamente com o @IndexColumn, você precisará gerenciar seu index de forma manual, vamos ver como.

Primeiramente o seu mapeamento deverá ficar como na Listagem 7, juntando o mappedBy e o @IndexColumn.

Listagem 7. @IndexColumn e mappedBy


  @OneToMany(mappedBy="parent",cascade=CascadeType.ALL, fetch=FetchType.EAGER)
  @IndexColumn(name="INDEX_COL")
  List<Child> childs = new LinkedList<Child>();  

Lembre-se que o “INDEX_COL” deve ser uma coluna criada no banco de dados, pois só assim será possível indexar os elementos de forma correta.

Vejamos na Listagem 8 como ficaria nossa classe com um index manual.

Listagem 8. Indexando de forma manual


  @Column(name = “INDEX_COLUMN”)
  private int position;
   
  @ManyToOne
  public Parent getParent() {
                  return parent;
  }
   
  public int getPosition() {
                  return parent.getChildren().indexOf(this);
  }
   
  public void setPosition(int index) {
  }

O que temos acima é um campo chamado “position” que tem como propósito a indexação manual de nossos elementos na lista.

Das soluções propostas é muito útil avaliar a solução 2, pois é o caso mais comum, onde não há necessidade do uso de um List. Deve-se migrar para o Set, afim de evitar tais erros. Como citamos nas seções anteriores, se você não sabe ou não tem um objetivo especifico para usar o List, use o Set.

Por outro lado, há soluções como o uso de @IndexColumn e Lazy Loadings quando há real necessidade do uso de duas ou mais listas no mesmo mapeamento. O problema de usar-se Lazy Loading é que estaremos sempre amarrados a carregar uma lista por vez, o que pode ser um tanto quanto oneroso em se tratando de grandes aplicações que necessitam de maior produtividade e maior facilidade para manutenção de código. Sendo assim, não nos resta outra solução (mostrada neste artigo) se não a @IndexColumn que nos permite o uso simultâneo de duas ou mais listas, sem a necessidade de mudança brusca de código ou mesmo da estrutura atual do sistema.

Esperamos que este artigo tenha sido proveitoso e útil para o entendimento do problema em questão e resolução do mesmo.