Jakarta Persistence (JPA)

O problema do n+1

Já ouvi muita gente dizer que não gosta de JPA porque gera muito select a toa. Mas depois de analisar um pouco, percebi que elas não conheciam muito bem o próprio JPA.

Neste post, vou te mostrar uma situação muito comum entre usuários de JPA que não tomam alguns cuidados na hora de gerar suas consultas, e acabam acessando demais o banco de dados.

 

Para te mostrar esse problema, eu criei uma aplicação web com CDI e PrimeFaces para chegar perto de algo mais real, que pode acontecer com você no dia a dia.

A aplicação é bem simples, simula uma espécie de catálogo de veículos onde cada um tem seu proprietário. A única tela do sistema é uma tabela listando todos os veículos e seus proprietários. Veja o print abaixo:

Tela Lista de Veiculos

Se você ainda não percebeu, existe um relacionamento de ManyToOne entre Veiculo e Proprietario. Ou seja, um veículo possui um proprietário, mas um proprietário pode ter vários veículos.

@Entity
@Table(name = "veiculo")
public class Veiculo implements Serializable {

  private static final long serialVersionUID = 1L;

  private Long codigo;
  private String fabricante;
  private String modelo;
  private int anoFabricacao;
  private int anoModelo;
  private TipoCombustivel tipoCombustivel;
  private Proprietario proprietario;

    // getters e setters

    @ManyToOne
  @JoinColumn(name = "codigo_proprietario")
  public Proprietario getProprietario() {
    return proprietario;
  }

    //...
}

O objetivo dessa tela de listagem de veículos é ao acessa-la, já buscar todos os veículos para preencher a tabela.

Para fazer acesso ao banco de dados, criei a classe Veiculos que representa o repositório de veículos. No método todos() podemos implementar usando JPQL ou Criteria. Vamos ver as duas formas.

Em JPQL:

public List<Veiculo> todos() {
    return this.manager.createQuery("from Veiculo", Veiculo.class).getResultList();
}

Com Criteria:

public List<Veiculo> todos() {
    CriteriaBuilder builder = manager.getCriteriaBuilder();
    CriteriaQuery<Veiculo> criteriaQuery = builder.createQuery(Veiculo.class);
    
    Root<Veiculo> veiculo = criteriaQuery.from(Veiculo.class);
    criteriaQuery.select(veiculo);

    TypedQuery<Veiculo> query = manager.createQuery(criteriaQuery);
    return query.getResultList();
}

Com JPA 2.1, podemos usar a propriedade javax.persistence.sql-load-script-source e passar o nome de um arquivo com instruções sql para serem executadas quando a aplicação é iniciada. O arquivo ficou em META-INF/sql/carregar-dados.sql. Aproveitei e carreguei alguns veículos e proprietários no banco de dados.

insert into proprietario (codigo, nome, email, telefone) values (1, "João Silva", "joao@email.com", "9999-9999");
insert into proprietario (codigo, nome, email, telefone) values (2, "Maria Candida", "maria@email.com", "8888-8888");
insert into proprietario (codigo, nome, email, telefone) values (3, "Ricardo Augusto", "ricardo@email.com", "9898-9898");

insert into veiculo (fabricante, modelo, ano_fabricacao, ano_modelo, tipo_combustivel, codigo_proprietario) values ("Fiat", "Uno", 2013, 2013, "FLEX", 1);
insert into veiculo (fabricante, modelo, ano_fabricacao, ano_modelo, tipo_combustivel, codigo_proprietario) values ("Peugeot", "206", 2010, 2010, "GASOLINA", 1);
insert into veiculo (fabricante, modelo, ano_fabricacao, ano_modelo, tipo_combustivel, codigo_proprietario) values ("VW", "Gol", 2010, 2011, "ALCOOL", 2);
insert into veiculo (fabricante, modelo, ano_fabricacao, ano_modelo, tipo_combustivel, codigo_proprietario) values ("Ford", "Focus", 2014, 2014, "FLEX", 2);
insert into veiculo (fabricante, modelo, ano_fabricacao, ano_modelo, tipo_combustivel, codigo_proprietario) values ("Chevrolet", "Cruze", 2013, 2013, "FLEX", 3);

Repare que tenho 3 proprietários e 5 veículos.

Agora que o problema irá aparecer. Quando iniciar a aplicação no tomcat, por exemplo, e acessar a tela mostrada anteriormente, iremos ver no console as seguintes consultas:

