Mapeando views com Hibernate no spring-data - Bluesoft Labs
21 de fevereiro de 2017Ismael Soares Artigos, Programação1
Representar uma view como entidade do Hibernate não é nada complexo, você apenas cria uma classe com base na sua view e anota ela de acordo. Acontece que a view no hibernate não tem uma anotação especial, é uma entidade como as outras e por conta disso, precisa que um atributo esteja anotado com @Id. Só que você já sabe que sua view (normalmente) não tem um id, já que ela é uma representação de outros dados. Então você resolve anotar qualquer atributo com @Id e seguir em frente.
Só que quando você recuperar registros dessa view e houver duas linhas com o mesmo valor, só uma delas será retornarda. Confuso? Vamos ver um exemplo.
Imagine que nossa aplicação tem duas tabelas, uma de pessoas e outra de endereços. Além disso, temos uma view que nos retorna a pessoa e seu endereço completo. Nossa tabela de pessoa tem um Bruce Wayne cadastro com dois endereços e um Berry Allen com só um endereço. Vamos ver como fica nosso código.
@OneToMany(mappedBy = "pessoa")
@JoinColumn(name = "pessoa_id")
public class PessoaComEndereco {
private Long enderecoId;
private String enderecoCompleto;
Como você pode ver, a view teve seu id anotado no id da pessoa, acontece que quando tentarmos recuperar os dados de Bruce dentro da aplicação através da nossa view, vamos ter um problema. Veja só:
Se você executar o teste carregarEnderecosEsperandoDuplicidade presente na classe ViewHibernateSpringDataApplicationTests vai obter o mesmo resultado: O mesmo endereço é carregado duas vezes.
"Mas Angeliski, como isso é possível? Eu verifiquei o banco e o select, tá tudo certo?"
Obs. Eu não testei isso sem usar o spring-data para verificar se esse erro ocorre também.
O problema está claramente no @Id que especificamos na view. Vamos pensar na solução agora.
1º Solução – EmbeddedId
Quando falamos do @Id você pode logo pensar em resolver isso gerando um Id composto para a view. Então vamos criar uma classe para testar esse cenário:
@Table(name = "PESSOA_COM_ENDERECO")
public class PessoaComEnderecoIdEmbeddable {
private PessoaEnderecoId pessoaEnderecoId;
Agora sim, nosso resultado está correto quando realizamos a busca:
Isso resolve o nosso atual problema, só que nossas views quase nunca são tão enxutas e provavelmente não terão um conjunto de identificação único que sempre faça sentido.
"Angeliski, se o EmbeddedId não pode resolver sempre, como faz?"
2º Solução – Gerando um id para a View
É isso ai. Nós, programadores, vamos providenciar um identificador para essa view.
Primeiro vamos ver nossa classe, que representa isso:
public class PessoaComEnderecoIdGerado {
Se você notar, nós acrescentamos o campo Id nesse caso, para anotar ele como o @Id dessa view. Mas como vamos gerar um identificador no banco? Dê uma olhada na nossa declaração da view:
CREATE OR REPLACE VIEW pessoa_com_endereco AS
UUID() AS ID,
p.id pessoa_id,
e.id endereco_id,
concat(e.logradouro, ', ', e.bairro, ' – CEP:', e.cep) endereco_completo
FROM pessoa p INNER JOIN endereco e ON p.id = e.pessoa_id;
Se você observar, o primeiro item do select é o id, usando a função UUID() (para o Mysql!).
Essa função vai ser responsável por gerar nosso identificador.Se você se incomodar pelo identificador se uma String, pode usar a função UUID_SHORT() no lugar e mudar para Long.
Ou se estiver usando um database Oracle, pode usar o rownum para gerar seu identificador.
As duas abordagens tem suas desvantagens.
Criar identificadores compostos no JPA é verboso e muitas vezes não faz sentido, ainda mais se tratando de uma view. Além disso, por ser uma view, o conjunto de dados que torna a linha única, pode não estar presente no retorno, ou não ter a mesma consistência para os dados, o que torna inviável criar essa classe composta.
Criar uma coluna que que representa seu Id pode ser um problema quando você está mexendo em uma base legada por exemplo, nem sempre é possível alterar a view para adicionar essa coluna que você tanto necessita. Além disso, essa solução se parece muito com um contorno a limitação do JPA no mapeamento de Views.
Eu disponibilizei o código desses teste neste repositório, assim você pode reproduzir e testar por si mesmo.
Sugestões e melhorias no código são bem vindas, basta abrir uma issue/pull request.
Qual estratégia você vai usar vai depender do seu contexto, eu só apresentei as abordagens. E você já teve um problema parecido? Abordou de outra forma?