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:
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.
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:
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:
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:
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