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.
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_DEBUGdesativado em produção.
Se o seu sistema está lento, converse com a MidiaUAI. Fale com a gente.