Cómo solucionar el problema N + 1


El problema N + 1 es uno que existe en la mayoría de los ORM, o herramientas de mapeador relacional de objetos. El problema de N + 1 ocurre como resultado de la llamada "carga diferida". La carga diferida de datos de la base de datos ocurre cuando se realiza una consulta para un registro principal y luego una consulta adicional para cada registro secundario. En este tutorial, veremos cómo resolver el problema N + 1 haciendo uso de una técnica llamada Carga ansiosa. También inspeccionaremos cómo depurar estas consultas excesivas utilizando una herramienta dedicada.


Depurar consultas SQL con la barra de depuración de Laravel

Para comenzar a depurar algunas consultas SQL, tomemos https://github.com/barryvdh/laravel-debugbar e instálelo ahora. En la terminal:

vagrant @ homestead: ~ / Code / forumio $ composer requiere barryvdh / laravel-debugbar --dev
Usando la versión ^ 3.1 para barryvdh / laravel-debugbar
./composer.json ha sido actualizado
Carga de repositorios de compositor con información del paquete
Actualización de dependencias (incluido require-dev)
Operaciones del paquete: 2 instalaciones, 0 actualizaciones, 0 eliminaciones
  - Instalando maximebf / debugbar (v1.14.1): Descargando (100%)
  - Instalando barryvdh / laravel-debugbar (v3.1.0): Descargando (100%)
maximebf / debugbar sugiere instalar kriswallsmith / assetic (la mejor manera de administrar activos)
maximebf / debugbar sugiere instalar predis / predis (almacenamiento de Redis)
Escribir archivo de bloqueo
Generación de archivos de carga automática optimizados
> Illuminate \ Foundation \ ComposerScripts :: postAutoloadDump
> Paquete artesanal @php: descubre
Paquete descubierto: fideloper / proxy
Paquete descubierto: laravel / tinker
Paquete descubierto: barryvdh / laravel-debugbar
El manifiesto del paquete se generó correctamente.

Registrarse con AppServiceProvider

Una buena forma de cargar condicionalmente la barra de depuración es configurarla en AppServiceProvider como se muestra a continuación. Si no desea que la barra de depuración se ejecute en un entorno de producción y, por supuesto, no lo haría, esto cargará el servicio de la aplicación solo en un entorno local.

Recargar la aplicación nos muestra que la barra de depuración ya se está ejecutando. Observe que podemos hacer clic en varias opciones para ver información realmente interesante sobre la aplicación en ejecución. En nuestro caso, queremos centrarnos en la gran cantidad de consultas SQL que ocurren. Al hacer clic en la pestaña Consultas, se muestran 57 consultas. La carga diferida es la culpable aquí, y eso es lo que queremos arreglar.
ejemplo de la barra de depuración de laravel


De la carga perezosa a la ansiosa

La carga ansiosa es más eficiente que la carga diferida. En Carga ansiosa, cuando se realiza una consulta para una entidad, también incluye en la consulta la capacidad de buscar todas las entidades relacionadas a la vez. Hasta ahora, en esta serie de tutoriales, hemos sido vagos cargando casi todo. Está bien, ya que nos da la oportunidad de encontrar y corregir esos puntos en el código ahora.


Usar con () para reducir consultas

Comencemos con ThreadsController.

Tal como está ahora, cuando visitamos la vista para examinar todos los hilos, lo primero que sucede es que recuperamos todos los hilos de la base de datos. Sin embargo, en la vista, debemos tener en cuenta una relación con los hilos, y esa es la relación de los canales. Para reducir la dependencia de la carga diferida, podemos hacer uso del método elocuente with () para cargar ansiosamente los canales con los subprocesos. Actualicemos esa consulta para hacer uso del método with () aquí:

¡Podemos recargar la vista de índice y ver que el número de consultas SQL se ha reducido drásticamente!
consultas de la barra de depuración de laravel


Usar Cache para reducir consultas

Otra forma de reducir las consultas es mediante el uso inteligente de la fachada Cache. En AppServiceProvider configuramos un compositor de vistas que podría refactorizarse.

En el fragmento anterior, utilizamos la fachada de la caché para eliminar la necesidad de consultar los canales en cada carga de página. Esto también ayuda a reducir la cantidad de consultas necesarias.


Reducción de consultas al ver un solo hilo con varias respuestas

Hemos arreglado la página de índice principal donde podemos ver todos los hilos a la vez. Comenzamos con 57 consultas en total, pero terminamos con solo 2 consultas. ¡Ahora ves lo útil que es el método with ()! Pasando a ver un hilo y todas las respuestas asociadas, estamos viendo más consultas de las que probablemente necesitemos.
carga diferida


Reducción de consultas en sus archivos de vista

Tenga cuidado con las llamadas a métodos de relación en los archivos de visualización. Esta es una fuente común de generar más consultas de las que necesita. Abra reply.blade.php y busque las llamadas a $ reply-> favourites () -> count () específicamente. En realidad, estas llamadas desencadenan consultas en la base de datos. ¿Cómo podemos solucionar esto? Podemos hacer referencia a esos recuentos como atributos en lugar de llamadas a métodos.


De la llamada a la comprobación de atributos.

