UPDATE en Node.js
Actualizar registros existentes es una operación esencial en cualquier aplicación que gestione datos. En Node.js con mysql2, la sentencia UPDATE se ejecuta de forma similar a INSERT, utilizando sentencias preparadas para garantizar la seguridad. En este artículo aprenderás cómo actualizar uno o varios registros, verificar si realmente se modificaron datos y construir actualizaciones dinámicas basadas en los campos que el usuario desee cambiar.
Requisitos previos
Necesitas un pool de conexiones configurado y la tabla productos con datos. Si seguiste los artículos anteriores, ya deberías tener todo listo. De lo contrario, consulta el artículo de conexión para configurar el pool.
Código completo
Este ejemplo actualiza el precio y el stock de un producto específico:
const mysql = require('mysql2/promise');
async function actualizarProducto() {
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'tu_contraseña',
database: 'tienda'
});
const [result] = await pool.execute(
'UPDATE productos SET precio = ?, stock = ? WHERE id = ?',
[13499.99, 30, 1]
);
console.log('Filas encontradas:', result.affectedRows);
console.log('Filas modificadas:', result.changedRows);
console.log('Mensaje:', result.info);
await pool.end();
}
actualizarProducto().catch(console.error);Salida esperada:
Filas encontradas: 1
Filas modificadas: 1
Mensaje: Rows matched: 1 Changed: 1 Warnings: 0
Explicación paso a paso
El resultado de una sentencia UPDATE incluye dos propiedades especialmente importantes. La propiedad affectedRows indica cuántas filas cumplieron la condición WHERE, mientras que changedRows indica cuántas de esas filas realmente se modificaron. Estas dos cifras pueden diferir: si actualizas un campo con el mismo valor que ya tenía, affectedRows será 1 pero changedRows será 0.
Esta distinción es útil para dar retroalimentación precisa al usuario. Si affectedRows es 0, significa que no se encontró ningún registro con esa condición. Si affectedRows es mayor que 0 pero changedRows es 0, el registro existe pero ya tenía los valores indicados.
Verificar si la actualización tuvo efecto
async function actualizarPrecio(productoId, nuevoPrecio) {
const [result] = await pool.execute(
'UPDATE productos SET precio = ? WHERE id = ?',
[nuevoPrecio, productoId]
);
if (result.affectedRows === 0) {
console.log(`No se encontró el producto con ID ${productoId}`);
return false;
}
if (result.changedRows === 0) {
console.log('El producto ya tenía ese precio');
return false;
}
console.log(`Precio del producto ${productoId} actualizado a $${nuevoPrecio}`);
return true;
}Actualización dinámica
En muchas aplicaciones, el usuario puede modificar solo algunos campos de un registro. Construir una consulta UPDATE dinámica que solo actualice los campos proporcionados es una práctica habitual:
async function actualizarCampos(productoId, campos) {
const columnasPermitidas = ['nombre', 'categoria', 'precio', 'stock', 'activo'];
const sets = [];
const valores = [];
for (const [campo, valor] of Object.entries(campos)) {
if (columnasPermitidas.includes(campo)) {
sets.push(`${campo} = ?`);
valores.push(valor);
}
}
if (sets.length === 0) {
console.log('No se proporcionaron campos válidos para actualizar');
return null;
}
valores.push(productoId);
const [result] = await pool.execute(
`UPDATE productos SET ${sets.join(', ')} WHERE id = ?`,
valores
);
return {
encontrado: result.affectedRows > 0,
modificado: result.changedRows > 0
};
}
// Uso: actualizar solo precio y stock
const resultado = await actualizarCampos(1, {
precio: 11999.99,
stock: 50
});
console.log(resultado);Salida esperada:
{ encontrado: true, modificado: true }
Este enfoque valida los nombres de las columnas contra una lista de campos permitidos para evitar que un atacante pueda inyectar nombres de columnas arbitrarios.
Actualización condicional con CASE
Cuando necesitas actualizar múltiples registros con valores diferentes en una sola consulta, puedes usar la expresión CASE de MySQL:
async function actualizarPrecios(actualizaciones) {
// actualizaciones = [{ id: 1, precio: 12500 }, { id: 2, precio: 1650 }, ...]
if (actualizaciones.length === 0) return;
const ids = actualizaciones.map(a => a.id);
const cases = actualizaciones.map(() => 'WHEN id = ? THEN ?').join(' ');
const params = [];
actualizaciones.forEach(a => {
params.push(a.id, a.precio);
});
params.push(...ids);
const placeholders = ids.map(() => '?').join(', ');
const [result] = await pool.query(
`UPDATE productos SET precio = CASE ${cases} END WHERE id IN (${placeholders})`,
params
);
console.log(`${result.changedRows} precios actualizados de ${actualizaciones.length} solicitados`);
}
await actualizarPrecios([
{ id: 1, precio: 12500.00 },
{ id: 2, precio: 1650.00 },
{ id: 3, precio: 2399.00 }
]);Salida esperada:
3 precios actualizados de 3 solicitados
Caso práctico
Veamos un caso real: una función para actualizar el perfil de un usuario que solo modifica los campos que cambiaron y registra un log de auditoría:
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'tu_contraseña',
database: 'tienda'
});
async function actualizarPerfil(usuarioId, cambios) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// Obtener valores actuales para el log
const [antes] = await connection.execute(
'SELECT nombre, email, telefono FROM usuarios WHERE id = ?',
[usuarioId]
);
if (antes.length === 0) {
throw new Error(`Usuario ${usuarioId} no encontrado`);
}
// Construir UPDATE dinámico
const camposPermitidos = ['nombre', 'email', 'telefono', 'direccion'];
const sets = [];
const valores = [];
for (const [campo, valor] of Object.entries(cambios)) {
if (camposPermitidos.includes(campo) && antes[0][campo] !== valor) {
sets.push(`${campo} = ?`);
valores.push(valor);
}
}
if (sets.length === 0) {
await connection.rollback();
return { modificado: false, mensaje: 'No hay cambios que aplicar' };
}
valores.push(usuarioId);
await connection.execute(
`UPDATE usuarios SET ${sets.join(', ')}, fecha_actualizacion = NOW() WHERE id = ?`,
valores
);
// Registrar en log de auditoría
await connection.execute(
'INSERT INTO audit_log (tabla, registro_id, accion, detalles, fecha) VALUES (?, ?, ?, ?, NOW())',
['usuarios', usuarioId, 'UPDATE', JSON.stringify({ antes: antes[0], despues: cambios })]
);
await connection.commit();
return { modificado: true, campos: sets.length };
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}Manejo de errores
Las actualizaciones pueden fallar por violaciones de restricciones, datos incorrectos o problemas de concurrencia:
async function actualizarSeguro(productoId, datos) {
try {
const [result] = await pool.execute(
'UPDATE productos SET nombre = ?, precio = ? WHERE id = ?',
[datos.nombre, datos.precio, productoId]
);
if (result.affectedRows === 0) {
return { error: 'NOT_FOUND', mensaje: 'Producto no encontrado' };
}
return { exito: true, modificado: result.changedRows > 0 };
} catch (error) {
switch (error.code) {
case 'ER_DUP_ENTRY':
return { error: 'DUPLICADO', mensaje: 'Ya existe un producto con ese nombre' };
case 'ER_DATA_TOO_LONG':
return { error: 'LONGITUD', mensaje: 'El nombre excede el límite de caracteres' };
case 'ER_TRUNCATED_WRONG_VALUE_FOR_FIELD':
return { error: 'TIPO', mensaje: 'El precio debe ser un número válido' };
case 'ER_CHECK_CONSTRAINT_VIOLATED':
return { error: 'RESTRICCION', mensaje: 'El valor no cumple las restricciones de la tabla' };
case 'ER_LOCK_WAIT_TIMEOUT':
return { error: 'TIMEOUT', mensaje: 'El registro está siendo modificado por otro proceso' };
default:
console.error('Error inesperado al actualizar:', error);
return { error: 'INTERNO', mensaje: 'Error interno del servidor' };
}
}
}Ahora que conoces las técnicas para actualizar registros, en el siguiente artículo aprenderás a eliminar datos de MySQL desde Node.js.
Escrito por Eduardo Lázaro
