Jakarta Persistence (JPA)

Lazy loading com mapeamento OneToOne

Se você já trabalha com JPA (Java Persistence API), sabe que usar lazy loading em um relacionamento one-to-one é CABULOSO! 😂

Entenda de uma vez por todas qual é o problema e aprenda as 3 melhores formas de resolvê-lo neste artigo.

O problema do lazy loading com @OneToOne

Se você ainda não faz ideia de quando acontece 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 (de forma inesperada) 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, você precisa retirar o mappedBy da propriedade contaCorrente e adicionar @JoinColumn.

Veja como vai ficar a entidade do funcionário:

@Entity
public class Funcionario {

    // Passa 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 esperado.

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

Agora, tem um jeito também para 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 em uma primary key também.

No caso do nosso exemplo, você precisa fazer com que a coluna funcionario_id da tabela conta_corrente passe a ser, além da foreign key, a primary key.

Assim, sempre que for buscar o funcionário, pode utilizar o mesmo identificador para buscar a conta corrente, ou seja, a chave primária da conta corrente é 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. E 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 é remover a propriedade contaCorrente da entidade Funcionario.

Depois, acrescente a anotação @MapsId na propriedade funcionario da entidade ContaCorrente.

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

@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 precisamos configurar o id manualmente. Basta definir 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 saberá que precisa definir a propriedade id da conta corrente com o id do funcionário.

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

Usando NO_PROXY e implementando PersistentAttributeInterceptable

Essa é uma solução 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), com um código meio burocrático 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 na JPQL, como abaixo:

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

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

Conclusão

Esse é um conhecimento de JPA que considero avançado. É o tipo de coisa importante conhecer, não só para conseguir lidar com o problema, mas para se destacar no mercado e deixar o gerente de queixo caído.

Por isso é importante ter o conhecimento sobre as formas de resolver o problema de usar lazy loading em relacionamentos bidirecionais com @OneToOne.

E se você tem interesse em aprofundar MUITO MAIS em JPA e se tornar um especialista na tecnologia, entre para a lista de espera do nosso novo curso online avançado Especialista 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.

Trabalha como programador Java há mais de uma década, principalmente com desenvolvimento de sistemas corporativos. Além de colaborar com o blog, também é instrutor de vários cursos de Java na AlgaWorks.

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!