JPA e Hibernate

Lazy loading com mapeamento OneToOne

Você vai aprender as 3 melhores formas de resolver o problema do Lazy Loading do JPA no relacionamento @OneToOne.

Para aqueles que ainda não conhecem o problema, ele ocorre quando tentamos fazer a parte non-owner da relação se tornar Lazy.

Só para ficar claro… A parte non-owner (parte que não é dona da relação), é o caminho de volta do relacionamento, ou seja, é parte em que usamos o atributo mappedBy.

Acontece que, quando optamos em deixar essa parte como Lazy, simplesmente, não funciona.

A propriedade anotada com @OneToOne(mappedBy = "...") acaba sendo carregada do mesmo jeito.

Exemplificando…

Imagine duas entidades. A entidade Funcionario

@Entity
public class Funcionario {

    @Id
    private Integer id;

    private String nome;

    // Non-Owner da relação.
    @OneToOne(mappedBy = "funcionario", fetch = FetchType.LAZY)
    private ContaCorrente contaCorrente;

    // getters e setters omitidos.
}

… e a entidade ContaCorrente.

@Entity
public class ContaCorrente {

    @Id
    private Integer id;

    private String numero;

    // Owner da relação.
    @OneToOne
    @JoinColumn(name = "funcionario_id")
    private Funcionario funcionario;

    // getters e setters omitidos.
}

Com base no relacionamento @OneToOne configurado para as entidades acima, se fizermos uma tentativa de trazer um registro de funcionário como abaixo…

Funcionario funcionario = entityManager.find(Funcionario.class, 1);

… vai carregar a conta corrente também.

Veja o SQL gerado.

Hibernate:
select
        funcionari0_.id as id1_1_0_,
        funcionari0_.nome as nome2_1_0_
    from
        funcionario funcionari0_
    where
        funcionari0_.id=?
Hibernate:
select
        contacorre0_.id as id1_0_1_,
        contacorre0_.agencia as agencia2_0_1_,
        contacorre0_.banco as banco3_0_1_,
        contacorre0_.funcionario_id as funciona5_0_1_,
        contacorre0_.numero as numero4_0_1_,
        funcionari1_.id as id1_1_0_,
        funcionari1_.nome as nome2_1_0_
    from
        conta_corrente contacorre0_
    left outer join
        funcionario funcionari1_
        on contacorre0_.funcionario_id=funcionari1_.id
    where
        contacorre0_.funcionario_id=?

Como o esperado, é feita a consulta para a tabela funcionario, mas também, é feita, indevidamente, uma outra consulta para conta_corrente.

E é sobre isso que vamos tratar. Vamos implementar formas resolver esse problema.

Iremos pensar em formas estratégicas de resolver esse problema onde vamos, basicamente, repensar se podemos fazer pequenas alterações no modelo.

Depois veremos uma forma técnica de resolver o problema sem precisar repensar o modelo.

Adiantando, é necessário ter essas duas cartas na manga (a estratégica e a técnica), pois não temos uma solução que eu poderia chamar de perfeita para esse caso.

Minha preferência, em particular, é repensar o modelo.

Mas caso você não possa ou não queira fazer isso, vou também apresentar uma solução técnica que é o melhor workaround (solução de contorno) que encontrei até o momento.

Você pode repensar seu modelo?

Tenho consciência de que repensar o modelo não é uma solução viável para todos, mas é a primeira coisa que eu pensaria.

Uma primeira ideia aqui é você refletir:

Será que a parte que é non-owner não poderia ser owner?

Se a única parte que usa o Lazy é o non-owner (como você viu nas entidades apresentadas acima), então é uma solução a considerar.

Basicamente, você vai trocar o uso mappedBy pelo uso de @JoinColumn.

De acordo com o exemplo que passei do funcionário, seria você tirar o mappedBy, da propriedade contaCorrente, e adicionar @JoinColumn.

Ficaria, simplesmente, ao contrário.

Abaixo, veja como ficaria a entidade do funcionário.

@Entity
public class Funcionario {

    // Passaria a ser o owner da relação.
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "conta_corrente_id")
    private ContaCorrente contaCorrente;

    // As outras propriedades, e todos os getters e setters, foram omitidos.
}

E agora, a entidade da conta corrente.

@Entity
public class ContaCorrente {

    @OneToOne(mappedBy = "contaCorrente")
    private Funcionario funcionario;

    // As outras propriedades, e todos os getters e setters, foram omitidos.
}

Como agora a propriedade que está com o Lazy é o owner (dono) da relação, então funcionará como o esperado.

Lembrando que só recomendo essa alteração se a inversão fizer sentido no seu modelo de banco de dados.

Agora, tem um jeito também de não precisar inverter o owner. Basta utilizar @MapsId como veremos a seguir.

Utilizando @MapsId para fazer, da FK, uma PK também

Essa é uma boa solução, mas que também precisa que você repense um pouco o seu modelo.

Funciona assim…

Você vai tornar a foreign key uma primary key também.

No caso do nosso exemplo seria fazer com que a coluna funcionario_id da tabela conta_corrente passasse a ser, além da foreign key, a primary key.

Assim, sempre que fosse buscar o funcionário, poderia utilizar o mesmo valor para buscar a conta corrente, ou seja, a chave primária da conta corrente seria a mesma do funcionário.

