Você já teve problemas em um mapeamento OneToOne
com o JPA/Hibernate onde você gostaria que o comportamento fosse o lazy loading
mas ele insistia em sempre buscar a outra entidade?
Bem, é isso que eu quero te mostrar neste vídeo e post. Como isso pode acontecer e, o principal, como contornar a situação e resolver de uma maneira, digamos que simples. :)
O modelo de dados deste exemplo é bem simples, mas irá nos ajudar a entender o problema e como resolvê-lo de uma maneira simples, mas que deve ser usada com cautela, pois você pode alterar o comportamento esperado por outro desenvolvedor.
O código fonte do projeto pode ser encontrado no GitHub.
Neste projeto estamos utilizando alguns frameworks e ferramentas para nos auxiliarem, como o DBUnit, JIntegrity e JUnit. O projeto é gerenciado pelo Maven. Não vou entrar em detalhes de cada um destes frameworks e ferramentas por não ser o escopo deste post.
Para entender melhor sobre os testes de integração, você pode ler o post Testes de integração com DBUnit
Para criar as tabelas, depois de criado o schema exemplo_lazy_one_to_one no banco, execute o método main
da classe GerarTabelas
.
São duas classes de modelo, Usuario
e Endereco
, onde a segunda é a dona da relação, ou seja, a que possui a chave estrangeira.
Usuário:
@Entity @Table(name = "usuario") public class Usuario implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long codigo; private String nome; @OneToOne(mappedBy = "usuario") private Endereco endereco; // getters e setters }
Endereço:
@Entity @Table(name="endereco") public class Endereco implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long codigo; private String rua; @OneToOne @JoinColumn(name="codigo_usuario") private Usuario usuario; // getters e setters }
O problema que quero demonstrar e posteriormente resolver é, fazer uma consulta em Usuario
e ver que ele sempre irá fazer o select para Endereco
.
Na classe TestesConsultas
temos o método deveRetornarUsuario()
que faz uma consulta simples através do entity manager
.
Usuario u = this.manager.find(Usuario.class, 1L); assertEquals("João", u.getNome());
Execute e veja que mesmo não buscando nada de endereço, o Hibernate faz a consulta em Endereco
também.
Hibernate: select usuario0_.codigo as codigo1_1_0_, usuario0_.nome as nome2_1_0_, endereco1_.codigo as codigo1_0_1_, endereco1_.rua as rua2_0_1_, endereco1_.codigo_usuario as codigo_u3_0_1_ from usuario usuario0_ left outer join endereco endereco1_ on usuario0_.codigo=endereco1_.codigo_usuario where usuario0_.codigo=?
Você pode estar pensando: “Mas você esqueceu de colocar o fetch=FetchType.LAZY
“. Tente fazer, você irá perceber que mesmo assim ele irá fazer a consulta sem necessidade.
Isso acontece porque não é possível o Hibernate saber se a outra entidade é nula ou não sem fazer o select.
Existem algumas formas de resolver esse problema. Vou descrever uma delas que eu acho ser a mais fácil de todas. Me diga sua opinião, ficarei feliz em ouvir outras ideias e sugestões.
Primeira coisa a fazer é adicionar a anotação @LazyToOne
no atributo endereço de Usuario
. Depois implementar a interface org.hibernate.bytecode.internal.javassist.FieldHandled
, alterando também os métodos get
e set
do atributo endereço. A classe ficará como abaixo:
@Entity @Table(name = "usuario") public class Usuario implements Serializable, FieldHandled { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long codigo; private String nome; @OneToOne(mappedBy = "usuario", fetch=FetchType.LAZY) @LazyToOne(LazyToOneOption.NO_PROXY) private Endereco endereco; private FieldHandler handler; public Long getCodigo() { return codigo; } public void setCodigo(Long codigo) { this.codigo = codigo; } public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public Endereco getEndereco() { if (this.handler != null) { return (Endereco) this.handler.readObject(this, "endereco", endereco); } return endereco; } public void setEndereco(Endereco endereco) { if (this.handler != null) { this.endereco = (Endereco) this.handler.writeObject(this, "endereco", this.endereco, endereco); } this.endereco = endereco; } @Override public void setFieldHandler(FieldHandler handler) { this.handler = handler; } @Override public FieldHandler getFieldHandler() { return this.handler; } // hashCode e equals }
Tente novamente executar o código de teste. Você verá que somente a consulta em Usuario
foi feita.
Hibernate: select usuario0_.codigo as codigo1_1_0_, usuario0_.nome as nome2_1_0_ from usuario usuario0_ where usuario0_.codigo=?
O que fizemos foi enganar o Hibernate dizendo a ele que nossa classe já tinha sido “instrumentada” e nós estamos dando o comportamento que gostaríamos a ela no atributo endereço.
Para inicializar o endereço, basta chamar o get
que o Hibernate irá buscar no banco de dados.
Agora para finalizar, pode ser que em determinadas situações, como a criação de um WebService, você não queira de forma nenhuma que o endereço seja recuperado, ou seja, se não foi inicializado na consulta, deve retornar null
.
Você pode utilizar o beanlib-hibernate
e fazer uma cópia para um DTO. Veja o código final do método de teste.
@Test public void deveRetornarUsuario() { Usuario u = this.manager.find(Usuario.class, 1L); assertEquals("João", u.getNome()); Hibernate3DtoCopier copiador = new Hibernate3DtoCopier(); Usuario copia = copiador.hibernate2dto(u); assertNull(copia.getEndereco()); }
Repare que no objeto copia, estamos fazendo um assertNull
. E tudo está verde!
Deixe seu comentário do que você achou do workaround. Se já passou por isso e como resolveu.
Acesse ou baixe o código-fonte completo deste artigo no GitHub.
Para aprender mais sobre JPA 2 com Hibernate, conheça nosso curso online de Persistência de dados com JPA e Hibernate, que é completo e substitui a necessidade de cursos presenciais.
Olá,
o que você achou deste conteúdo? Conte nos comentários.