Necesitamos configurar Eager Loading en el modelo para facilitar esto. ¿Adivina qué? Eso es muy fácil usando el método withCount (). ¡Aquí destacamos el modelo de hilo actualizado!

Ahora estamos viendo una buena reducción en las consultas.
laravel withCount para carga ansiosa


Más problemas en la vista

En response.blade.php, hay otro infractor de carga diferida mediante la llamada a $ reply-> owner-> name. Lo que sucede con esto es que cada vez que se carga una respuesta, se deben realizar consultas adicionales para el propietario de esa respuesta. Queremos cargar las respuestas y los propietarios relacionados, todo de una vez. Una vez más, Eager Loading está aquí para que eso funcione para nosotros. En el método de respuestas del Thread Model, agregamos otra llamada a with () así.

Esto eliminará la necesidad de llamar a $ reply-> owner-> name para realizar consultas adicionales. El propietario ahora está ansioso por cargar en el modelo de Thread. Esto redujo otra consulta en nuestra barra de depuración.
laravel con ejemplo de método


Un problema de carga diferida en el modelo de respuesta

Recuerde que creamos una pequeña función en nuestro modelo de respuesta llamada isFavorited () para ayudar a verificar si un usuario ha aplicado un favorito a una respuesta en particular o no. El código se ve así.

Funciona y cumple la meta que tenemos. Sin embargo, el problema es que está utilizando la carga diferida. Entonces, cada vez que se agrega una nueva respuesta a un hilo, se agrega otra consulta a esa página. No queremos eso.


Carga ansiosa usando protected $ con

¿Quiere activar la carga rápida automáticamente? Puede hacer esto completando la propiedad $ with en su Modelo y proporcionando una matriz de cualquier modelo relacionado que desee cargar con entusiasmo. Esto hará que la llamada explícita a with () sea innecesaria. Veamos cómo funciona esto.

En el modelo de respuesta, podemos rellenar la propiedad $ con la cadena de 'propietario'. Lo que esto significa es que queremos cargar ansiosamente esta relación para cada consulta. Ahora, cada vez que se obtiene una respuesta de la base de datos, el propietario relacionado siempre está disponible. No es necesario llamar a ningún método adicional with (). Entonces, si tenemos este código en Reply.php

Eso significa que podemos * eliminar * la llamada a with () en el modelo Thread así:


Ansioso por cargar múltiples relaciones

Puede cargar ansiosamente tantas relaciones como desee. ¿Qué tal cuando cargamos una respuesta, también cargamos el propietario además de los favoritos? Todo lo que tenemos que hacer es rellenar el $ con una propiedad como esta.

Con este cambio, podemos actualizar el método isFavorited () en el modelo Reply. En lugar de esto:

Ahora podemos hacer esto:

También podemos actualizar el modelo de Thread para cargar siempre la relación de 'creador'. Si sentimos que en cualquier momento vamos a buscar un hilo, también vamos a querer tener acceso al creador, entonces, una vez más, podemos usar esa útil propiedad $ witch.


Determinación del recuento de modelos con un getter personalizado

Quizás quieras volver a mantener el método replies () en el modelo Thread súper simple. Lo que significa que también podemos eliminar la llamada a withCount ('favoritos').

Para hacer esto, podemos configurar un captador personalizado en el modelo Responder. Aquí es cómo.


'Canal' de carga ansiosa en el modelo Thread

Agreguemos también una carga ansiosa para la relación de 'canal' en el modelo Thread así:

Con esto en su lugar, podemos modificar el método getThreads () en ThreadsController eliminando cualquier llamada explícita al método with ().
carga ansiosa en modelo


Mejor organización del código con un rasgo

Para terminar, usaremos nuestras nuevas habilidades de refactorización para limpiar el modelo de respuesta extrayendo algo de código a un rasgo dedicado. En primer lugar, podemos crear el archivo Trait.
Rasgo PHP

Ahora actualice el modelo Responder para usar el rasgo Favorito de esta manera:

Finalmente, puede hacer que PHP Storm haga todo el trabajo pesado por usted simplemente usando la herramienta Pull Members Up.
phpstorm tira de miembros hacia arriba

Ahora tienes un modelo de respuesta limpio y agradable y también un rasgo agradable y limpio:
Reply.php

Favoriteable.php


¡Ejecute sus pruebas!

Como yo, puede que estés paranoico de que con todo lo que hemos tocado en este tutorial, seguramente algo ahora debe estar roto en alguna parte. Bueno, podemos ejecutar nuestro conjunto de pruebas completo, y resulta que todo pasa, ¡así que estamos listos para comenzar!

vagabundo @ homestead: ~ / Código / forumio $ phpunit
PHPUnit 6.5.5 por Sebastian Bergmann y colaboradores.

......................... 25/25 (100%)

Tiempo: 4,32 segundos, memoria: 12,00 MB

OK (25 pruebas, 42 afirmaciones)

Cómo solucionar el resumen del problema N + 1

Hacer uso de los ORM modernos es una excelente manera de hacer mucho en poco tiempo. La sintaxis que puede utilizar también es muy expresiva y relativamente fácil de entender. Sin embargo, debe tener en cuenta que es posible que esté activando demasiadas consultas SQL si la carga diferida está en uso. En este tutorial, aprendimos algunas formas de asegurarnos de que reducimos el número de consultas SQL mediante el uso de Eager Loading.