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.