15 de mayo de 2024 • 4 min de lectura
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.
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.
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)
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á:
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
Descubre cuánto te está costando tu sistema de comunicaciones.
Obtén la auditoría de comunicaciones