Embora o JPA tenha muito anos de estrada e inúmeros projetos já tenham nascido utilizando este útil e famoso padrão para persistência de dados, vejo com mais frequência do que eu gostaria o uso indiscriminado de um recurso simples, aparentemente até inofensivo, mas que pode ser o seu principal problema de performance no futuro.
Qual recurso? O famoso EAGER.
Acredito que uma parte significativa dos desenvolvedores utilizam o EAGER sem entender o que ele realmente faz por trás dos bastidores ou, mesmo quando sabe, não tem dimensão do impacto dele.
Sem mais delongas, vamos falar um pouco do que é o EAGER para entendermos o motivo dele ser tão perigoso.
O que é o EAGER no JPA
Para explicar como o EAGER funciona no JPA, vamos começar mostrando esta entidade Pessoa:
@Entity public class Pessoa { @Id private Long id; @Column private String nome; @OneToMany private List<Endereco> enderecos; @ManyToOne private Endereco enderecoPrincipal; }
E esta classe Endereco:
@Entity public class Endereco{ @Id private Long id; @Column private String logradouro; @Column private String numero; @ManyToOne(fetch = FetchType.LAZY) private Pessoa pessoa; }
Quando fazemos uma busca pela entidade Pessoa no banco de dados, o JPA vai carregar sempre os atributo id e nome da Pessoa. Mas, quando há relacionamento entre 2 tabelas, como ocorre com enderecos (representado por uma lista de endereços da pessoa) e o enderecoPrincipal (um único endereço, o principal endereço da pessoa), existem 2 formas de obter estas as informações destas entidades no JPA:
- Carregar a(s) entidade(s) já na busca de Pessoa, assim como ocorre com os outros atributos básicos (id e nome) de Pessoa. Este é o método EAGER.
- Carregar a(s) entidade(s) quando ela for necessária. Ou seja, quando for solicitada. Este é o método LAZY.
No exemplo de mapeamento de entidade Pessoa e Endereco, mostrado acima, temos ambos os casos (EAGER e LAZY) com os relacionamentos entre Pessoa e Endereco.
Vamos, então, falar um pouco do método EAGER e sua relação com com os mapeamentos via anotações @OneToOne, @ManyToOne, @OneToMany e @ManyToMany.
O mapeamento *ToOne: o EAGER ninja
Silencioso e fatal, todo relacionamento no JPA do tipo *ToOne (OneToOne e ManyToOne), por padrão, é do tipo EAGER.
Isto quer dizer que sempre que ao consultar uma Pessoa no nosso exemplo, o framework que estiver usando (seja Hibernate1, OpenJPA, etc) vai trazer também o endereço principal da pessoa.
@ManyToOne // deste jeito, este cara é sempre EAGER! public Endereco enderecoPrincipal;
Assim, se fizermos uma consulta JPQL como esta:
SELECT pessoa FROM Pessoa pessoa
E tivermos um mapeamento configurado como EAGER, conforme ocorre com o endereço principal, teremos um SQL parecido com este sendo gerado no momento da consulta JPA:
SELECT p.id, p.nome, -- informações de pessoas e.id, e.logradouro, e.numero -- informações de endereço FROM Pessoa as p JOIN Endereco e ON p.enderecoPrincipal_id = e.id;
Para entender o perigo que isto representa para o seu mapeamento de tabelas, veja a seguir este caso bem comum.
EAGER em dose dupla: o que ocorre quando um usuário é relacionado a pessoa
Considerando a classe Pessoa mostrada anteriormente, imagine que uma nova funcionalidade foi adicionada ao sistema e temos agora a entidade Usuario. Cada usuário é representado por uma pessoa, então é natural criarmos um relacionamento entre as entidades Usuario e Pessoa:
@Entity public class Usuario{ @Id private Long id; //outros atributos de Usuario @ManyToOne private Pessoa pessoa; }
Temos agora mais um mapeamento @ManyToOne no sistema que, como já dissemos, é EAGER por padrão.
Agora sempre que consultarmos um inocente usuário:
SELECT usuario FROM Usuario usuario
Vamos ter a pessoa relacionada a este usuário também sendo carregada! A consulta SQL gerada seria parecida com esta:
SELECT u.id, u.* -- informações de Usuario p.id, p.nome, -- informações de Pessoa e.id, e.logradouro, e.numero -- informações de Endereco FROM Usuario as u JOIN Pessoa as p ON u.pessoa_id = p.id JOIN Endereco e ON p.enderecoPrincipal_id = e.id;
Pode não parecer, mas temos um grande problema nascendo.
Como podem notar, quando fazemos agora uma simples busca pela entidade Usuario não temos mais um único EAGER, temos 2 EAGERs:
- de usuario para pessoa
- de pessoa para o endereço principal
Quando ocorrer uma consulta por Usuario, ele vai trazer sempre a Pessoa por causa do relacionamento ManyToOne (que é EAGER por padrão) que, por sua vez, vai trazer sempre o endereço principal da Pessoa (que também está com um relacionamento do tipo EAGER).
Momento de reflexão: pense agora como as várias entidades dentro de um sistema podem se relacionar assim, cada uma com várias colunas, com campos de texto de tamanhos consideráveis... Você logo chegará a conclusão que o impacto deste comportamento pode ser bem significativo. Com alguns EAGERs, você pode correr o risco de trazer uma porção de informações do banco de dados que você não vai utilizar em 90% das vezes.
Eu arrisco dizer que o uso indiscriminado de EAGER é o principal problema de performance em sistemas que usam um framework JPA.
Se isto já parece um problema sério o bastante para você, há um outro cenário comum em projetos envolvendo EAGER que tem impacto muito pior no desempenho. Mas falaremos dele no próximo texto :).
1. No Hibernate Mapping Files, que não segue a especificação JPA, todos os mapeamentos estarão configurados como LAZY por padrão, o que eu considero o comportamento mais correto. É no JPA que está especificado que os mapeamentos *ToOne devem ser EAGER por padrão e, por esta razão, o Hibernate (e demais frameworks JPA) são assim.↩
Nenhum comentário :
Postar um comentário