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.

Publicado15 de mayo de 2024
Lectura4 min de lectura
CategoríaDesarrollo web
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

#Laravel 3177 visitas

¿Necesitás ayuda con una base de código Laravel o un desarrollo como este? Mirá mi servicio de consultoría Laravel.

Suscribite a mi newsletter

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

Gonzalo Gomez · Especialista en IA y Automatización

Posts relacionados