Categoría: Laravel

  • Laravel a escala: 5 trucos para consultar millones de registros

    Laravel a escala: 5 trucos para consultar millones de registros

    Este artículo resume cinco técnicas prácticas para consultar millones de filas en Laravel sin agotar memoria ni tiempo CPU. Cada técnica incluye una explicación breve y ejemplos de código listos para usar.

    Aplica estas prácticas en procesos de reporting, ETL o consultas masivas para mantener tiempos de respuesta y uso de recursos controlados.

    Introducción

    En consultas a tablas masivas, prácticas comunes como SELECT * o cargar todo con get() provocan consumo excesivo de RAM y CPU. Las siguientes cinco estrategias reducen uso de recursos y aceleran procesos.

    Prerrequisitos

    Antes de aplicar las técnicas: asegúrate de contar con un esquema de base de datos razonable y mecanismos de cacheo/colas si vas a usar preagregados o jobs en segundo plano.

    • Acceso al código de tus modelos/Eloquent.
    • Permiso para crear índices y migraciones en la base de datos.
    • Un sistema de cache (Redis, Memcached) y, si procede, soporte para jobs/colas.

    Desarrollo

    Procedimiento

    A continuación se presentan las cinco técnicas: seleccionar columnas esenciales, procesar por chunks o stream, indexar y optimizar WHERE, preagregar datos y cachear resultados pesados.

    1. Seleccionar sólo las columnas esenciales

    Evita SELECT * cuando sólo necesitas unas pocas columnas. Reducir columnas reduce CPU, memoria y ancho de banda.

    <?php
    // ❌ Evitar: carga todas las columnas
    $orders = Order::all();
    
    // ✅ Seleccionar sólo lo necesario
    $orders = Order::select('id', 'amount', 'created_at')
        ->whereBetween('created_at', [$start, $end])
        ->get();
    Lenguaje del código: PHP (php)

    En el ejemplo base, reducir de 20 columnas a 3 puede bajar el consumo de memoria de ~1.2 GB a ~200 MB en un conjunto grande de filas.

    2. Procesar por chunks o con cursor (stream)

    No uses get() para millones de filas. chunk() y cursor() procesan en partes y mantienen baja la memoria.

    <?php
    // Procesar en bloques de 10.000
    Order::where('status', 'completed')
        ->select('id', 'amount')
        ->chunk(10000, function ($orders) {
            foreach ($orders as $order) {
                // generar fila de reporte
            }
        });
    
    // O procesar con cursor para stream perezoso
    $orders = Order::cursor()->filter(fn ($order) => $order->amount > 1000);
    Lenguaje del código: PHP (php)

    En pruebas citadas, procesar 5M de filas con cursor/chunks reduce consumo de memoria a decenas de MB frente a cientos o más.

    3. Índices y cláusulas WHERE eficientes

    Filtros y ordenamientos sin índices provocan full table scans. Indexa columnas usadas en WHERE, JOIN y ORDER BY y evita patrones ineficientes como LIKE ‘%…%’.

    <?php
    // Ejemplo de migración para agregar índices
    Schema::table('orders', function (Blueprint $table) {
        $table->index('status');
        $table->index('created_at');
    });
    Lenguaje del código: PHP (php)

    Un ejemplo reportado mostró una reducción de tiempo de consulta de 120 s a 0.8 s tras indexar columnas utilizadas en filtros.

    4. Preagregar datos para reportes pesados

    Evita agrupar o sumar decenas de millones de filas en tiempo real. Precalcula resúmenes con jobs nocturnos o usa vistas/materialized views si la BD lo permite.

    <?php
    class GenerateOrderSummary implements ShouldQueue {
        public function handle() {
            $summary = Order::selectRaw('
                DATE(created_at) AS date,
                COUNT(*) AS total_orders,
                SUM(amount) AS revenue
            ')
            ->whereDate('created_at', today()->subDay())
            ->groupBy('date')
            ->first();
    
            Report::updateOrCreate(['date' => $summary->date], (array)$summary);
        }
    }
    Lenguaje del código: PHP (php)

    Con preagregados, la carga de un reporte puede pasar de minutos a decenas de milisegundos; en el ejemplo base, un reporte cargó en ~50 ms.

    5. Cachear consultas costosas

    Para consultas repetidas, guarda el resultado en cache con expiración y/o tags. Invalida o refresca la cache cuando los datos subyacentes cambien.

    <?php
    $report = Cache::remember("daily_sales_report:{$date}", 3600, fn() =>
        Order::whereDate('created_at', $date)
            ->selectRaw('COUNT(*) AS orders, SUM(amount) AS revenue')
            ->first()
    );
    Lenguaje del código: PHP (php)

    Usa Redis o Memcached según tu infraestructura y define políticas claras de invalidación para evitar datos obsoletos.

    Ejemplos

    Ejemplos concretos ya mostrados: selección de columnas, chunk/cursor, migración para índices, job para preagregados y cacheo con Cache::remember.

    Adapta los tamaños de chunk y la expiración del cache a la carga real y al patrón de acceso de tu sistema.

    Checklist

    1. Revisar consultas y eliminar SELECT *.
    2. Usar chunk() o cursor() para procesado masivo.
    3. Indexar columnas usadas en WHERE, JOIN y ORDER BY.
    4. Preagregar resultados pesados con jobs o vistas.
    5. Cachear consultas repetidas y definir estrategia de invalidación.

    Conclusión

    Combinando selección precisa de columnas, procesamiento por partes, buenos índices, preagregados y cacheo, puedes consultar millones de registros en Laravel con uso de recursos razonable y tiempos de respuesta aceptables.

    Empieza por los cambios menos invasivos (select, chunk, cache) y mide impacto antes de introducir cambios más estructurales como materialized views o rediseño de esquemas.

  • Evolución de las colas en Laravel: workers, deferred y background

    Evolución de las colas en Laravel: workers, deferred y background

    Este artículo resume la evolución del sistema de colas en Laravel: desde los workers tradicionales hasta el driver background basado en concurrencia. Explica usos, diferencias y ejemplos prácticos de implementación.

    Introducción

    Laravel ha transitado de un sistema de colas basado en workers persistentes a opciones que reducen la complejidad operativa: drivers deferred y background (impulsado por concurrencia).

    Prerrequisitos

    Antes de aplicar cualquiera de las estrategias descritas, confirma lo siguiente:

    • Proyecto Laravel con entorno PHP configurado.
    • Driver de colas definido (Redis, database, etc.) si planeas usar workers tradicionales.
    • En caso de background driver, la configuración de conexiones en config/queue.php debe incluir la conexión ‘background’ (ver ejemplo).

    Desarrollo

    Procedimiento

    A continuación se describen las tres aproximaciones principales y cómo elegir entre ellas según la carga y la tolerancia a fallos.

    1. Workers tradicionales: ejecutan jobs mediante procesos persistentes (p. ej. php artisan queue:work redis), adecuados para cargas pesadas y con control avanzado de reintentos y fallos.
    2. Driver deferred: ejecuta el job después de enviar la respuesta HTTP, sin worker. Adecuado para tareas ligeras y no críticas.
    3. Driver background (concurrencia): ejecuta jobs en procesos PHP separados, sin worker persistente; equilibrio entre robustez y simplicidad operativa.

    Ventajas y limitaciones resumidas:

    • Workers tradicionales: alto rendimiento y control; requieren DevOps para gestionar procesos.
    • Deferred: baja sobrecarga; limitado en reintentos y duración de tarea.
    • Background: sin workers dedicados, más robusto que deferred; útil para tareas de duración media.
    php artisan queue:work redis
    Lenguaje del código: Bash (bash)
    <?php
    return [
        'connections' => [
            'background' => [
                'driver' => 'background',
            ],
        ],
    ];
    Lenguaje del código: PHP (php)

    El ejemplo anterior muestra cómo declarar una conexión ‘background’ en config/queue.php. Su dispatch usa la conexión para ejecutar el job en otro proceso PHP.

    Ejemplos

    Ejemplo de clase Job que implementa ShouldQueue y se serializa para su ejecución en background o por un worker.

    <?php
    namespace App\Jobs;
    
    use Illuminate\Bus\Queueable;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Foundation\Bus\Dispatchable;
    use Illuminate\Queue\SerializesModels;
    
    class RecordDelivery implements ShouldQueue
    {
        use Dispatchable, Queueable, SerializesModels;
    
        public function __construct(public $order)
        {
        }
    
        public function handle()
        {
            // lógica de procesamiento
        }
    }
    Lenguaje del código: PHP (php)

    Dispatch desde un controlador usando la conexión background para evitar bloquear la petición HTTP.

    <?php
    use App\Jobs\RecordDelivery;
    
    class DeliveryController
    {
        public function store(Request $request)
        {
            $order = Order::create([...]);
            RecordDelivery::dispatch($order)->onConnection('background');
    
            return response()->json(['status' => 'processing'], 202);
        }
    }
    Lenguaje del código: PHP (php)

    Checklist

    1. Determina la criticidad de la tarea (alta → worker tradicional).
    2. Si la tarea es ligera y no crítica, considera deferred.
    3. Para equilibrio operativo (sin workers pero más robusto que deferred), usa el driver background.
    4. Configura reintentos, timeouts y monitoreo según el método elegido.

    Conclusión

    La evolución de las colas en Laravel ofrece alternativas para distintos perfiles de carga operativa. Elegir entre workers, deferred o background depende de la carga, la tolerancia a fallos y la capacidad operativa del equipo.

    Si buscas simplificar la operación sin renunciar a capacidad de procesamiento, el driver background —impulsado por concurrencia— es una opción intermedia interesante.

  • AsyncRequestContext en Laravel: reducir middleware con contexto

    AsyncRequestContext en Laravel: reducir middleware con contexto

    Este artículo explica cómo reemplazar middleware que solo propagan datos por petición con un contexto scoped (AsyncRequestContext) en Laravel. El objetivo: reducir la complejidad de rutas, mejorar la claridad de dependencias y facilitar pruebas unitarias.

    Incluye el diseño del contexto, el enlace en el service container y ejemplos de uso en acciones, controladores, rutas y pruebas.

    Introducción

    El patrón AsyncRequestContext propone un contenedor ligero por petición para almacenar estado que actualmente se inyecta en Illuminate\Http\Request mediante middleware.

    En lugar de ensuciar el Request con claves arbitrarias, se inicializa un contexto por ciclo de petición y se usa desde acciones y bindings del contenedor.

    Prerrequisitos

    Conocimientos básicos de Laravel: service container, providers, uso de acciones/servicios y rutas. No se requiere configuración adicional en el framework más allá de registrar el provider.

    Desarrollo

    La idea central es exponer un RequestContext scoped que se inicializa al enlazar en el container y se limpia en termination. Evita ‘request pollution’ y hace explícitas las dependencias.

    <?php
    namespace App\Context;
    
    class RequestContext
    {
        private static ?array $data = null;
    
        public static function initialize(): void
        {
            self::$data = [];
        }
    
        public static function set(string $key, mixed $value): void
        {
            self::$data[$key] = $value;
        }
    
        public static function get(string $key): mixed
        {
            return self::$data[$key] ?? null;
        }
    
        public static function flush(): void
        {
            self::$data = null;
        }
    }
    Lenguaje del código: PHP (php)

    En el service provider se enlaza RequestContext para inicializarlo por petición y se registra un hook de terminating para limpiar el estado.

    <?php
    namespace App\Providers;
    
    use Illuminate\Support\ServiceProvider;
    use App\Context\RequestContext;
    
    class ContextServiceProvider extends ServiceProvider
    {
        public function boot()
        {
            $this->app->bind(RequestContext::class, function () {
                RequestContext::initialize();
                return new RequestContext();
            });
    
            $this->app->terminating(function () {
                RequestContext::flush();
            });
        }
    }
    Lenguaje del código: PHP (php)

    Procedimiento

    En lugar de usar middleware que sólo añade datos al Request, crea acciones que reciben el Request y escriben en el RequestContext. Eso permite invocarlas donde y cuando hagan falta.

    <?php
    namespace App\Actions;
    
    use Illuminate\Http\Request;
    use App\Context\RequestContext;
    use App\Models\Tenant;
    
    class ResolveTenant
    {
        public function __construct(private RequestContext $context) {}
    
        public function execute(Request $request): void
        {
            $tenant = Tenant::fromDomain($request->host());
            $this->context->set('tenant', $tenant);
        }
    }
    Lenguaje del código: PHP (php)

    Consume el contexto explícitamente en controladores o servicios, lo que revela dependencias y facilita pruebas unitarias.

    <?php
    namespace App\Http\Controllers;
    
    use App\Context\RequestContext;
    
    class ReportController
    {
        public function index(RequestContext $context)
        {
            $tenant = $context->get('tenant');
            return $tenant->reports()->paginate();
        }
    }
    Lenguaje del código: PHP (php)

    Las rutas pueden simplificarse ejecutando acciones de resolución dentro de la closure y pasando el contexto al controlador.

    <?php
    use App\Actions\ResolveTenant;
    use App\Context\RequestContext;
    use App\Http\Controllers\ReportController;
    use Illuminate\Support\Facades\Route;
    
    Route::get('/reports', function (
        ResolveTenant $resolve,
        RequestContext $context
    ) {
        $resolve->execute(request());
        return (new ReportController)->index($context);
    })->middleware('auth:api');
    Lenguaje del código: PHP (php)

    Con este enfoque mantienes middleware para filtrado (autenticación, throttling) y usas acciones/contexto para propagar estado.

    Ejemplos

    Ejemplo de binding contextual que elige una implementación según un valor en RequestContext.

    <?php
    // En AppServiceProvider.php
    $this->app->bind(ReportExporter::class, function () {
        $format = RequestContext::get('export_format') ?? 'csv';
        return match ($format) {
            'excel' => new ExcelExporter,
            'pdf'   => new PDFExporter,
            default => new CSVExporter,
        };
    });
    Lenguaje del código: PHP (php)

    Prueba unitaria sin levantar el HTTP layer inicializando el contexto manualmente:

    <?php
    test('exports tenant reports in excel', function () {
        RequestContext::initialize();
        RequestContext::set('tenant', Tenant::factory()->create());
        RequestContext::set('export_format', 'excel');
    
        $exporter = app(ReportExporter::class);
        $this->assertInstanceOf(ExcelExporter::class, $exporter);
    });
    Lenguaje del código: PHP (php)

    Checklist

    1. Auditar middleware que solo escriben datos en el Request.
    2. Refactorizar esas responsabilidades en acciones (ResolveX classes).
    3. Sustituir $request->get(‘foo’) por $context->get(‘foo’) donde corresponda.
    4. Reducir arrays de middleware en rutas dejando filtros reales (auth, throttle).
    5. Asegurar que RequestContext::flush() se ejecuta en terminating.

    Conclusión

    AsyncRequestContext ofrece una alternativa práctica al uso excesivo de middleware para propagar estado por petición. Mejora la claridad, facilita pruebas y mantiene middleware para filtrado.

    Middleware es para filtrado, no para propagar datos. Deja que el contexto lleve tu estado cross‑cutting.