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.
Olá,
o que você achou deste conteúdo? Conte nos comentários.