Entrevista Laravel: diagnostica consultas N+1, validación y colas

Practica una entrevista Laravel con 39 comprobaciones: mide N+1, conserva las claves de Eloquent y revisa validación, afterCommit y entregas duplicadas.

Author: PracHub

Published: 10/11/2026

Entrevista Laravel: diagnostica consultas N+1, validación y colas

October 11, 2026

Quick Overview

Laravel13.35.0 PHP8.5.11 SQLite3.53.4 real39checks: fourtickets repeated/nullauthor lazy4eager2, missingFKallnullbutloaded, growth1/10/40 lazy2/11/41eager2. StandaloneFormRequest3invalid/no writes/extrafielddiscard; actualSyncQueueaftercommit rollbackdiscard/commitoutsideTX/explicitduplicatehandler2uniqueDBrow1/postcommitcallbackthrowticketremains. NoHTTPkernel/broker/worker/concurrency/mail/durableoutboxproof.

Backend EngineerFree

Una lista de tickets puede ejecutar pocas consultas y devolver nombres incorrectos. Una operación puede validar bien sus datos y fallar al enviar el trabajo posterior. Para preparar una entrevista Laravel, conviene seguir las tres fronteras por separado: qué se lee, qué se permite escribir y qué ocurre después del commit.

Este artículo propone un laboratorio original con cuatro tickets, dos autores y una notificación registrada en base de datos. Lo ejecutamos con Laravel 13.35.0 y comprobamos 39 resultados. Para profundizar en consultas de relaciones, tienes también la guía de Eloquent y N+1 de PracHub; aquí conectaremos esa lectura con Form Request y tareas posteriores a la transacción.

Límite de evidencia. La documentación oficial respalda los mecanismos citados. Las cifras proceden de esta fixture, y los consejos de entrevista son inferencias de preparación. Las preguntas públicas de PracHub son material reportado para practicar, no una garantía de que una empresa utilice Laravel o repita el ejercicio. No afirmamos rondas, tiempos ni criterios de contratación basándonos en testimonios de candidatos.

Tres fronteras de un ticket: leer relaciones, validar la entrada y observar el trabajo posterior al commit.

Define el resultado antes de contar consultas

El contrato inicial es pequeño: devolver cuatro tickets ordenados por ID, con el nombre del autor o null. Los tickets 1 y 2 pertenecen a Ana; el 3, a Luis; el 4 no tiene autor. El resultado esperado es [Ana, Ana, Luis, null].

Dos tickets de Ana permiten comprobar si compartir un autor evita realmente repetir su consulta. Si tu explicación supone que cada autor compartido se consulta una sola vez automáticamente, el log permitirá contrastarla. El autor nulo también importa: evita aplicar mecánicamente la fórmula «filas más una» sin mirar las claves que contiene la fixture.

Al recibir un caso parecido en entrevista, pregunta primero si una relación vacía es válida y si deben conservarse esas filas. En nuestro contrato sí. Una modificación que elimina el ticket sin autor rompe el resultado aunque reduzca las consultas. Después concreta qué campos necesita el consumidor; cargar todos los datos de cada autor tampoco es una solución universal.

N+1: observa la expresión que cruza la relación

Hecho oficial. La documentación de relaciones de Eloquent distingue cargar una relación al acceder a su propiedad y solicitarla mediante eager loading. El problema se encuentra siguiendo el acceso real al modelo, no contando únicamente las llamadas escritas en el controlador.

En la fixture, obtenemos los tickets y leemos $ticket->author?->name. El log registra cuatro consultas: una para los tickets y tres para los autores no nulos. Los dos modelos que apuntan a Ana generan accesos separados en este recorrido lazy. El ticket con author_id nulo no añade esa consulta.

Con eager loading, el código es:

$tickets = Ticket::with('author:id,name')
    ->orderBy('id')
    ->get();

La variante con author:id,name conserva [Ana, Ana, Luis, null] y el log registra dos consultas. Las cifras describen esta relación belongsTo, estos modelos y SQLite local. No son una garantía de dos consultas para cualquier anidación, relación polimórfica, scope o accesor de una aplicación.

Para distinguir una coincidencia pequeña de un patrón, el laboratorio repite la lectura con 1, 10 y 40 tickets, todos del mismo autor. Lazy registra 2, 11 y 41 consultas; eager, 2 en cada caso. No medimos tiempos: la conclusión es crecimiento de consultas, no una afirmación de que la aplicación sea cuarenta veces más rápida.

