Panel de Control

Laravel nos provee del poderoso ORM Eloquent como hemos aprendido en lecciones anteriores, sin embargo «con un gran poder viene una gran responsabilidad» y debemos conocer el problema de N+1, cómo detectarlo y solucionarlo para garantizar que nuestras consultas a la base de datos se realicen de una forma más óptima y no se salgan de control. Para detectar este problema instalaremos una barra de depuración o «debugbar» entre otros componentes y luego aplicaremos una técnica llamada «carga ambiciosa» al momento de construir la consulta.

Repositorio

Ver el código de esta lección en GitHub

Instalación de Laravel Debugbar

Laravel no incluye una barra de depuración oficial, sin embargo podemos instalar una de manera muy sencilla utilizando el comando:

#
composer require barryvdh/laravel-debugbar --dev

Puesto que a partir de Laravel 5.5 disponemos de una característica llamada Package Discovery en Laravel 5.5, luego de ejecutar este comando veremos la barra disponible en la parte inferior al visualizar el proyecto en el navegador.

Uno de los primeros puntos que notaremos es que se están ejecutando 17 consultas SQL o más:

Laravel Debugbar queries

Esto se produce por un problema llamado N+1, cada vez que iteramos un usuario e intentamos obtener la información de su equipo (donde corresponda), Eloquent ejecuta una consulta adicional a la base de datos.

Mejora tus habilidades con Eloquent ORM y aprende a desarrollar módulos avanzados con Laravel

Ver más

Antes de solucionar este problema veamos otra forma en que podemos detectarlo:

Instalación de Laravel N+1 Query Detector

Este componente se puede instalar ejecutando:

#
composer require beyondcode/laravel-query-detector --dev

Nuevamente el paquete estará disponible de forma automática y al recargar nuestro listado encontraremos otra sorpresa, en este caso un molesto alert:

Query Detector

El alert nos brinda más pistas sobre cómo solucionar este problema.

Solución al problema de N+1con Eloquent

La solución a este problema es más sencilla de lo que parece, simplemente debemos cargar de manera ambiciosa las relaciones a las que queremos acceder luego. Esto se logra de forma muy sencilla:

Al momento de construir nuestro query llamaremos al método with pasando como argumento la relación o relaciones que queremos cargar de forma ambiciosa: User::with('team') donde «team» es, por supuesto, el nombre de la relación. También podemos escribir User::query()->with('team').

Ahora si recargamos el listado de usuario podemos ver que se están ejecutando 3 consultas sencillas en vez de +-17. Una para contar los usuarios (para la paginación), otra para traer a los usuarios y una última para traer a los equipos relaciones.

Dependiendo de lo grave del problema de N+1 la cantidad de memoria y tiempo requeridos para cargar la página podrían reducirse considerablemente.

Puedes aprender más con el tutorial Lazy Loading vs Eager Loading escrito por Carlos Fernandes.

Bonus: Detección del problema de N+1 en las pruebas unitarias

Este proyecto lo hemos estado desarrollando con docenas de pruebas, sin embargo ninguna de estas atrapó el problema de n+1:

PHPUnit Tests

Esto podemos mejorarlo con un pequeño trait que yo diseñé y utilicé también en el Curso de Técnicas de autorización con Laravel:

Comencemos por copiar la clase DetectRepeatedQueries al directorio tests/ de nuestra aplicación.

Ahora vamos a llamar al método enableQueryLog dentro del método setUp y al método flushQueryLog dentro de tearDown de esta manera:

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DetectRepeatedQueries;

    public function setUp()
    {
        parent::setUp();

        //...

        $this->enableQueryLog();
    }

    public function tearDown()
    {
        $this->flushQueryLog();

        //...

        parent::tearDown();
    }

}

Nota que deliberadamente dejé código por fuera para simplificar el código de ejemplo.

Luego de hacer esto vamos a llamar al método $this->assertNotRepeatedQueries(); justo al final de la prueba donde queramos detectar posibles N+1. Las pruebas van a pasar si no se encuentran consultas repetidas y a fallar con un mensaje descriptivo de lo contrario:

Repeated query

Únete a nuestra comunidad en Discord y comparte con los usuarios y autores de Styde, 100% gratis.

Únete hoy

Enlaces Relacionados

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Búsqueda avanzada con Eloquent usando whereHas y Scopes Lección siguiente Combinar paginación con búsqueda y filtros en Laravel