segunda-feira, 23 de novembro de 2015

Os perigos que o EAGER pode trazer para a sua aplicação (parte 1)

Olá, pessoal!

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.