Inferencia práctica: cuando depures, observa también el Resource o transformador que prepara la respuesta. Una consulta de controlador puede convertirse en muchas al leer propiedades después. En esta fixture usamos map para mantener visible el recorrido; no hemos ejecutado un Resource HTTP completo.

Una relación marcada como cargada también puede estar mal

Reducir columnas parece una mejora razonable hasta que se elimina una clave necesaria. Ejecutamos esta variante:

Ticket::select('id', 'subject')
    ->with('author:id,name')
    ->orderBy('id')
    ->get();

La consulta principal omitió author_id. En el resultado observado, los cuatro nombres son null y relationLoaded('author') devuelve true para todos los tickets. relationLoaded confirma el estado de carga de la relación; los cuatro null muestran que ese estado puede coexistir con un resultado incorrecto.

Al restaurar author_id en select, reaparecen [Ana, Ana, Luis, null] y se registran dos consultas. También conservamos id en la selección del autor, porque es la clave usada para emparejar los modelos de esta relación. No basta con que el nombre esté presente en el SELECT de la tabla relacionada.

Usa ese contraejemplo para revisar qué comprueba tu optimización. Primero compara valores y filas, después el número de consultas, y finalmente el tamaño o coste que necesites medir. Una prueba que solo revise relationLoaded puede pasar con un resultado vacío incorrecto.

En una respuesta breve: «Localizo la propiedad que provoca la lectura, conservo las claves de emparejamiento y verifico el payload antes y después». Esa frase explica una decisión comprobable y deja espacio para discutir paginación, índices o autorización cuando el caso los requiera.

Form Request: delimita la entrada antes de escribir

Hecho oficial. Laravel permite agrupar autorización y reglas en Form Requests. El laboratorio instancia StoreTicketRequest, ejecuta validateResolved y obtiene validated antes de crear el ticket. Es una validación real del framework en un harness; no ejecutamos el kernel HTTP ni afirmamos haber comprobado una respuesta 422.

Las reglas del caso exigen subject como string no vacío, con máximo de 80 caracteres. author_id es nullable, integer y debe existir en authors cuando se proporciona. authorize devuelve true deliberadamente para aislar la validación: no representa una política de permisos lista para producción.

Probamos tres entradas inválidas: asunto vacío, autor 999 inexistente y asunto de 81 caracteres. Cada una produce ValidationException con errores y deja el número de tickets sin cambios. La operación que escribe se encuentra después de validar; no se alcanza en esas ramas.

Una entrada válida incluye author_id nulo y un campo extra is_admin. validated devuelve únicamente subject y author_id. Eso permite mostrar qué datos recibe la escritura, pero no demuestra por sí solo que toda la aplicación sea segura: aquí el modelo tiene guarded vacío y confiamos explícitamente en pasar el conjunto validado de esta ruta.

Inferencia de revisión: explica por separado validación, autorización y asignación de atributos. Que un ID exista no implica que el usuario pueda utilizarlo. Añadir pruebas de acceso entre usuarios o tenants sería un siguiente paso; no atribuyas esas garantías a los tres rechazos de esta fixture.

afterCommit: comprueba cuándo se ejecuta el handler

Hecho oficial. La documentación de colas describe el despacho después de confirmar transacciones. El laboratorio configura una conexión sync con after_commit=true y un job con afterCommit=true. Al usar SyncQueue, ejecutamos callbacks reales de Laravel en el mismo proceso; no hay Redis, broker ni worker independiente.

Dentro de la transacción creamos un ticket y enviamos RecordNotification. Antes de commit, el contador de ejecuciones del job sigue en cero. Si lanzamos la excepción de la rama rollback, no queda ticket, no hay notificación y el handler no se ejecuta. La intención pendiente se descarta en ese recorrido.

En la rama de éxito, el handler se ejecuta después de commit. Registra transaction_level igual a cero, encuentra el ticket y añade una fila notifications. Observar únicamente «el ticket existe» en la misma conexión sería insuficiente para explicar el momento; por eso también registramos el nivel transaccional.

Rama controladaTickets tras terminarFilas de notificaciónEjecución observada
Excepción antes de commit00Handler no ejecutado
Commit correcto11Handler fuera de la transacción
Segunda entrega del mismo evento11Handler ejecutado otra vez
Nuevo ticket; callback falla tras commit21Error al caller; ticket conservado

