Schema::getColumnListing en Laravel: inserts sin romper

15 de mayo de 2024 • 4 min de lectura

Inicio / Blog / Schema::getColumnListing en Laravel: inserts sin romper

Sobre el autor

Author

Gonzalo Gomez

Especialista en IA y Automatización

Diseño sistemas de comunicación impulsados por IA. Mi trabajo se centra en agentes de voz, chatbots de WhatsApp, asistentes de IA y automatización de flujos, construidos principalmente sobre Twilio, n8n y LLM modernos como OpenAI y Claude. En los últimos 7 años he entregado más de 30 proyectos de automatización que manejan más de 250 000 interacciones mensuales.

Suscríbete a mi newsletter

Si te gusta el contenido que hago, puedes suscribirte y recibir información valiosa por correo. No se envía spam, solo novedades sobre publicaciones interesantes o contenido especializado del que hablo.

Schema::getColumnListing en Laravel: inserts sin romper | Cuando una API de terceros suma columnas, el insert masivo se rompe. Cómo usar Schema::getColumnListing en Laravel para filtrar las claves y evitarlo.

Introducción

En un post anterior hablé sobre usar queries de Eloquent o el DB Facade para hacer un insert masivo de registros, que podés leer acá. Hoy va el complemento: cómo Schema::getColumnListing evita que ese insert se rompa.

 

¿Pero qué pasa si estás insertando datos por chunks y esos datos, que vienen de una API de terceros, de golpe cambian de estructura y suman columnas nuevas? Sí, podemos discutir que es una mala práctica del proveedor, porque su API debería estar versionada. Igual es un problema con el que te vas a cruzar en tu día a día como Software Engineer (me pasó hoy, de ahí el post sobre cómo resolverlo)

 

Schema::getColumnListing() al rescate

El método getColumnListing(string $table) devuelve todas las columnas de una tabla en formato array. Eso es lo que nos va a servir para sacar las claves que no queremos de los registros que vamos a volcar en la DB. La comparación es simple: qué propiedades del objeto no están entre las columnas de la tabla. Veamos el código de ejemplo:

 

private function insertCallRecords(Collection $sessionRecords): void
{
    $recordsIds = $sessionRecords->pluck('SdrId');

    $dbRecords = SessionRecord::whereIn('SdrId', $recordsIds)->get();

    foreach($dbRecords ?? [] as $dbRecord) {
        // We will remove all the already existing records that are in DB to avoid double insertions
        $sessionRecords = $sessionRecords->filter(function ($callRecord) use ($dbRecord) {
            return $callRecord->SdrId != $dbRecord->SdrId;
        });
    }

    if ($sessionRecords->isNotEmpty()) {
        $sessionRecords = $sessionRecords->map(function ($record) {
            // The API in this case usually return values as an empty stdClass value, so we need to convert it to null so it doesnt break when inserting the rows in DB
            foreach ($record as $key => $value) {
                // Check if the value is an empty stdClass
                if ($value instanceof stdClass && count((array)$value) === 0) {
                    $record->$key = null;
                }
            }

            return (array) $record;
        });

        try {
            $recordsChunk = $sessionRecords->chunk(500);

            DB::beginTransaction();

            foreach ($recordsChunk as $chunk) {
                DB::table('session_records')->insert($chunk->all());
            }

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error("Something went wrong inserting the call records into the DB. Reason: " . $e->getMessage());
        }
    }
}

 

Este es un script de una aplicación real que inserta miles de registros cada 5 minutos. Vamos a desglosarlo en puntos para ver qué pasa acá:

 

  • El método recibe la colección de registros crudos que vienen de la API de terceros
  • Hacemos pluck del SdrId de los registros para traer los que la DB ya tiene
  • Filtramos los que ya están en la DB para insertar solo los nuevos
  • Sanitizamos algunos campos que vienen con formato incorrecto por cómo es la API de terceros
  • Partimos los datos en chunks y hacemos la inserción

 

La implementación funciona. Pero un día te levantás con un ticket de soporte que dice que los registros ya no se insertan, después de meses andando bien. Vas a debuggear y te encontrás con este log:

 

[2024-05-15 10:15:12] local.ERROR: Something went wrong inserting the call records into the DB. Reason: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'NewRecordKey' in 'field list'

 

Eso pasa porque nunca creamos esa columna: es un campo nuevo que empezó a mandar el proveedor de la API. Ahí tenés 2 opciones. La primera es crear la columna para que la inserción vuelva a funcionar. Es la más rápida, pero te vas a comer el mismo error de nuevo en el futuro.

 

La segunda opción es usar el método getListingColumn(). Así traemos los nombres de las columnas de la tabla e insertamos solo las claves y valores de los registros que coincidan con esas columnas:

 

private function insertCallRecords(Collection $sessionRecords)
{
    ...    

    if ($sessionRecords->isNotEmpty()) {
        // Here we store the column names to perform the rows key/values filtering
        $columns = Schema::getColumnListing('session_records');


        $sessionRecords = $sessionRecords->map(function ($record) use ($columns) {
            foreach ($record as $key => $value) {
                // If key doesnt exist in our column names, remove it from the record and move to the next one
                if (!in_array($key, $columns)) {
                    unset($record->$key);
                    continue;
                }
                
                // Check if the value is an empty stdClass
                if ($value instanceof stdClass && count((array)$value) === 0) {
                    $record->$key = null;
                }

            }

            return (array) $record;
        });

        ...
    }
}

 

Así nos aseguramos de que los registros se inserten según la estructura de la base. Si la API de terceros manda campos nuevos, la inserción no se rompe. Y si después necesitás usar esos datos nuevos, creás la columna en la base y mantenés esta lógica para evitar roturas futuras.

 

¿Te sirvió? ¡Suscribite a mi newsletter para no perderte los próximos posts! (Sin spam)

-Gonza

2743
Laravel
Publicado el 15 de mayo de 2024

Descubre cuánto te está costando tu sistema de comunicaciones.

Obtén la auditoría de comunicaciones

Posts relacionados

Cómo enviar SMS con Laravel y Twilio en producción

5 de abril de 2024
IntroducciónEn un capítulo anterior armé una guía con mis consejos para verificar tu número 10DLC en Twilio. Lo que nunca hice fue la guía de... Leer más

Cómo integrar Google Calendar con Laravel paso a paso

3 de septiembre de 2024
Cómo integrar Google Calendar en tu app LaravelIntegrar Google Calendar en tu aplicación Laravel te cambia el juego, sobre todo si necesitás manejar eventos, agendar... Leer más

Eloquent o DB Facade: cuál usar para inserts masivos

1 de mayo de 2024
IntroducciónEn Laravel, cómo resolvés las operaciones de base de datos impacta directo en el rendimiento y en la escalabilidad de la aplicación. Cuando tenés que... Leer más

Automatizar respuestas de WhatsApp con Twilio y Laravel

12 de mayo de 2025
IntroducciónTu equipo responde exactamente las mismas preguntas todos los días, por WhatsApp, mail y DMs: "¿Cuál es la dirección?""¿Abren los feriados?""¿Cuál es la política de devoluciones?""¿Dan... Leer más