Blog
Laravel Debugbar com N+1, causa no controle de acesso e código antigo riscado em vermelho e novo em azul

Laravel Debugbar: N+1 no controle de acesso

O sistema inteiro estava lento. Com o Laravel Debugbar, acompanhamos as consultas e encontramos a causa principal em uma única linha dentro do controle de acesso: a cada permissão verificada, uma consulta buscava os grupos do usuário. O problema apareceu em um projeto concluído para uma instituição de ensino superior, em um sistema Laravel 9 com Livewire integrado ao TOTVS Corpore RM, em SQL Server.

O que é uma consulta N+1

Uma consulta N+1 acontece quando o sistema faz uma consulta para buscar uma lista e depois faz outra para cada item dessa lista. Uma tela com muitos registros pode, assim, gerar uma sequência de buscas parecidas. É como pedir a lista de estudantes de uma turma e, em seguida, perguntar o nome de cada um, um por um.

O sistema ainda pode mostrar a informação certa. O problema está no trabalho repetido que fica escondido atrás da tela. Uma consulta a mais pode parecer inofensiva; repetida muitas vezes, em vários pontos da aplicação, ela aumenta o tempo de resposta. Quando a lógica fica em um componente compartilhado, como o menu, o efeito aparece em páginas diferentes.

Como o Laravel Debugbar mostrou as consultas repetidas

Instalamos o pacote barryvdh/laravel-debugbar no ambiente de desenvolvimento. Na aba de consultas, o Laravel Debugbar mostrou a mesma consulta sendo executada várias vezes durante o carregamento de uma página. A repetição aparecia nas páginas analisadas. Isso nos levou a investigar primeiro os componentes que elas tinham em comum.

A aba de consultas do Laravel Debugbar destaca consultas duplicadas.
No exemplo do site do Laravel Debugbar, olhe o resumo com 8 consultas duplicadas e as linhas repetidas, incluindo a consulta a users três vezes.

O Laravel Debugbar mostra as consultas feitas durante uma requisição e ajuda a perceber padrões repetidos. No nosso caso, seguimos esse rastro até o controle de acesso e o seletor de contexto. O pacote deve ficar em require-dev, e APP_DEBUG deve permanecer desativado em produção. A ferramenta nos ajudou a enxergar o problema no desenvolvimento, sem ficar habilitada no ambiente de produção.

Uma consulta em cada verificação de permissão

Cada permissão era registrada como um Gate do Laravel, e os menus usavam @can para decidir o que exibir. O menu lateral chegava a fazer 29 verificações. O painel de configurações acrescentava outras 10. Em cada verificação, a função buscava os grupos do usuário com $this->groups()->get().

No Eloquent, groups() chama o relacionamento como método e retorna uma consulta que executamos com get(); groups, sem parênteses, acessa a propriedade dinâmica, que carrega o relacionamento e mantém o resultado no modelo durante a requisição. Trocamos uma linha:

// Antes: executava uma consulta a cada verificação
public function hasAclPermission($permission)
{
    $userGroups = $this->groups()->get();

    return $permission->groups
        ->pluck('id')
        ->intersect($userGroups->pluck('id'))
        ->isNotEmpty();
}

// Depois: acessa os grupos pelo relacionamento carregado
public function hasAclPermission($permission)
{
    $userGroups = $this->groups;

    return $permission->groups
        ->pluck('id')
        ->intersect($userGroups->pluck('id'))
        ->isNotEmpty();
}

Na prática, cada item de menu que o usuário podia ou não ver custava uma ida ao banco, em toda página aberta. Para quem usava o sistema, isso significava que uma checagem invisível no menu podia atrasar a página inteira, antes mesmo de chegar ao conteúdo principal.

Seletor de contexto e listagens

Também encontramos consultas repetidas no seletor de contexto, que aparecia em todas as páginas e listava unidades e tipos de curso disponíveis para o usuário. Para cada opção, um helper consultava o Corpore RM para buscar o nome da unidade. Passamos a aproveitar os dados que já estavam na sessão do usuário. Publicamos essas correções três dias depois de instalar o Laravel Debugbar.

Nas semanas seguintes, aplicamos o mesmo método às listagens paginadas. Antes, cada linha buscava separadamente informações relacionadas, como a situação da inscrição. Em uma listagem sem carregamento antecipado, o acesso a $inscricao->status->nome dispara uma consulta por linha:

// Antes: consulta a relação dentro do laço
foreach ($inscricoes as $inscricao) {
    echo $inscricao->status->nome;
}

Substituímos essas buscas por uma consulta com joins, trazendo somente as colunas exibidas. Este exemplo é simplificado e usa nomes ilustrativos:

// Depois: busca os dados da inscrição e da situação em conjunto
$inscricoes = Inscricao::query()
    ->join('situacoes', 'situacoes.id', '=', 'inscricoes.situacao_id')
    ->select('inscricoes.id', 'inscricoes.nome', 'situacoes.nome as status')
    ->paginate();

Quando a consulta pede objetos relacionados do Eloquent, with() também permite eager loading: as relações são carregadas antecipadamente, em vez de serem buscadas uma a uma dentro do laço. A escolha entre joins e eager loading depende dos dados e do resultado que a tela precisa exibir. Nas listagens que corrigimos, usamos joins e selecionamos apenas os campos necessários.

O resultado e o que levamos do trabalho

Com as correções, o tempo de resposta do servidor caiu mais de 70%. Uma linha no controle de acesso tinha efeito amplo porque a função era chamada repetidamente por menus compartilhados entre as páginas. As listagens tinham outro padrão de repetição, mas o princípio da correção era o mesmo: localizar buscas feitas item por item e reunir o acesso aos dados relacionados.

Levamos algumas práticas desse trabalho:

  • Quando todas as páginas ficam lentas, começamos por menus, permissões e outros componentes compartilhados.
  • Observamos a aba de consultas para encontrar repetições que não aparecem na interface.
  • No Eloquent, verificamos se chamamos um relacionamento como método ou se acessamos a propriedade dinâmica.
  • Em listagens paginadas, carregamos os dados relacionados em conjunto, com joins ou with(), conforme a consulta necessária.
  • Mantemos ferramentas de depuração no ambiente de desenvolvimento, com APP_DEBUG desativado em produção.

Se o seu sistema está lento, converse com a MidiaUAI. Fale com a gente.