La tabla sigue el orden del programa. Cada cifra pertenece al estado acumulado de la fixture en esa fila. No son cuatro escenarios reiniciados con la misma cantidad inicial.

Rollback descarta el trabajo pendiente; commit permite ejecutarlo, pero un fallo posterior no deshace el ticket.

Duplicados y fallos posteriores: separa dos garantías

Invocamos de nuevo el mismo job con la misma event_key. El recibo registra dos ejecuciones del handler y una fila notifications: insertOrIgnore junto al índice unique evita otra fila con la misma event_key. La prueba demuestra idempotencia de esa escritura local bajo dos entregas secuenciales explícitas.

No demuestra que se haya enviado un único correo. No hay proveedor de email ni otro efecto externo en el programa. Si el handler enviara un mensaje antes de intentar la inserción, el índice no borraría el mensaje ya enviado. Tampoco hemos reproducido concurrencia entre workers, un retry automático ni una caída del proceso.

La última rama crea otro ticket y provoca un fallo en el callback posterior a commit, antes de insertar su notificación. El caller recibe la excepción, pero el ticket ya está confirmado: quedan dos tickets y una notificación. Este resultado evita explicar cualquier error observado por el caller como prueba de rollback.

Inferencia de diseño: si la entrega del evento debe sobrevivir a una caída entre commit y envío, considera una intención durable, como un outbox transaccional, y diseña quién la entrega y cómo reconoce duplicados. El laboratorio no implementa ese outbox. afterCommit resuelve un momento de ejecución; por sí solo no cierra todas las ventanas de pérdida ni ofrece exactamente una vez.

Usa el recibo para practicar una explicación defendible

Descarga el laboratorio Laravel. Incluye código, composer.json, composer.lock y results.json. Requiere PHP con pdo_sqlite y mbstring, además de Composer. Ejecuta composer install --no-interaction y después php lab.php; vendor se descarga localmente y queda fuera del ZIP.

La ejecución registrada utilizó PHP 8.5.11, Laravel 13.35.0 y SQLite 3.53.4 en memoria. Las 39 comprobaciones guardan valores observados y esperados. Los logs SQL proceden de datos sintéticos de esta fixture; no incluyen consultas de producción ni credenciales.

Antes de mirar results.json, escribe tus tres predicciones; así podrás localizar el supuesto que falló. Predice los nombres cuando falta author_id, la cantidad de consultas con cuarenta tickets y el estado después del callback fallido. Luego compara tu explicación con la ejecución. Si el resultado sorprende, localiza qué frontera habías supuesto: carga, validación, commit o efecto posterior.

La base en memoria no prueba persistencia tras reiniciar ni bloqueos de MySQL o PostgreSQL. El harness tampoco comprueba autenticación HTTP. Mantén esos límites cerca del resultado, especialmente si propones llevar la misma estructura a un proyecto real.

Cinco preguntas relacionadas para ampliar el caso

Material reportado de práctica. Estas preguntas verificadas de PracHub tratan contratos backend transferibles. No afirmamos que estén escritas para Laravel ni que las empresas exijan este framework.

Pregunta completaQué debes explicar
Build a Queue Worker with Backoff and a Dead-Letter QueueQué añadirías al harness para observar fallos y recuperación de un worker real.
At-Least-Once Job Queue with Timeouts, Retries, a Dead-Letter Queue and DependenciesPor qué recibir otra entrega exige reconocer efectos ya aplicados.
Many-to-Many Schema Design With a Join Table, Composite Primary Key, and QueriesCómo conservar claves de emparejamiento al seleccionar columnas.
Production-Ready Event Ingestion Handler: Validation, Idempotent Writes, DLQ, MetricsDónde termina la validación y qué escritura resiste una repetición.
Implement an Idempotent Versioned Database UpdateQué garantiza una clave y qué conflictos requieren otra regla.

Como siguiente práctica, abre el handler de ingestión de eventos. Define una entrada inválida, una repetición y un error posterior a commit. Para cada caso, predice tanto el error recibido como las filas que permanecen. Después contrasta tus predicciones con una prueba: distinguir error y estado persistido no garantiza por sí solo entrega durable ni recuperación.

Sources and Further Reading


Comments (0)