JPA e Hibernate

Lazy loading com mapeamento OneToOne

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.

Graduado em Engenharia Elétrica pela Universidade Federal de Uberlândia e detentor das certificações LPIC-1, SCJP e SCWCD.

Olá,

o que você achou deste conteúdo? Conte nos comentários.

Junte-se a mais de 100.000 pessoas

Entre para nossa lista e receba conteúdos exclusivos e com prioridade

Você se Inscreveu com Sucesso!