DELETE en PHP
Eliminar registros de MySQL desde PHP es una operación delicada que requiere precauciones para evitar pérdidas de datos accidentales. PDO permite ejecutar sentencias DELETE con sentencias preparadas que garantizan la seguridad, y el manejo de transacciones te da la posibilidad de revertir cambios si algo sale mal. En este artículo aprenderás las diferentes técnicas de eliminación, incluyendo el borrado lógico y el manejo de restricciones de claves foráneas.
Requisitos previos
Necesitas una conexión PDO configurada y tablas con datos de prueba:
USE tienda;
CREATE TABLE IF NOT EXISTS productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
categoria VARCHAR(50) NOT NULL,
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
);
INSERT INTO productos (nombre, categoria, precio, stock) VALUES
('Laptop HP Pavilion', 'Computadoras', 12999.99, 25),
('Mouse Logitech MX Master', 'Periféricos', 1599.00, 150),
('Teclado Mecánico Corsair K70', 'Periféricos', 2299.50, 80),
('Monitor Samsung 27"', 'Monitores', 6499.00, 40),
('Auriculares Sony WH-1000XM5', 'Audio', 5999.99, 60);Código completo
Este ejemplo elimina un producto por su ID:
<?php
$pdo = new PDO(
'mysql:host=localhost;dbname=tienda;charset=utf8mb4',
'root', 'tu_contraseña',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
$productoId = 5;
$stmt = $pdo->prepare('DELETE FROM productos WHERE id = ?');
$stmt->execute([$productoId]);
if ($stmt->rowCount() > 0) {
echo "Producto con ID {$productoId} eliminado correctamente\n";
echo "Filas eliminadas: " . $stmt->rowCount() . "\n";
} else {
echo "No se encontró el producto con ID {$productoId}\n";
}Salida esperada:
Producto con ID 5 eliminado correctamente
Filas eliminadas: 1
Explicación paso a paso
La sentencia DELETE se ejecuta igual que cualquier otra sentencia preparada en PDO. El método rowCount() devuelve el número de filas eliminadas. Si devuelve 0, significa que no había ninguna fila que cumpliera la condición WHERE.
Es fundamental siempre usar sentencias preparadas con placeholders para los valores de la condición WHERE. Nunca concatenes variables directamente en la consulta SQL, ya que esto es la causa principal de vulnerabilidades por inyección SQL.
Eliminar con verificación previa
Una buena práctica es mostrar al usuario qué va a eliminarse antes de proceder:
<?php
function eliminarConConfirmacion(PDO $pdo, int $productoId): array {
// Verificar que existe
$stmt = $pdo->prepare('SELECT id, nombre, precio FROM productos WHERE id = ?');
$stmt->execute([$productoId]);
$producto = $stmt->fetch();
if (!$producto) {
return ['eliminado' => false, 'mensaje' => 'Producto no encontrado'];
}
// Eliminar
$stmt = $pdo->prepare('DELETE FROM productos WHERE id = ?');
$stmt->execute([$productoId]);
return [
'eliminado' => true,
'producto' => $producto,
'mensaje' => "Producto \"{$producto['nombre']}\" eliminado"
];
}
$resultado = eliminarConConfirmacion($pdo, 4);
echo $resultado['mensaje'] . "\n";Borrado lógico (soft delete)
El borrado lógico marca los registros como eliminados sin borrarlos realmente. Es el enfoque preferido en aplicaciones empresariales:
<?php
class ProductoRepository {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function softDelete(int $id): bool {
$stmt = $this->pdo->prepare(
'UPDATE productos SET activo = 0, eliminado_en = NOW() WHERE id = ? AND activo = 1'
);
$stmt->execute([$id]);
return $stmt->rowCount() > 0;
}
public function restaurar(int $id): bool {
$stmt = $this->pdo->prepare(
'UPDATE productos SET activo = 1, eliminado_en = NULL WHERE id = ? AND activo = 0'
);
$stmt->execute([$id]);
return $stmt->rowCount() > 0;
}
public function listarActivos(): array {
$stmt = $this->pdo->query(
'SELECT id, nombre, precio, stock FROM productos WHERE activo = 1 ORDER BY nombre'
);
return $stmt->fetchAll();
}
public function listarPapelera(): array {
$stmt = $this->pdo->query(
'SELECT id, nombre, eliminado_en FROM productos WHERE activo = 0 ORDER BY eliminado_en DESC'
);
return $stmt->fetchAll();
}
public function vaciarPapelera(int $diasLimite = 30): int {
$stmt = $this->pdo->prepare(
'DELETE FROM productos WHERE activo = 0 AND eliminado_en < DATE_SUB(NOW(), INTERVAL ? DAY)'
);
$stmt->execute([$diasLimite]);
return $stmt->rowCount();
}
}
// Uso
$repo = new ProductoRepository($pdo);
$repo->softDelete(3);
echo "Producto 3 movido a la papelera\n";
$papelera = $repo->listarPapelera();
echo "Productos en papelera: " . count($papelera) . "\n";
$repo->restaurar(3);
echo "Producto 3 restaurado\n";Eliminar múltiples registros
Para eliminar varios registros a la vez, genera los placeholders dinámicamente:
<?php
function eliminarPorIds(PDO $pdo, array $ids): int {
if (empty($ids)) return 0;
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("DELETE FROM productos WHERE id IN ({$placeholders})");
$stmt->execute($ids);
return $stmt->rowCount();
}
$eliminados = eliminarPorIds($pdo, [4, 5, 6]);
echo "{$eliminados} productos eliminados\n";Caso práctico
Veamos una implementación completa de eliminación de categoría con sus productos:
<?php
function eliminarCategoria(PDO $pdo, int $categoriaId, string $modo = 'soft'): array {
$pdo->beginTransaction();
try {
// Verificar que existe
$stmt = $pdo->prepare('SELECT id, nombre FROM categorias WHERE id = ?');
$stmt->execute([$categoriaId]);
$categoria = $stmt->fetch();
if (!$categoria) {
$pdo->rollBack();
return ['exito' => false, 'mensaje' => 'Categoría no encontrada'];
}
// Contar productos afectados
$stmt = $pdo->prepare('SELECT COUNT(*) FROM productos WHERE categoria_id = ? AND activo = 1');
$stmt->execute([$categoriaId]);
$totalProductos = (int) $stmt->fetchColumn();
if ($modo === 'soft') {
$stmt = $pdo->prepare(
'UPDATE productos SET activo = 0, eliminado_en = NOW() WHERE categoria_id = ? AND activo = 1'
);
$stmt->execute([$categoriaId]);
$stmt = $pdo->prepare('UPDATE categorias SET activa = 0 WHERE id = ?');
$stmt->execute([$categoriaId]);
} else {
$stmt = $pdo->prepare('DELETE FROM productos WHERE categoria_id = ?');
$stmt->execute([$categoriaId]);
$stmt = $pdo->prepare('DELETE FROM categorias WHERE id = ?');
$stmt->execute([$categoriaId]);
}
$pdo->commit();
return [
'exito' => true,
'categoria' => $categoria['nombre'],
'productos_afectados' => $totalProductos,
'modo' => $modo
];
} catch (PDOException $e) {
$pdo->rollBack();
return ['exito' => false, 'mensaje' => 'Error: ' . $e->getMessage()];
}
}Manejo de errores
Los errores más frecuentes al eliminar registros en PHP:
<?php
function eliminarSeguro(PDO $pdo, int $productoId): array {
try {
$stmt = $pdo->prepare('DELETE FROM productos WHERE id = ?');
$stmt->execute([$productoId]);
if ($stmt->rowCount() === 0) {
return ['error' => 'NOT_FOUND', 'mensaje' => 'Producto no encontrado'];
}
return ['exito' => true, 'eliminados' => $stmt->rowCount()];
} catch (PDOException $e) {
$codigo = $e->errorInfo[1];
switch ($codigo) {
case 1451:
return [
'error' => 'REFERENCIA',
'mensaje' => 'No se puede eliminar: otros registros dependen de este producto'
];
case 1205:
return [
'error' => 'TIMEOUT',
'mensaje' => 'El registro está bloqueado por otra operación'
];
case 1213:
return [
'error' => 'DEADLOCK',
'mensaje' => 'Interbloqueo detectado. Intenta de nuevo'
];
default:
return ['error' => 'INTERNO', 'mensaje' => "Error [{$codigo}]: {$e->getMessage()}"];
}
}
}En la siguiente sección veremos cómo conectar MySQL con Perl, un lenguaje con una larga tradición en el manejo de bases de datos.
Escrito por Eduardo Lázaro