Hibernate: 
    select
        veiculo0_.codigo as codigo1_1_,
        veiculo0_.ano_fabricacao as ano_fabr2_1_,
        veiculo0_.ano_modelo as ano_mode3_1_,
        veiculo0_.fabricante as fabrican4_1_,
        veiculo0_.modelo as modelo5_1_,
        veiculo0_.codigo_proprietario as codigo_p7_1_,
        veiculo0_.tipo_combustivel as tipo_com6_1_ 
    from
        veiculo veiculo0_
Hibernate: 
    select
        proprietar0_.codigo as codigo1_0_0_,
        proprietar0_.email as email2_0_0_,
        proprietar0_.nome as nome3_0_0_,
        proprietar0_.telefone as telefone4_0_0_ 
    from
        proprietario proprietar0_ 
    where
        proprietar0_.codigo=?
Hibernate: 
    select
        proprietar0_.codigo as codigo1_0_0_,
        proprietar0_.email as email2_0_0_,
        proprietar0_.nome as nome3_0_0_,
        proprietar0_.telefone as telefone4_0_0_ 
    from
        proprietario proprietar0_ 
    where
        proprietar0_.codigo=?
Hibernate: 
    select
        proprietar0_.codigo as codigo1_0_0_,
        proprietar0_.email as email2_0_0_,
        proprietar0_.nome as nome3_0_0_,
        proprietar0_.telefone as telefone4_0_0_ 
    from
        proprietario proprietar0_ 
    where
        proprietar0_.codigo=?

No total, 4 consultas foram realizadas. Por quê? É o problema do N+1!

Para a consulta de veículo, foram realizadas 3 consultas para proprietário, pois são os que tem relacionamentos com veículos. Se tivessem mais proprietários ali, mais consultas seriam realizadas em sequência.

Mas por que isso acontece? Porque não estamos usando JPA corretamente.

E como resolver? Simples, vamos fazer um fetch na consulta.

Em JPQL:

public List<Veiculo> todos() {
    return this.manager.createQuery("from Veiculo v join fetch v.proprietario", Veiculo.class).getResultList();
}

Com Criteria:

public List<Veiculo> todos() {
    CriteriaBuilder builder = manager.getCriteriaBuilder();
    CriteriaQuery<Veiculo> criteriaQuery = builder.createQuery(Veiculo.class);
    
    Root<Veiculo> veiculo = criteriaQuery.from(Veiculo.class);
    veiculo.fetch("proprietario");
    criteriaQuery.select(veiculo);

    TypedQuery<Veiculo> query = manager.createQuery(criteriaQuery);
    return query.getResultList();
}

Reiniciando o tomcat e acessando a tela novamente, veremos a saída abaixo:

Hibernate: 
    select
        veiculo0_.codigo as codigo1_1_0_,
        proprietar1_.codigo as codigo1_0_1_,
        veiculo0_.ano_fabricacao as ano_fabr2_1_0_,
        veiculo0_.ano_modelo as ano_mode3_1_0_,
        veiculo0_.fabricante as fabrican4_1_0_,
        veiculo0_.modelo as modelo5_1_0_,
        veiculo0_.codigo_proprietario as codigo_p7_1_0_,
        veiculo0_.tipo_combustivel as tipo_com6_1_0_,
        proprietar1_.email as email2_0_1_,
        proprietar1_.nome as nome3_0_1_,
        proprietar1_.telefone as telefone4_0_1_ 
    from
        veiculo veiculo0_ 
    inner join
        proprietario proprietar1_ 
            on veiculo0_.codigo_proprietario=proprietar1_.codigo

A sacada que utilizamos foi fazer um join junto com um fetch. O fetch significa, traz ou busca os dados de proprietário nesse join.

Repare que com uma palavra simples mudamos drasticamente o comportamento do JPA! Ao invés de fazer 4 consultas no banco de dados ele faz apenas 1! Isso é muito, concorda?

Conhecer bem JPA faz uma diferença enorme nas suas aplicações. Não é simplesmente funcionar, mas funcionar bem.

Deixe seu comentário abaixo dizendo o que achou desse post. Realmente uma palavrinha simples pode fazer uma diferença, certo?

Para aprender mais sobre JPA, conheça nosso curso online de JPA e Hibernate, que é completo e substitui a necessidade de cursos presenciais.

Graduado em Engenharia Elétrica pela Universidade Federal de Uberlândia e detentor das certificações LPIC-1, SCJP e SCWCD.

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!