Introdução
O Hibernate é um framework para mapeamento objeto-relacional ou como também é conhecido originalmente do inglês, ORM (Object-Relational Mapping). O Hibernate tem como objetivo abstrair a complexidade, no tocante a disparidade do paradigma Orientado a Objetos x Modelo de Dados Relacional. Porém, há uma série de configurações a serem realizadas, para que a aplicação tenha o resultado esperado para o projeto. Dentre tantas configurações é comum deparar-se com erros/exceções. Logo, esse artigo relata uma falha comum ao mapear/configurar coleções de dados em entidades da aplicação.
O Problema
Quando trabalhamos com mais de uma coleção de dados, mapeada com o Hibernate, é natural obtermos um erro conhecido como: Multiple Bags. Isso ocorre porque uma associação/coleção pode ser tachada como uma Bag pelo Hibernate, por termos mais de um mapeamentos do tipo EAGER (como estratégia de busca), na mesma entidade, ou no grafo de objetos.
Caused by: org.hibernate.HibernateException: cannot simultaneously fetch multiple bags
Em Java, não existe implementação para o tipo Bag, somente: List, Map e Set.
O Bag representa uma coleção desordenada de objetos e permite a duplicidade.
Logo, cabe a pergunta: “Por que Java não tem uma implementação que represente uma estrutura Bag, mas o Hibernate identifica um List como sendo um Bag?”. Para respondermos essa pergunta, vamos primeiro entender como funciona internamente a estrutura de dados conhecida como List.
List – é uma coleção de dados ordenada, porém permite a duplicidade de objetos. Em Java, temos as seguintes implementações de List: ArrayList, Vector e a LinkedList.
Agora, imagine o seguinte cenário: poderemos ter duas coleções EAGER, ou seja, configurada de forma a trazer todos os itens do banco de uma só vez (estratégia que normalmente chamamos de busca ansiosa), através do seguinte código:
@Entity(name = "tbcliente") public class Cliente implements Serializable{ @Id @Column(name = "id", nullable = false) private Integer codigo; @CollectionOfElements(fetch = FetchType.EAGER) private List<String> emails; @CollectionOfElements(fetch = FetchType.EAGER) private List<String> telefones;
Ao tentar executar uma operação com um objeto da classe/entidade Cliente, receberemos o mesmo erro já exposto no início do post [simultaneously fetch multiple bags]. Esse erro ocorre devido o Hibernate ter como comportamento padrão, reconhecer a List, quando mapeada dessa forma, como um Bag. Pois em cenários como esse, apenas uma das coleções podem ser configuradas como EAGER, as outras deveriam ser mapeadas como LAZY. Podemos considerar a configuração correta do mapeamento, como uma primeira opção para o Hibernate não detectar um Bag.
A segunda alternativa seria indicar ao Hibernate que essa coleção é uma lista de dados ordenada. Para tal, precisamos utilizar a anotação: @IndexColumn. Veja exemplo abaixo:
@Entity(name = "tbcliente") public class Cliente implements Serializable{ @Id @Column(name = "id", nullable = false) private Integer codigo; @CollectionOfElements(fetch = FetchType.EAGER) @IndexColumn(name = "email") private List<String> emails; @CollectionOfElements(fetch = FetchType.EAGER) @IndexColumn(name = "fones") private List<String> telefones;
Vejamos o que aconteceu agora:
No código acima da Listagem 3, foram mapeadas duas Coleções do tipo List, onde a partir do momento em que as configuramos com a anotação @IndexColumn, o Hibernate passa a reconhecê-las como um lista de objetos ordenados. No banco de dados será criada uma tabela, onde a mesma terá uma coluna que mantém o índice, o qual a função desse, é manter a ordem na qual fora adicionado o e-mail ou telefone. Veja uma representação da tabela criada pelo Hibernate abaixo:
tbcliente_id [pk] | Element | Email [pk] |
---|---|---|
1 | carlos@gmail.com | 0 |
1 | maria@gmail.com | 1 |
1 | andrea@gmail.com | 2 |
A Tabela 1, indica que para o objeto cliente de ID/Codigo = 1, temos três e-mails cadastrados onde, a coluna Email, mantém a ordem de inserção, ou seja, o e-mail [carlos@gmail.com] foi o primeiro a ser adicionado na lista. Dessa forma, quando recuperarmos o cliente dono desses e-mails, teremos uma lista com e-mails na mesma ordem que foram salvos.
Uma outra opção, ainda para resolver o mesmo problema de múltiplas coleções em um grafo de objeto, é: ao invés de utilizar java.util.List para manter os objetos, opte por usar uma implementação de Set ou ainda um SortedSet. Veja abaixo como ficaria com uma coleção do tipo Set>:
@Entity(name = "tbcliente") public class Cliente implements Serializable{ @Id @Column(name = "id", nullable = false) private Integer codigo; @CollectionOfElements(fetch = FetchType.EAGER) Set<String> emails; @CollectionOfElements(fetch = FetchType.EAGER) Set<String> telefones;
Com o código acima, conseguimos garantir a unicidade dos objetos, pois como já sabemos as coleções do tipo Set, não permitem duplicatas de objetos. Você ainda poderá fazer como no código abaixo, que usa um SortedSet, que nesse caso, precisa-se indicar como será ordenador por: ordem NATURAL ou COMPARATOR. Para isso utilize a anotação @Sort.
@Entity(name = "tbcliente") public class Cliente implements Serializable{ @Id @Column(name = "id", nullable = false) private Integer codigo; @CollectionOfElements(fetch = FetchType.EAGER) @IndexColumn(name = "email") @Sort(type = SortType.NATURAL) SortedSet<String> emails; @CollectionOfElements(fetch = FetchType.EAGER) @Sort(ype = Sorte.NATURAL) SortedSet<String> telefones;
Conclusão
O problema do qual é tratado nesse artigo, é comum no dia a dia dos desenvolvedores Java que trabalham com o framework Hibernate. Contudo, nos cabe avaliar, qual a melhor estratégia a ser utilizada no projeto. Pois, assim como várias implementações a serem feitas em projetos de software, tudo depende do cenário que será aplicado e dos requisitos do sistema.