Essa alternativa vai permitir que você não precise do mapeamento bidirecional, ou seja, você não vai precisar do lado non-owner da relação. Vai, simplesmente, removê-lo.

Como a parte non-owner não vai estar mais presente, então claro, não vai existir o problema com carregamento tardio (Lazy).

A anotação @MapsId não é essencial para fazer esse mapeamento, mas ela deixa o nosso código mais automatizado. Primeiro vamos fazer o mapeamento, depois explico o porquê.

A primeira coisa seria remover a propriedade contaCorrente da entidade Funcionario.

Depois você iria acrescentar a anotação @MapsId na propriedade funcionario da entidade ContaCorrente.

E também, a propriedade @Id não poderia mais ter a anotação @GeneratedValue. Se houver, ela precisará ser removida.

Veja abaixo como ficaria.

@Entity
public class ContaCorrente {

    @Id
    private Integer id;

    @MapsId
    @OneToOne
    @JoinColumn(name = "funcionario_id")
    private Funcionario funcionario;

    // As outras propriedades, e todos os getters e setters, foram omitidos.
}

Quanto a questão de termos um pouco mais de automatização, é o seguinte…

Ao construir um objeto do tipo ContaCorrente, não precisaríamos configurar o id manualmente. Basta preencher a propriedade funcionario.

ContaCorrente cc = new ContaCorrente();
cc.setBanco("012");
cc.setAgencia("345");
cc.setNumero("67890");
cc.setFuncionario(funcionario); // O JPA retira o id da conta dessa propriedade.

Assim, através de @MapsId, o JPA iria saber que precisa preencher a propriedade id da conta corrente com o id do funcionário.

Se essa solução (e a anterior) não te atendem, então tem a solução técnica que veremos a partir de agora.

Usando NO_PROXY e implementando PersistentAttributeInterceptable

Essa é uma solução em que você não precisa remover ou alterar as propriedades que já existem.

Em contrapartida, é uma solução considerada mais um workaround (solução de contorno)… pelo código extra que precisa ser adicionado.

O primeiro passo é anotar a propriedade contaCorrente, da entidade Funcionario, com @LazyToOne.

@LazyToOne(LazyToOneOption.NO_PROXY)
@OneToOne(mappedBy = "funcionario", fetch = FetchType.LAZY)
private ContaCorrente contaCorrente;

Depois, é preciso que a entidade Funcionario implemente a interface PersistentAttributeInterceptable.

public class Funcionario implements PersistentAttributeInterceptable {

    @Transient
    private PersistentAttributeInterceptor persistentAttributeInterceptor;

    @Override
    public PersistentAttributeInterceptor $$_hibernate_getInterceptor() {
        return persistentAttributeInterceptor;
    }

    @Override
    public void $$_hibernate_setInterceptor(PersistentAttributeInterceptor persistentAttributeInterceptor) {
        this.persistentAttributeInterceptor = persistentAttributeInterceptor;
    }

    // As outras propriedades e os outros getters e setters, foram omitidos.
}

Por fim, é preciso alterar os métodos getter e setter da propriedade contaCorrente para utilizar a propriedade persistentAttributeInterceptor como abaixo.

public ContaCorrente getContaCorrente() {
    if (this.persistentAttributeInterceptor != null) {
        return (ContaCorrente) this.persistentAttributeInterceptor.readObject(
                                        this, "contaCorrente", this.contaCorrente);
    }
    return this.contaCorrente;
}

public void setContaCorrente(ContaCorrente contaCorrente) {
    if (this.persistentAttributeInterceptor != null) {
        this.contaCorrente = (ContaCorrente) persistentAttributeInterceptor.writeObject(
                                        this, "contaCorrente", this.contaCorrente, contaCorrente);
    } else {
        this.contaCorrente = contaCorrente;
    }
}

Agora, fazendo uma busca com o EntityManager.find para algum funcionário, a conta corrente do mesmo não será carregada a menos que o getter getContaCorrente seja invocado.

E você ainda pode carregar a conta corrente, juntamente com o funcionário, usando o JOIN FETCH como abaixo.

Funcionario funcionario = entityManager
                .createQuery("select f from Funcionario f join fetch f.contaCorrente cc where f.id = 1", Funcionario.class)
                .getSingleResult();

Essa é uma solução mais verbosa, mas, no caso de não poder ou não querer mexer no modelo, é uma boa alternativa.

Pensamentos finais

Esse já é um conhecimento praticamente avançado sobre JPA. É o tipo de coisa que vai fazer com que a equipe olhe pra você de forma diferente.

Por isso é importante ter o conhecimento sobre as formas de resolver o problema do LAZY na parte bidirecional de um relacionamento @OneToOne.

E se você tem interesse em aprofundar mais em JPA, recomendo que você faça o nosso curso online avançado de JPA.

Um abraço e até a próxima!

PS: você pode baixar o código-fonte dos exemplos em nosso GitHub: http://github.com/algaworks/artigo-problema-lazy-loading-onetoone.

É graduado em Sistemas de Informação, trabalha como instrutor na AlgaWorks e está no mercado de programação Java há mais de 9 anos, principalmente no desenvolvimento de sistemas corporativos.

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!