DELETE en Node.js
Eliminar registros de la base de datos requiere especial cuidado porque, a diferencia de las actualizaciones, los datos borrados no se pueden recuperar fácilmente. En este artículo aprenderás a ejecutar sentencias DELETE de forma segura desde Node.js, implementar el patrón de borrado lógico (soft delete) como alternativa y considerar los efectos en cascada sobre registros relacionados.
Requisitos previos
Necesitas un pool de conexiones configurado y la tabla productos con datos. Además, para los ejemplos de borrado en cascada, necesitarás tablas relacionadas:
USE tienda;
CREATE TABLE IF NOT EXISTS categorias (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(50) NOT NULL UNIQUE,
activa TINYINT(1) DEFAULT 1
);
CREATE TABLE IF NOT EXISTS productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
categoria_id INT,
precio DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0,
activo TINYINT(1) DEFAULT 1,
eliminado_en DATETIME DEFAULT NULL,
fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (categoria_id) REFERENCES categorias(id) ON DELETE SET NULL
);
INSERT INTO categorias (nombre) VALUES ('Computadoras'), ('Periféricos'), ('Audio');
INSERT INTO productos (nombre, categoria_id, precio, stock) VALUES
('Laptop HP Pavilion', 1, 12999.99, 25),
('Mouse Logitech MX Master', 2, 1599.00, 150),
('Auriculares Sony WH-1000XM5', 3, 5999.99, 60);Código completo
Este ejemplo elimina un producto por su ID y verifica el resultado:
const mysql = require('mysql2/promise');
async function eliminarProducto() {
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'tu_contraseña',
database: 'tienda'
});
const productoId = 3;
const [result] = await pool.execute(
'DELETE FROM productos WHERE id = ?',
[productoId]
);
if (result.affectedRows > 0) {
console.log(`Producto con ID ${productoId} eliminado correctamente`);
console.log('Filas eliminadas:', result.affectedRows);
} else {
console.log(`No se encontró el producto con ID ${productoId}`);
}
await pool.end();
}
eliminarProducto().catch(console.error);Salida esperada:
Producto con ID 3 eliminado correctamente
Filas eliminadas: 1
Explicación paso a paso
La sentencia DELETE funciona de forma similar a UPDATE en cuanto a la estructura del resultado. La propiedad affectedRows indica cuántas filas se eliminaron. Si el valor es 0, significa que ninguna fila cumplió la condición WHERE, lo que normalmente indica que el registro no existía o ya había sido eliminado.
Es fundamental incluir siempre una condición WHERE en las sentencias DELETE. Un DELETE sin WHERE eliminará todos los registros de la tabla, lo cual casi nunca es el comportamiento deseado. El uso de sentencias preparadas con parámetros ? no solo protege contra inyecciones SQL sino que también hace más difícil cometer el error de ejecutar un DELETE sin condición.
Borrado lógico (soft delete)
En muchas aplicaciones empresariales, eliminar datos de forma permanente no es aceptable por razones legales, auditoría o la posibilidad de que el usuario necesite recuperar la información. El borrado lógico consiste en marcar un registro como eliminado sin borrarlo realmente:
async function softDelete(productoId) {
const [result] = await pool.execute(
'UPDATE productos SET activo = 0, eliminado_en = NOW() WHERE id = ? AND activo = 1',
[productoId]
);
if (result.affectedRows === 0) {
return { eliminado: false, mensaje: 'Producto no encontrado o ya eliminado' };
}
return { eliminado: true };
}
async function restaurar(productoId) {
const [result] = await pool.execute(
'UPDATE productos SET activo = 1, eliminado_en = NULL WHERE id = ? AND activo = 0',
[productoId]
);
if (result.affectedRows === 0) {
return { restaurado: false, mensaje: 'Producto no encontrado o ya activo' };
}
return { restaurado: true };
}
// Todas las consultas SELECT deben filtrar por activo = 1
async function listarProductos() {
const [rows] = await pool.execute(
'SELECT id, nombre, precio FROM productos WHERE activo = 1 ORDER BY nombre'
);
return rows;
}
// Ver papelera de reciclaje
async function listarEliminados() {
const [rows] = await pool.execute(
'SELECT id, nombre, eliminado_en FROM productos WHERE activo = 0 ORDER BY eliminado_en DESC'
);
return rows;
}Eliminar múltiples registros
Para eliminar varios registros que cumplan cierta condición, puedes usar operadores como IN o condiciones compuestas:
async function eliminarPorIds(ids) {
if (ids.length === 0) return { eliminados: 0 };
const placeholders = ids.map(() => '?').join(', ');
const [result] = await pool.query(
`DELETE FROM productos WHERE id IN (${placeholders})`,
ids
);
console.log(`${result.affectedRows} productos eliminados de ${ids.length} solicitados`);
return { eliminados: result.affectedRows };
}
await eliminarPorIds([4, 5, 6]);Salida esperada:
3 productos eliminados de 3 solicitados
Caso práctico
Veamos un caso completo donde se implementa una función de eliminación que maneja relaciones entre tablas:
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'tu_contraseña',
database: 'tienda'
});
async function eliminarCategoria(categoriaId, modo = 'soft') {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// Verificar que la categoría existe
const [categorias] = await connection.execute(
'SELECT id, nombre FROM categorias WHERE id = ?',
[categoriaId]
);
if (categorias.length === 0) {
throw new Error('Categoría no encontrada');
}
// Contar productos asociados
const [conteo] = await connection.execute(
'SELECT COUNT(*) AS total FROM productos WHERE categoria_id = ? AND activo = 1',
[categoriaId]
);
const totalProductos = conteo[0].total;
if (modo === 'soft') {
await connection.execute(
'UPDATE productos SET activo = 0, eliminado_en = NOW() WHERE categoria_id = ? AND activo = 1',
[categoriaId]
);
await connection.execute(
'UPDATE categorias SET activa = 0 WHERE id = ?',
[categoriaId]
);
} else if (modo === 'hard') {
await connection.execute(
'DELETE FROM productos WHERE categoria_id = ?',
[categoriaId]
);
await connection.execute(
'DELETE FROM categorias WHERE id = ?',
[categoriaId]
);
}
await connection.commit();
console.log(`Categoría "${categorias[0].nombre}" eliminada (modo: ${modo})`);
console.log(`Productos afectados: ${totalProductos}`);
return { exito: true, productosAfectados: totalProductos };
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}Manejo de errores
Los errores más frecuentes al eliminar registros están relacionados con las restricciones de claves foráneas:
async function eliminarSeguro(productoId) {
try {
const [result] = await pool.execute(
'DELETE FROM productos WHERE id = ?',
[productoId]
);
if (result.affectedRows === 0) {
return { error: 'NOT_FOUND', mensaje: 'Registro no encontrado' };
}
return { exito: true, eliminados: result.affectedRows };
} catch (error) {
switch (error.code) {
case 'ER_ROW_IS_REFERENCED_2':
return {
error: 'REFERENCIA',
mensaje: 'No se puede eliminar porque otros registros dependen de este'
};
case 'ER_LOCK_WAIT_TIMEOUT':
return {
error: 'TIMEOUT',
mensaje: 'El registro está bloqueado por otra operación'
};
case 'ER_LOCK_DEADLOCK':
return {
error: 'DEADLOCK',
mensaje: 'Se detectó un interbloqueo. Intenta de nuevo'
};
default:
console.error('Error inesperado al eliminar:', error);
return { error: 'INTERNO', mensaje: 'Error interno del servidor' };
}
}
}Ahora que conoces las diferentes formas de eliminar datos, en el siguiente artículo aprenderás a llamar procedimientos almacenados de MySQL desde Node.js.
Escrito por Eduardo Lázaro
