Diferencia entre revisiones de «PHP UD3 TRANSACCIONS»
| Línea 1: | Línea 1: | ||
| − | == | + | == Introdución == |
| − | + | En certas ocasións unha operación da aplicación require realizar varias modificacións relacionadas na base de datos. | |
| + | Por exemplo: | ||
| + | |||
| + | :* dar de alta un libro e varios exemplares; | ||
| + | :* asociar un libro con varios autores; | ||
| + | :* borrar un libro xunto cos datos dependentes que corresponda eliminar; | ||
| + | :* gardar un pedido e todas as súas liñas. | ||
| + | |||
| + | Nestes casos normalmente queremos que se cumpra unha regra: | ||
| + | |||
| + | :'''ou se realizan correctamente todas as operacións, ou non se garda ningunha.''' | ||
| + | |||
| + | Para conseguir este comportamento empregamos unha '''transacción'''. | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Que é unha transacción? === | |
| − | |||
| + | Unha transacción é un conxunto de operacións que se consideran unha única unidade lóxica de traballo. | ||
| − | : | + | O esquema básico é: |
| − | + | :# iniciar a transacción; | |
| − | : | + | :# realizar as operacións; |
| + | :# se todo é correcto, executar <code>COMMIT</code>; | ||
| + | :# se se produce un erro, executar <code>ROLLBACK</code>. | ||
| − | + | Por exemplo: | |
| − | + | ::<syntaxhighlight lang="text" enclose="div"> | |
| − | + | INICIO | |
| − | |||
| − | |||
| + | INSERT libro | ||
| + | INSERT exemplar 1 | ||
| + | INSERT exemplar 2 | ||
| + | INSERT exemplar 3 | ||
| + | SE TODO FOI CORRECTO | ||
| + | COMMIT | ||
| + | SE HOUBO UN ERRO | ||
| + | ROLLBACK | ||
| + | </syntaxhighlight> | ||
| + | Se falla a inserción do terceiro exemplar, o <code>ROLLBACK</code> desfará tamén as insercións anteriores realizadas dentro da mesma transacción. | ||
| − | + | <br /> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Propiedades ACID === | |
| − | |||
| − | |||
| + | As transaccións adoitan explicarse mediante as propiedades '''ACID''': | ||
| + | :* '''Atomicidade (Atomicity)''': todas as operacións da transacción se realizan ou non se realiza ningunha. | ||
| + | :* '''Consistencia (Consistency)''': unha transacción debe levar a base de datos dun estado válido a outro estado válido. | ||
| + | :* '''Illamento (Isolation)''': as transaccións concorrentes deben interferir entre si de forma controlada. | ||
| + | :* '''Durabilidade (Durability)''': unha vez realizado o <code>COMMIT</code>, os cambios deben permanecer gardados. | ||
| + | <br /> | ||
| − | + | === Autocommit === | |
| − | |||
| − | + | Normalmente MySQL traballa con <code>autocommit</code> activado. | |
| + | Isto significa que unha orde individual como: | ||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | UPDATE contas | ||
| + | SET saldo = saldo - 100 | ||
| + | WHERE id = 1; | ||
| + | </syntaxhighlight> | ||
| − | + | queda confirmada automaticamente cando remata correctamente. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | Cando iniciamos explicitamente unha transacción con PDO: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->beginTransaction(); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | PDO desactiva temporalmente o modo de confirmación automática para esa conexión. | ||
| + | |||
| + | Os cambios non se confirman ata executar: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->commit(); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Se executamos: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->rollBack(); | ||
| + | </syntaxhighlight> | ||
| + | desfaranse os cambios realizados desde o inicio da transacción. | ||
| + | Ao finalizar mediante <code>commit()</code> ou <code>rollBack()</code>, a conexión volve ao funcionamento normal con autocommit. | ||
<br /> | <br /> | ||
| − | == | + | == Requirimentos e consideracións == |
| − | + | Para empregar transaccións debemos traballar cun motor de almacenamento que as soporte. | |
| − | |||
| − | |||
| − | |||
| + | En MySQL empregaremos normalmente: | ||
| − | + | ::<syntaxhighlight lang="text" enclose="div"> | |
| + | InnoDB | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | As táboas implicadas na operación deben empregar un motor transaccional. | ||
| + | |||
| + | <br /> | ||
| − | + | Nos exemplos desta unidade empregaremos: | |
| − | : | ||
| − | |||
| − | |||
| − | |||
| + | :* PHP 8.5. | ||
| + | :* PDO como mecanismo principal de acceso á base de datos. | ||
| + | :* MySQL/MariaDB con táboas InnoDB. | ||
| + | :* consultas preparadas para os valores variables. | ||
| + | :* excepcións para detectar erros. | ||
| + | :* <code>error_log()</code> para rexistrar a información técnica. | ||
| + | O usuario da base de datos deberá ter unicamente os permisos necesarios para as operacións da aplicación. | ||
<br /> | <br /> | ||
| − | + | === Arquivo de configuración === | |
| − | + | Como nas seccións anteriores, os datos de conexión e as consultas estarán nun arquivo situado fóra de <code>/var/www/html</code>: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | </ | ||
| − | + | ::<syntaxhighlight lang="text" enclose="div"> | |
| − | + | /var/www/confBD/bd.php | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | </syntaxhighlight> | ||
| + | Por exemplo: | ||
| − | :: | + | ::<syntaxhighlight lang="php" enclose="div"> |
| + | <?php | ||
| + | define('SERVIDOR', 'localhost'); | ||
| + | define('USUARIO', 'user_php'); | ||
| + | define('PASSWORD', 'user_php'); | ||
| + | define('BD', 'php'); | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| − | + | === Conexión PDO empregada nos exemplos === | |
| − | + | ||
| − | $ | + | ::<syntaxhighlight lang="php" enclose="div"> |
| − | + | $dsn = sprintf( | |
| − | </syntaxhighlight> | + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', |
| + | SERVIDOR, | ||
| + | BD | ||
| + | ); | ||
| + | |||
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| − | : | + | Ao empregar: |
| − | |||
| − | |||
| − | |||
| − | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | </syntaxhighlight> | ||
| − | + | un erro SQL provoca unha <code>PDOException</code>, o que facilita realizar o <code>ROLLBACK</code> desde o bloque <code>catch</code>. | |
| − | |||
| − | |||
| − | + | <br /> | |
| − | |||
| − | </ | ||
| + | == Funcionamento dunha transacción con PDO == | ||
| + | O patrón básico será: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | try { | ||
| − | + | require '/var/www/confBD/bd.php'; | |
| − | |||
| − | |||
| + | $dsn = sprintf( | ||
| + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', | ||
| + | SERVIDOR, | ||
| + | BD | ||
| + | ); | ||
| − | + | $conex = new PDO( | |
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| − | + | $conex->beginTransaction(); | |
| − | |||
| − | $conex | ||
| − | |||
| − | + | // Operación 1 | |
| − | + | // Operación 2 | |
| − | + | // Operación 3 | |
| − | |||
| − | |||
| − | + | $conex->commit(); | |
| − | + | } catch (PDOException $e) { | |
| − | |||
| − | |||
| − | $ | ||
| − | + | if ( | |
| − | + | isset($conex) && | |
| − | + | $conex->inTransaction() | |
| − | + | ) { | |
| − | + | $conex->rollBack(); | |
| + | } | ||
| − | + | error_log($e->getMessage()); | |
| + | die('Erro relacionado coa base de datos.'); | ||
| − | + | } catch (Throwable $e) { | |
| − | |||
| − | |||
| − | $ | ||
| − | + | if ( | |
| − | + | isset($conex) && | |
| − | + | $conex->inTransaction() | |
| − | if ($conex->inTransaction()) { | + | ) { |
$conex->rollBack(); | $conex->rollBack(); | ||
} | } | ||
| − | |||
| − | + | error_log($e->getMessage()); | |
| + | die('Produciuse un erro na aplicación.'); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | <br /> | ||
| + | === beginTransaction() === | ||
| + | Para iniciar a transacción empregamos: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $conex->beginTransaction(); | |
| − | + | </syntaxhighlight> | |
| − | + | ||
| − | + | A partir dese momento, as modificacións realizadas pola conexión non quedan confirmadas definitivamente ata executar <code>commit()</code>. | |
| − | + | ||
| − | + | <br /> | |
| − | + | ||
| − | + | === commit() === | |
| − | + | ||
| − | + | Se todas as operacións finalizaron correctamente: | |
| − | + | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $conex->commit(); | |
| − | + | </syntaxhighlight> | |
| − | + | ||
| − | + | confirma os cambios. | |
| + | |||
| + | O <code>commit()</code> debe executarse unicamente cando todas as operacións que forman a unidade lóxica finalizaron correctamente. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === rollBack() === | ||
| + | |||
| + | Se se produce unha excepción: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->rollBack(); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | desfai os cambios realizados dentro da transacción. | ||
| + | |||
| + | Antes de chamar a <code>rollBack()</code> resulta conveniente comprobar: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->inTransaction() | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Por exemplo: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | if ( | |
| − | + | isset($conex) && | |
| − | + | $conex->inTransaction() | |
| − | + | ) { | |
| − | + | $conex->rollBack(); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Isto evita intentar desfacer unha transacción que xa non está activa. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Exemplo completo === | ||
| + | |||
| + | No arquivo <code>bd.php</code> poderiamos ter: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | define( | ||
| + | 'INSERIR_PEDIDO', | ||
| + | 'INSERT INTO pedidos (cliente_id) | ||
| + | VALUES (:cliente_id)' | ||
| + | ); | ||
| − | </syntaxhighlight> | + | define( |
| + | 'INSERIR_LINA_PEDIDO', | ||
| + | 'INSERT INTO lina_pedido ( | ||
| + | pedido_id, | ||
| + | produto_id, | ||
| + | cantidade | ||
| + | ) | ||
| + | VALUES ( | ||
| + | :pedido_id, | ||
| + | :produto_id, | ||
| + | :cantidade | ||
| + | )' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | E a páxina PHP: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | |||
<?php | <?php | ||
| + | |||
try { | try { | ||
| − | |||
| − | |||
| − | |||
| + | require '/var/www/confBD/bd.php'; | ||
| + | |||
| + | $dsn = sprintf( | ||
| + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', | ||
| + | SERVIDOR, | ||
| + | BD | ||
| + | ); | ||
| + | |||
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | $clienteId = 10; | ||
| + | |||
| + | $produtos = [ | ||
| + | [ | ||
| + | 'produto_id' => 5, | ||
| + | 'cantidade' => 2 | ||
| + | ], | ||
| + | [ | ||
| + | 'produto_id' => 8, | ||
| + | 'cantidade' => 1 | ||
| + | ] | ||
| + | ]; | ||
| − | |||
$conex->beginTransaction(); | $conex->beginTransaction(); | ||
| − | // | + | /* |
| − | $ | + | * Creamos o pedido. |
| − | + | */ | |
| − | + | $stmtPedido = $conex->prepare( | |
| + | INSERIR_PEDIDO | ||
| + | ); | ||
| + | |||
| + | $stmtPedido->execute([ | ||
| + | ':cliente_id' => $clienteId | ||
| + | ]); | ||
| + | |||
| + | /* | ||
| + | * Recuperamos o identificador antes | ||
| + | * de finalizar a transacción. | ||
| + | */ | ||
| + | $pedidoId = | ||
| + | (int) $conex->lastInsertId(); | ||
| + | |||
| + | /* | ||
| + | * Preparamos unha única vez a consulta | ||
| + | * que imos executar varias veces. | ||
| + | */ | ||
| + | $stmtLina = $conex->prepare( | ||
| + | INSERIR_LINA_PEDIDO | ||
| + | ); | ||
| + | |||
| + | foreach ($produtos as $produto) { | ||
| + | |||
| + | $stmtLina->execute([ | ||
| + | ':pedido_id' => $pedidoId, | ||
| + | ':produto_id' => | ||
| + | $produto['produto_id'], | ||
| + | ':cantidade' => | ||
| + | $produto['cantidade'] | ||
| + | ]); | ||
} | } | ||
| − | // | + | /* |
| − | + | * Todas as operacións remataron ben. | |
| − | + | */ | |
| − | + | $conex->commit(); | |
| − | + | ||
| + | } catch (PDOException $e) { | ||
| − | + | if ( | |
| − | + | isset($conex) && | |
| − | + | $conex->inTransaction() | |
| − | if ( | + | ) { |
$conex->rollBack(); | $conex->rollBack(); | ||
} | } | ||
| − | + | ||
| − | } | + | error_log($e->getMessage()); |
| − | catch ( | + | |
| − | if ( | + | die('Erro relacionado coa base de datos.'); |
| + | |||
| + | } catch (Throwable $e) { | ||
| + | |||
| + | if ( | ||
| + | isset($conex) && | ||
| + | $conex->inTransaction() | ||
| + | ) { | ||
$conex->rollBack(); | $conex->rollBack(); | ||
} | } | ||
| − | + | ||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | die('Produciuse un erro na aplicación.'); | ||
} | } | ||
| − | + | </syntaxhighlight> | |
| − | + | ||
| − | + | Fixarse en varios detalles: | |
| − | ? | + | |
| + | :* as consultas variables son preparadas; | ||
| + | :* a consulta das liñas se prepara unha soa vez e se reutiliza dentro do bucle; | ||
| + | :* <code>lastInsertId()</code> se consulta antes de finalizar a transacción; | ||
| + | :* se calquera <code>execute()</code> lanza unha excepción, non se chega ao <code>commit()</code>; | ||
| + | :* o bloque <code>catch</code> realiza o <code>rollBack()</code>; | ||
| + | :* non se mostra ao usuario o texto interno da excepción. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | == Que operacións deben incluírse na transacción? == | ||
| + | Unha transacción debe ser o máis curta posible, pero debe conter todas as operacións que forman unha única unidade lóxica. | ||
| − | + | Por exemplo, non deberiamos facer: | |
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->beginTransaction(); | ||
| + | INSERT ... | ||
| − | + | // Procesamento moi longo. | |
| − | + | // Chamadas externas. | |
| − | + | // Interacción co usuario. | |
| − | + | // Esperas. | |
| − | + | ||
| + | UPDATE ... | ||
| − | |||
| − | |||
$conex->commit(); | $conex->commit(); | ||
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | |||
| + | porque durante unha transacción poden manterse bloqueos sobre filas e índices. | ||
| + | |||
| + | O recomendable é: | ||
| + | |||
| + | :# validar previamente todos os datos que poidamos validar en PHP; | ||
| + | :# preparar os datos necesarios; | ||
| + | :# iniciar a transacción; | ||
| + | :# realizar unicamente as operacións da base de datos necesarias; | ||
| + | :# confirmar ou desfacer; | ||
| + | :# finalizar a transacción canto antes. | ||
| + | |||
| + | <br /> | ||
| − | : | + | == Sentenzas DDL e COMMIT implícito == |
| − | + | ||
| − | + | Debemos evitar executar sentenzas de definición de datos dentro dunha transacción MySQL. | |
| − | </ | + | |
| + | Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
| + | CREATE TABLE ... | ||
| + | ALTER TABLE ... | ||
| + | DROP TABLE ... | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | MySQL pode realizar un <code>COMMIT</code> implícito con este tipo de sentenzas. | ||
| + | Isto significa que non debemos esperar que: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->rollBack(); | ||
| + | </syntaxhighlight> | ||
| − | + | poida desfacer sempre cambios anteriores se durante a transacción executamos sentenzas DDL. | |
| − | |||
| − | |||
| − | |||
| + | As transaccións dos exemplos desta unidade estarán formadas fundamentalmente por operacións: | ||
| − | + | ::<syntaxhighlight lang="text" enclose="div"> | |
| − | + | SELECT | |
| − | + | INSERT | |
| + | UPDATE | ||
| + | DELETE | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | <br /> | ||
| + | |||
| + | == Niveis de illamento == | ||
| + | |||
| + | Cando varias transaccións se executan simultaneamente debemos considerar como ven unhas transaccións os cambios realizados polas outras. | ||
| + | |||
| + | Isto está relacionado co '''nivel de illamento'''. | ||
| + | |||
| + | InnoDB soporta os niveis: | ||
| + | |||
| + | :* <code>READ UNCOMMITTED</code> | ||
| + | :* <code>READ COMMITTED</code> | ||
| + | :* <code>REPEATABLE READ</code> | ||
| + | :* <code>SERIALIZABLE</code> | ||
| + | |||
| + | En MySQL/InnoDB o nivel por defecto é normalmente: | ||
| + | ::<syntaxhighlight lang="text" enclose="div"> | ||
| + | REPEATABLE READ | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Problemas de concorrencia === | |
| − | + | ||
| − | + | Os niveis de illamento están relacionados con problemas como: | |
| − | + | ||
| + | :* lecturas sucias; | ||
| + | :* lecturas non repetibles; | ||
| + | :* lecturas fantasma; | ||
| + | :* bloqueos entre transaccións. | ||
| + | |||
| + | Non debemos entender o illamento simplemente como "bloquear toda a táboa". | ||
| + | |||
| + | InnoDB utiliza mecanismos de concorrencia, instantáneas e bloqueos sobre rexistros e índices dependendo: | ||
| + | |||
| + | :* do nivel de illamento; | ||
| + | :* do tipo de consulta; | ||
| + | :* dos índices empregados; | ||
| + | :* das filas que se modifican ou bloquean. | ||
| − | + | <br /> | |
| − | + | ||
| − | + | === Modificar o nivel de illamento === | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Normalmente non necesitaremos cambialo nos primeiros exemplos. | |
| − | + | Se fose necesario, MySQL permite configurar o nivel dunha transacción mediante SQL. | |
| + | Por exemplo: | ||
| − | + | ::<syntaxhighlight lang="sql" enclose="div"> | |
| − | + | SET TRANSACTION ISOLATION LEVEL | |
| − | + | READ COMMITTED; | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| + | Debe facerse comprendendo previamente as consecuencias sobre a concorrencia e a consistencia. | ||
| − | + | <br /> | |
| − | |||
| − | + | == Interbloqueos ou deadlocks == | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Un '''deadlock''' pode producirse cando dúas transaccións esperan mutuamente por recursos bloqueados pola outra. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Un exemplo simplificado: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | < | + | ::<syntaxhighlight lang="text" enclose="div"> |
| + | TRANSACCIÓN A | ||
| + | bloquea fila 1 | ||
| + | espera pola fila 2 | ||
| + | TRANSACCIÓN B | ||
| + | bloquea fila 2 | ||
| + | espera pola fila 1 | ||
| + | </syntaxhighlight> | ||
| + | InnoDB pode detectar o interbloqueo e desfacer unha das transaccións para permitir que a outra continúe. | ||
| + | Por este motivo, unha aplicación con moita concorrencia debe estar preparada para que unha transacción poida fallar e, cando resulte apropiado, repetir a operación. | ||
| + | Para reducir os deadlocks resulta útil: | ||
| + | :* acceder ás táboas e filas nunha orde consistente; | ||
| + | :* manter as transaccións curtas; | ||
| + | :* utilizar índices axeitados; | ||
| + | :* bloquear unicamente os datos necesarios. | ||
<br /> | <br /> | ||
| − | |||
| + | == Transaccións en procedementos almacenados == | ||
| + | |||
| + | Unha transacción tamén pode controlarse desde un procedemento almacenado MySQL. | ||
| + | |||
| + | Neste caso debemos decidir onde se encontra o límite da transacción: | ||
| − | * | + | :* '''alternativa A''': a transacción é controlada por PHP/PDO; |
| + | :* '''alternativa B''': a transacción é controlada polo procedemento almacenado. | ||
| − | + | Non debemos iniciar unha transacción en PHP e, dentro dela, chamar a un procedemento que executa outro <code>START TRANSACTION</code>. | |
| − | |||
| − | + | MySQL non admite transaccións anidadas reais. Un novo <code>START TRANSACTION</code> pode finalizar implicitamente a transacción anterior. | |
| − | |||
| − | + | <br /> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Alternativa A: transacción controlada por PDO === | |
| − | + | O procedemento almacenado non executa: | |
| − | |||
| − | + | ::<syntaxhighlight lang="sql" enclose="div"> | |
| − | + | START TRANSACTION | |
| + | COMMIT | ||
| + | ROLLBACK | ||
| + | </syntaxhighlight> | ||
| − | + | PHP controla todo: | |
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | $ | + | $conex->beginTransaction(); |
| − | |||
| − | + | try { | |
| − | |||
| − | |||
| + | // CALL procedemento 1 | ||
| + | // CALL procedemento 2 | ||
| + | // OUTRAS OPERACIÓNS | ||
| − | + | $conex->commit(); | |
| − | |||
| − | + | } catch (Throwable $e) { | |
| + | if ($conex->inTransaction()) { | ||
| + | $conex->rollBack(); | ||
| + | } | ||
| + | throw $e; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | Esta opción é útil cando a unidade de traballo abrangue varias chamadas ou operacións diferentes realizadas desde PHP. | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| − | ::<syntaxhighlight lang=" | + | === Alternativa B: transacción controlada polo procedemento === |
| + | |||
| + | O procedemento pode encapsular completamente a operación. | ||
| + | |||
| + | Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql" enclose="div"> | ||
DELIMITER $$ | DELIMITER $$ | ||
| − | CREATE | + | CREATE PROCEDURE PROCEDEMENTO_CON_TRANSACCION( |
| + | IN p_param1 INT, | ||
| + | IN p_param2 VARCHAR(50), | ||
| + | IN p_param3 VARCHAR(200) | ||
| + | ) | ||
BEGIN | BEGIN | ||
| − | |||
| − | + | DECLARE EXIT HANDLER FOR SQLEXCEPTION | |
| − | + | BEGIN | |
| − | + | ROLLBACK; | |
| − | + | RESIGNAL; | |
| − | + | END; | |
| + | |||
| + | START TRANSACTION; | ||
| − | + | INSERT INTO TABLE_1 ( | |
| − | + | col1, | |
| − | VALUES ( | + | col2 |
| − | + | ) | |
| − | + | VALUES ( | |
| − | + | p_param2, | |
| + | p_param3 | ||
| + | ); | ||
| − | + | INSERT INTO TABLE_2 ( | |
| − | + | col1, | |
| + | col2 | ||
| + | ) | ||
| + | VALUES ( | ||
| + | p_param1, | ||
| + | LAST_INSERT_ID() | ||
| + | ); | ||
| − | + | COMMIT; | |
| − | |||
| − | |||
| − | |||
| − | |||
END$$ | END$$ | ||
| + | |||
DELIMITER ; | DELIMITER ; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Neste caso: | ||
| + | |||
| + | :* <code>START TRANSACTION</code> inicia a transacción dentro do procedemento; | ||
| + | :* o <code>EXIT HANDLER</code> captura calquera excepción SQL; | ||
| + | :* <code>ROLLBACK</code> desfai os cambios; | ||
| + | :* <code>RESIGNAL</code> volve propagar o erro cara a PHP; | ||
| + | :* <code>COMMIT</code> só se executa se todas as operacións anteriores rematan correctamente. | ||
| + | |||
| + | Desde PHP '''non iniciaremos outra transacción''': | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'CALL PROCEDEMENTO_CON_TRANSACCION( | ||
| + | :param1, | ||
| + | :param2, | ||
| + | :param3 | ||
| + | )' | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':param1' => $param1, | ||
| + | ':param2' => $param2, | ||
| + | ':param3' => $param3 | ||
| + | ]); | ||
| + | $stmt->closeCursor(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | Se o procedemento lanza novamente o erro mediante <code>RESIGNAL</code>, PDO recibirá unha <code>PDOException</code>. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | == Exercicio proposto 1: asociar libros e autores == | ||
| + | Queremos xestionar unha relación N:M entre libros e autores. | ||
| + | A idea é: | ||
| − | < | + | :# seleccionar un libro nun <code><select></code>; |
| − | + | :# amosar todos os autores; | |
| + | :# marcar mediante <code>checkbox</code> os autores xa asociados ao libro; | ||
| + | :# permitir marcar ou desmarcar autores; | ||
| + | :# ao premer '''ASOCIAR''', substituír as asociacións actuais polas seleccionadas. | ||
| − | + | A operación final implica: | |
| − | : | + | :# borrar as asociacións existentes do libro; |
| − | : | + | :# inserir unha fila por cada autor seleccionado. |
| − | |||
| − | |||
| − | |||
| − | |||
| + | Estas operacións deben formar unha transacción. | ||
| − | + | <br /> | |
| − | + | === Arquivos === | |
| − | |||
| − | |||
| + | Empregaremos: | ||
| + | ::<syntaxhighlight lang="text" enclose="div"> | ||
| + | alta_libro_autores.php | ||
| + | procesar_alta_libro_autores.php | ||
| + | </syntaxhighlight> | ||
<gallery caption="Libros-Autores" widths="350" heights="300px" perrow="2"> | <gallery caption="Libros-Autores" widths="350" heights="300px" perrow="2"> | ||
| − | Image:UD3_op_bd_trans_ex_1_A.jpg| Escollemos | + | Image:UD3_op_bd_trans_ex_1_A.jpg| Escollemos un libro. |
| − | Image:UD3_op_bd_trans_ex_1_B.jpg| | + | Image:UD3_op_bd_trans_ex_1_B.jpg| Amosamos os autores asociados e modificamos a selección. |
</gallery> | </gallery> | ||
| + | <br /> | ||
| + | |||
| + | === Arquivo bd.php === | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | define( | ||
| + | 'CONSULTAR_LIBROS', | ||
| + | 'SELECT id, titulo | ||
| + | FROM libros | ||
| + | ORDER BY titulo' | ||
| + | ); | ||
| + | define( | ||
| + | 'CONSULTAR_AUTORES', | ||
| + | 'SELECT id, apelidos, nome | ||
| + | FROM autores | ||
| + | ORDER BY apelidos, nome' | ||
| + | ); | ||
| + | |||
| + | define( | ||
| + | 'CONSULTAR_AUTORES_POR_LIBRO', | ||
| + | 'SELECT | ||
| + | autores.id, | ||
| + | autores.apelidos, | ||
| + | autores.nome | ||
| + | FROM autores | ||
| + | INNER JOIN libro_autor | ||
| + | ON libro_autor.autor_id = autores.id | ||
| + | WHERE libro_autor.libro_id = :libro_id | ||
| + | ORDER BY autores.apelidos, autores.nome' | ||
| + | ); | ||
| + | |||
| + | define( | ||
| + | 'INSERIR_LIBRO_AUTOR', | ||
| + | 'INSERT INTO libro_autor ( | ||
| + | libro_id, | ||
| + | autor_id | ||
| + | ) | ||
| + | VALUES ( | ||
| + | :libro_id, | ||
| + | :autor_id | ||
| + | )' | ||
| + | ); | ||
| + | |||
| + | define( | ||
| + | 'BORRAR_LIBRO_AUTOR_POR_LIBRO', | ||
| + | 'DELETE FROM libro_autor | ||
| + | WHERE libro_id = :libro_id' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| + | === Arrays enviados por formularios === | ||
| − | + | Cada autor pode representarse cun checkbox: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | ::<syntaxhighlight lang="html" enclose="div"> | ||
| + | <input | ||
| + | type="checkbox" | ||
| + | name="autores[]" | ||
| + | value="10" | ||
| + | > | ||
| + | </syntaxhighlight> | ||
| − | + | Ao repetir: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | </syntaxhighlight> | + | ::<syntaxhighlight lang="text" enclose="div"> |
| + | name="autores[]" | ||
| + | </syntaxhighlight> | ||
| + | PHP recibirá os autores seleccionados como un array: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $_POST['autores'] | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Se o usuario non selecciona ningún autor, o campo pode non chegar na petición. | ||
| + | |||
| + | Por iso empregaremos: | ||
| − | '' | + | ::<syntaxhighlight lang="php" enclose="div"> |
| − | ::<syntaxhighlight lang=" | + | $autoresRecibidos = |
| + | $_POST['autores'] ?? []; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Isto tamén permite eliminar todas as asociacións dun libro deixando todos os checkbox sen marcar. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Arquivo alta_libro_autores.php === | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | try { |
| + | |||
| + | require '/var/www/confBD/bd.php'; | ||
| − | + | $dsn = sprintf( | |
| − | $conex = new | + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', |
| − | $conex-> | + | SERVIDOR, |
| − | + | BD | |
| + | ); | ||
| + | |||
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | /* | ||
| + | * Lista de libros. | ||
| + | */ | ||
| + | $stmt = $conex->query( | ||
| + | CONSULTAR_LIBROS | ||
| + | ); | ||
| + | |||
| + | $libros = $stmt->fetchAll(); | ||
| + | |||
| + | $libroId = null; | ||
| + | $autores = []; | ||
| + | $autoresLibro = []; | ||
| + | |||
| + | /* | ||
| + | * Se o usuario seleccionou un libro, | ||
| + | * cargamos todos os autores e as | ||
| + | * asociacións existentes. | ||
| + | */ | ||
| + | if (isset($_POST['btnLibro'])) { | ||
| + | |||
| + | $libroId = filter_input( | ||
| + | INPUT_POST, | ||
| + | 'lstLibros', | ||
| + | FILTER_VALIDATE_INT, | ||
| + | [ | ||
| + | 'options' => [ | ||
| + | 'min_range' => 1 | ||
| + | ] | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | if ( | ||
| + | $libroId === false || | ||
| + | $libroId === null | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'O libro seleccionado non é válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | $stmt = $conex->query( | ||
| + | CONSULTAR_AUTORES | ||
| + | ); | ||
| + | |||
| + | $autores = $stmt->fetchAll(); | ||
| + | |||
| + | $stmt = $conex->prepare( | ||
| + | CONSULTAR_AUTORES_POR_LIBRO | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | ':libro_id' => $libroId | ||
| + | ]); | ||
| − | + | $autoresLibro = array_map( | |
| − | + | 'intval', | |
| − | + | array_column( | |
| + | $stmt->fetchAll(), | ||
| + | 'id' | ||
| + | ) | ||
| + | ); | ||
} | } | ||
| − | |||
| + | } catch (PDOException $e) { | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | die('Erro relacionado coa base de datos.'); | ||
| + | |||
| + | } catch (Throwable $e) { | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | die('Produciuse un erro na aplicación.'); | ||
| + | } | ||
?> | ?> | ||
| − | <!DOCTYPE html | + | <!DOCTYPE html> |
| − | + | <html lang="gl"> | |
| − | <html | + | <head> |
| − | + | <meta charset="UTF-8"> | |
| − | + | <title>Xestión de libros e autores</title> | |
| − | + | </head> | |
| − | </ | + | <body> |
| − | < | + | |
| + | <h1>Asociar libros e autores</h1> | ||
| + | |||
| + | <form method="post"> | ||
| + | |||
| + | <label for="lstLibros"> | ||
| + | Libro: | ||
| + | </label> | ||
| + | |||
| + | <select | ||
| + | id="lstLibros" | ||
| + | name="lstLibros" | ||
| + | required | ||
| + | > | ||
| + | |||
| + | <option value=""> | ||
| + | Escolle un libro | ||
| + | </option> | ||
| − | + | <?php foreach ($libros as $libro): ?> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <?php | |
| − | + | $idLibro = | |
| − | + | (int) $libro['id']; | |
| − | |||
| − | |||
| − | |||
?> | ?> | ||
| − | |||
| − | |||
| − | |||
| − | + | <option | |
| − | + | value="<?= $idLibro ?>" | |
| − | + | <?= $libroId === $idLibro | |
| − | + | ? 'selected' | |
| − | + | : '' ?> | |
| − | + | > | |
| − | $ | + | <?= htmlspecialchars( |
| − | + | $libro['titulo'], | |
| − | + | ENT_QUOTES | ENT_SUBSTITUTE, | |
| − | + | 'UTF-8' | |
| − | + | ) ?> | |
| − | + | </option> | |
| − | + | ||
| − | + | <?php endforeach; ?> | |
| − | + | ||
| − | + | </select> | |
| + | |||
| + | <button | ||
| + | type="submit" | ||
| + | name="btnLibro" | ||
| + | value="BUSCAR" | ||
| + | > | ||
| + | Buscar autores | ||
| + | </button> | ||
| + | |||
| + | </form> | ||
| + | |||
| + | <?php if ($libroId !== null): ?> | ||
| + | |||
| + | <form | ||
| + | action="procesar_alta_libro_autores.php" | ||
| + | method="post" | ||
| + | > | ||
| + | |||
| + | <input | ||
| + | type="hidden" | ||
| + | name="libro_id" | ||
| + | value="<?= $libroId ?>" | ||
| + | > | ||
| + | |||
| + | <table> | ||
| + | <thead> | ||
| + | <tr> | ||
| + | <th>Autor</th> | ||
| + | <th>Asociado</th> | ||
| + | </tr> | ||
| + | </thead> | ||
| + | |||
| + | <tbody> | ||
| + | |||
| + | <?php foreach ($autores as $autor): ?> | ||
| + | |||
| + | <?php | ||
| + | $autorId = | ||
| + | (int) $autor['id']; | ||
| + | ?> | ||
| + | |||
| + | <tr> | ||
| + | <td> | ||
| + | <?= htmlspecialchars( | ||
| + | $autor['apelidos'] . | ||
| + | ', ' . | ||
| + | $autor['nome'], | ||
| + | ENT_QUOTES | | ||
| + | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </td> | ||
| − | + | <td> | |
| − | + | <input | |
| − | + | type="checkbox" | |
| − | + | name="autores[]" | |
| − | + | value="<?= $autorId ?>" | |
| − | + | <?= in_array( | |
| − | + | $autorId, | |
| − | + | $autoresLibro, | |
| − | + | true | |
| − | + | ) | |
| − | + | ? 'checked' | |
| − | + | : '' ?> | |
| − | + | > | |
| − | + | </td> | |
| − | + | </tr> | |
| − | |||
| − | ?> | + | <?php endforeach; ?> |
| + | |||
| + | </tbody> | ||
</table> | </table> | ||
| − | < | + | |
| + | <button | ||
| + | type="submit" | ||
| + | name="btnAsociar" | ||
| + | value="ASOCIAR" | ||
| + | > | ||
| + | Asociar | ||
| + | </button> | ||
| + | |||
</form> | </form> | ||
| + | <?php endif; ?> | ||
| − | + | <?php if (isset($_GET['msg'])): ?> | |
| − | |||
| − | |||
| + | <p> | ||
| + | <?= htmlspecialchars( | ||
| + | $_GET['msg'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </p> | ||
| − | + | <?php endif; ?> | |
| − | + | ||
| − | + | <?php if (isset($_GET['error'])): ?> | |
| − | ?> | + | |
| − | </ | + | <p> |
| − | + | <?= htmlspecialchars( | |
| − | </body> | + | $_GET['error'], |
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </p> | ||
| + | |||
| + | <?php endif; ?> | ||
| + | |||
| + | </body> | ||
</html> | </html> | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Non empregamos <code>rowCount()</code> para comprobar se un <code>SELECT</code> devolveu filas. | ||
| + | |||
| + | Recuperamos os resultados mediante: | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $stmt->fetchAll() | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | e traballamos directamente co array obtido. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Arquivo procesar_alta_libro_autores.php === | ||
| + | |||
| + | Esta é a operación realmente transaccional. | ||
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | try { | ||
| + | |||
| + | require '/var/www/confBD/bd.php'; | ||
| + | |||
| + | $dsn = sprintf( | ||
| + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', | ||
| + | SERVIDOR, | ||
| + | BD | ||
| + | ); | ||
| + | |||
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | if ( | ||
| + | !isset($_POST['btnAsociar']) || | ||
| + | $_POST['btnAsociar'] !== 'ASOCIAR' | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'Operación non válida.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | * Validamos o libro. | ||
| + | */ | ||
| + | $libroId = filter_input( | ||
| + | INPUT_POST, | ||
| + | 'libro_id', | ||
| + | FILTER_VALIDATE_INT, | ||
| + | [ | ||
| + | 'options' => [ | ||
| + | 'min_range' => 1 | ||
| + | ] | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | if ( | ||
| + | $libroId === false || | ||
| + | $libroId === null | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'O libro non é válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | * Pode chegar un array baleiro | ||
| + | * se non hai ningún autor seleccionado. | ||
| + | */ | ||
| + | $autoresRecibidos = | ||
| + | $_POST['autores'] ?? []; | ||
| + | if (!is_array($autoresRecibidos)) { | ||
| + | throw new RuntimeException( | ||
| + | 'Os autores non teñen un formato válido.' | ||
| + | ); | ||
| + | } | ||
| − | + | /* | |
| − | + | * Validamos todos os identificadores | |
| − | + | * ANTES de iniciar a transacción. | |
| − | + | */ | |
| − | + | $autores = []; | |
| − | |||
| − | |||
| − | + | foreach ($autoresRecibidos as $autor) { | |
| − | $ | + | $autorId = filter_var( |
| − | $ | + | $autor, |
| − | + | FILTER_VALIDATE_INT, | |
| − | + | [ | |
| − | + | 'options' => [ | |
| − | + | 'min_range' => 1 | |
| − | + | ] | |
| − | + | ] | |
| − | + | ); | |
| − | |||
| − | |||
| − | + | if ($autorId === false) { | |
| − | + | throw new RuntimeException( | |
| − | + | 'Existe un autor non válido.' | |
| + | ); | ||
} | } | ||
| + | |||
| + | $autores[] = $autorId; | ||
} | } | ||
| + | |||
| + | /* | ||
| + | * Evitamos repetidos recibidos | ||
| + | * nunha petición manipulada. | ||
| + | */ | ||
| + | $autores = array_values( | ||
| + | array_unique($autores) | ||
| + | ); | ||
| + | |||
| + | /* | ||
| + | * INICIO DA TRANSACCIÓN. | ||
| + | */ | ||
| + | $conex->beginTransaction(); | ||
| + | |||
| + | /* | ||
| + | * Eliminamos as asociacións anteriores. | ||
| + | */ | ||
| + | $stmtBorrar = $conex->prepare( | ||
| + | BORRAR_LIBRO_AUTOR_POR_LIBRO | ||
| + | ); | ||
| + | |||
| + | $stmtBorrar->execute([ | ||
| + | ':libro_id' => $libroId | ||
| + | ]); | ||
| + | |||
| + | /* | ||
| + | * Preparamos unha única vez o INSERT. | ||
| + | */ | ||
| + | $stmtInserir = $conex->prepare( | ||
| + | INSERIR_LIBRO_AUTOR | ||
| + | ); | ||
| + | |||
| + | foreach ($autores as $autorId) { | ||
| + | |||
| + | $stmtInserir->execute([ | ||
| + | ':libro_id' => $libroId, | ||
| + | ':autor_id' => $autorId | ||
| + | ]); | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | * Se chegamos aquí, todas as operacións | ||
| + | * se realizaron correctamente. | ||
| + | */ | ||
$conex->commit(); | $conex->commit(); | ||
| − | + | ||
| − | catch ( | + | header( |
| − | if( | + | 'Location: alta_libro_autores.php?msg=' . |
| − | $ | + | urlencode( |
| − | } | + | 'As asociacións foron actualizadas correctamente.' |
| − | + | ) | |
| − | + | ); | |
| − | + | exit; | |
| − | } | + | |
| − | + | } catch (PDOException $e) { | |
| − | if( | + | |
| − | $conex-> | + | if ( |
| − | $conex-> | + | isset($conex) && |
| + | $conex->inTransaction() | ||
| + | ) { | ||
| + | $conex->rollBack(); | ||
| + | } | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | header( | ||
| + | 'Location: alta_libro_autores.php?error=' . | ||
| + | urlencode('Erro relacionado coa base de datos.') | ||
| + | ); | ||
| + | exit; | ||
| + | |||
| + | } catch (Throwable $e) { | ||
| + | |||
| + | if ( | ||
| + | isset($conex) && | ||
| + | $conex->inTransaction() | ||
| + | ) { | ||
| + | $conex->rollBack(); | ||
} | } | ||
| + | |||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | header( | ||
| + | 'Location: alta_libro_autores.php?error=' . | ||
| + | urlencode('Produciuse un erro na aplicación.') | ||
| + | ); | ||
| + | exit; | ||
} | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Fixarse en que validamos todos os identificadores '''antes''' de iniciar a transacción. | ||
| + | |||
| + | Desta forma a transacción permanece aberta o menor tempo posible. | ||
| + | |||
| + | <br /> | ||
| − | + | == Exercicio proposto 2: alta dun libro e varios exemplares == | |
| − | |||
| − | + | Queremos crear unha páxina que permita: | |
| − | + | :# indicar o título dun libro; | |
| + | :# indicar cantos exemplares queremos dar de alta; | ||
| + | :# amosar un segundo formulario con datos para cada exemplar; | ||
| + | :# dar de alta o libro; | ||
| + | :# obter o identificador autonumérico xerado; | ||
| + | :# inserir todos os exemplares asociados ao novo libro. | ||
| + | Os datos de cada exemplar serán: | ||
| + | :* estado; | ||
| + | :* imaxe; | ||
| + | :* prezo. | ||
| + | |||
| + | A operación debe ser atómica: | ||
| + | |||
| + | :se falla a alta de calquera exemplar, non debe quedar gardado nin o libro nin ningún dos exemplares. | ||
| + | |||
| + | <gallery caption="Libros-Exemplares" widths="350" heights="300px" perrow="2"> | ||
| + | Image:UD3_op_bd_trans_ex_2_A.jpg| Indicamos título e número de exemplares. | ||
| + | Image:UD3_op_bd_trans_ex_2_B.jpg| Introducimos os datos de cada exemplar. | ||
| + | </gallery> | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| + | === Deseño recomendado === | ||
| − | + | Neste exercicio hai dúas alternativas válidas: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | :* realizar toda a transacción desde PDO; | ||
| + | :* crear un único procedemento almacenado que encapsule a transacción completa. | ||
| − | + | Non é recomendable crear un procedemento para inserir o libro, outro para inserir cada exemplar e ademais iniciar unha transacción tanto en PHP como dentro dos procedementos. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Nesta solución principal faremos a transacción desde '''PDO'''. | |
| + | Despois amosaremos unha alternativa cun procedemento almacenado. | ||
| + | <br /> | ||
| − | + | === Consultas necesarias === | |
| − | ::<syntaxhighlight lang=" | + | |
| − | + | No arquivo <code>bd.php</code>: | |
| − | + | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | define( | |
| − | + | 'CONSULTAR_ESTADOS', | |
| + | 'SELECT id, descripcion | ||
| + | FROM estados | ||
| + | ORDER BY descripcion' | ||
| + | ); | ||
| + | |||
| + | define( | ||
| + | 'INSERIR_LIBRO', | ||
| + | 'INSERT INTO libros (titulo) | ||
| + | VALUES (:titulo)' | ||
| + | ); | ||
| + | |||
| + | define( | ||
| + | 'INSERIR_EXEMPLAR', | ||
| + | 'INSERT INTO exemplares ( | ||
| + | libro_id, | ||
| + | id_exemplar, | ||
| + | imaxe, | ||
| + | prezo, | ||
| + | estado_id | ||
| + | ) | ||
| + | VALUES ( | ||
| + | :libro_id, | ||
| + | :id_exemplar, | ||
| + | :imaxe, | ||
| + | :prezo, | ||
| + | :estado_id | ||
| + | )' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| − | + | Se o título debe ser único, esta regra debería estar garantida tamén mediante unha restrición <code>UNIQUE</code> na base de datos. | |
| − | + | <br /> | |
| − | |||
| − | |||
| − | |||
| − | + | === Validación dos prezos === | |
| − | |||
| − | + | Un valor decimal procedente dun formulario chega a PHP como texto. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Podemos normalizar unha coma decimal a punto: | |
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | < | + | $prezoTexto = str_replace( |
| + | ',', | ||
| + | '.', | ||
| + | trim($prezoTexto) | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| − | + | e validalo: | |
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $prezo = filter_var( | |
| − | + | $prezoTexto, | |
| − | + | FILTER_VALIDATE_FLOAT | |
| − | + | ); | |
| − | |||
| − | + | if ( | |
| − | + | $prezo === false || | |
| − | + | $prezo <= 0 | |
| + | ) { | ||
| + | // Prezo non válido. | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| − | + | PDO non dispón de <code>PDO::PARAM_FLOAT</code>. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Por iso, unha vez validado o decimal, podemos envialo como cadea: | |
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $stmt->bindValue( | |
| − | + | ':prezo', | |
| − | + | (string) $prezo, | |
| − | + | PDO::PARAM_STR | |
| − | + | ); | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | + | A columna MySQL correspondente seguirá sendo de tipo numérico, por exemplo <code>DECIMAL(6,2)</code>. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <br /> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Non sanitizamos a imaxe para gardala === | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Se neste exercicio o campo <code>imaxe</code> contén un nome ou ruta textual, validaremos a súa lonxitude e formato. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Por exemplo: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| + | $imaxe = trim( | ||
| + | $imaxeRecibida | ||
| + | ); | ||
| − | + | if ( | |
| − | + | $imaxe === '' || | |
| − | + | mb_strlen($imaxe, 'UTF-8') > 100 | |
| − | + | ) { | |
| − | + | // Dato non válido. | |
| − | |||
| − | |||
| − | |||
} | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Non empregaremos: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| + | FILTER_SANITIZE_FULL_SPECIAL_CHARS | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | para transformar o dato antes de gardalo. | ||
| + | |||
| + | Se posteriormente se mostra nun documento HTML, empregaremos <code>htmlspecialchars()</code> nese momento. | ||
| + | |||
| + | <br /> | ||
| + | |||
| + | === Procesamento transaccional do libro e os exemplares === | ||
| − | + | Supoñemos que o segundo formulario envía: | |
| + | ::<syntaxhighlight lang="text" enclose="div"> | ||
| + | txtTitulo | ||
| + | lstEstado[] | ||
| + | txtImaxe[] | ||
| + | txtPrezo[] | ||
| + | </syntaxhighlight> | ||
| + | Antes de iniciar a transacción validaremos que os arrays existen e teñen o mesmo número de elementos. | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | ::<syntaxhighlight lang=" | ||
<?php | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | require | + | try { |
| + | |||
| + | require '/var/www/confBD/bd.php'; | ||
| + | |||
| + | $dsn = sprintf( | ||
| + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', | ||
| + | SERVIDOR, | ||
| + | BD | ||
| + | ); | ||
| + | |||
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | if ( | ||
| + | !isset($_POST['btnAlta']) || | ||
| + | $_POST['btnAlta'] !== 'ALTA' | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'Operación non válida.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | * Validamos o título. | ||
| + | */ | ||
| + | $titulo = trim( | ||
| + | $_POST['txtTitulo'] ?? '' | ||
| + | ); | ||
| + | |||
| + | if ( | ||
| + | $titulo === '' || | ||
| + | mb_strlen($titulo, 'UTF-8') > 300 | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'O título non é válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | * Recuperamos os arrays. | ||
| + | */ | ||
| + | $estadosRecibidos = | ||
| + | $_POST['lstEstado'] ?? []; | ||
| + | |||
| + | $imaxesRecibidas = | ||
| + | $_POST['txtImaxe'] ?? []; | ||
| + | |||
| + | $prezosRecibidos = | ||
| + | $_POST['txtPrezo'] ?? []; | ||
| + | |||
| + | if ( | ||
| + | !is_array($estadosRecibidos) || | ||
| + | !is_array($imaxesRecibidas) || | ||
| + | !is_array($prezosRecibidos) | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'Os datos dos exemplares non son válidos.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | $numExemplares = | ||
| + | count($estadosRecibidos); | ||
| + | |||
| + | if ( | ||
| + | $numExemplares < 1 || | ||
| + | $numExemplares > 9 || | ||
| + | count($imaxesRecibidas) !== | ||
| + | $numExemplares || | ||
| + | count($prezosRecibidos) !== | ||
| + | $numExemplares | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'O número de exemplares non é válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | * VALIDAMOS TODO ANTES DA TRANSACCIÓN. | ||
| + | */ | ||
| + | $exemplares = []; | ||
| + | |||
| + | for ( | ||
| + | $indice = 0; | ||
| + | $indice < $numExemplares; | ||
| + | $indice++ | ||
| + | ) { | ||
| + | |||
| + | $estadoId = filter_var( | ||
| + | $estadosRecibidos[$indice], | ||
| + | FILTER_VALIDATE_INT, | ||
| + | [ | ||
| + | 'options' => [ | ||
| + | 'min_range' => 1 | ||
| + | ] | ||
| + | ] | ||
| + | ); | ||
| + | |||
| + | if ($estadoId === false) { | ||
| + | throw new RuntimeException( | ||
| + | 'Existe un estado non válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | $imaxe = trim( | ||
| + | $imaxesRecibidas[$indice] | ||
| + | ); | ||
| + | |||
| + | if ( | ||
| + | $imaxe === '' || | ||
| + | mb_strlen( | ||
| + | $imaxe, | ||
| + | 'UTF-8' | ||
| + | ) > 100 | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'Existe un nome de imaxe non válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | $prezoTexto = str_replace( | ||
| + | ',', | ||
| + | '.', | ||
| + | trim( | ||
| + | $prezosRecibidos[$indice] | ||
| + | ) | ||
| + | ); | ||
| − | + | $prezo = filter_var( | |
| − | + | $prezoTexto, | |
| − | + | FILTER_VALIDATE_FLOAT | |
| − | + | ); | |
| + | |||
| + | if ( | ||
| + | $prezo === false || | ||
| + | $prezo <= 0 | ||
| + | ) { | ||
| + | throw new RuntimeException( | ||
| + | 'Existe un prezo non válido.' | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | $exemplares[] = [ | ||
| + | 'estado_id' => $estadoId, | ||
| + | 'imaxe' => $imaxe, | ||
| + | 'prezo' => $prezo | ||
| + | ]; | ||
| + | } | ||
| − | // | + | /* |
| + | * A PARTIR DE AQUÍ EMPEZA | ||
| + | * A TRANSACCIÓN. | ||
| + | */ | ||
$conex->beginTransaction(); | $conex->beginTransaction(); | ||
| − | + | /* | |
| − | + | * Inserimos o libro. | |
| − | + | */ | |
| − | + | $stmtLibro = $conex->prepare( | |
| + | INSERIR_LIBRO | ||
| + | ); | ||
| + | |||
| + | $stmtLibro->execute([ | ||
| + | ':titulo' => $titulo | ||
| + | ]); | ||
| + | |||
| + | $libroId = | ||
| + | (int) $conex->lastInsertId(); | ||
| + | /* | ||
| + | * Preparamos unha única vez | ||
| + | * o INSERT dos exemplares. | ||
| + | */ | ||
| + | $stmtExemplar = $conex->prepare( | ||
| + | INSERIR_EXEMPLAR | ||
| + | ); | ||
| − | + | foreach ( | |
| − | $ | + | $exemplares as $indice => $exemplar |
| − | $ | + | ) { |
| − | + | ||
| − | + | $stmtExemplar->bindValue( | |
| − | + | ':libro_id', | |
| + | $libroId, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | |||
| + | $stmtExemplar->bindValue( | ||
| + | ':id_exemplar', | ||
| + | $indice + 1, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | |||
| + | $stmtExemplar->bindValue( | ||
| + | ':imaxe', | ||
| + | $exemplar['imaxe'], | ||
| + | PDO::PARAM_STR | ||
| + | ); | ||
| + | |||
| + | $stmtExemplar->bindValue( | ||
| + | ':prezo', | ||
| + | (string) $exemplar['prezo'], | ||
| + | PDO::PARAM_STR | ||
| + | ); | ||
| + | |||
| + | $stmtExemplar->bindValue( | ||
| + | ':estado_id', | ||
| + | $exemplar['estado_id'], | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | |||
| + | $stmtExemplar->execute(); | ||
} | } | ||
| + | |||
| + | /* | ||
| + | * Todo foi correcto. | ||
| + | */ | ||
$conex->commit(); | $conex->commit(); | ||
| − | } | + | header( |
| − | catch (PDOException $e) { | + | 'Location: alta_libro_exemplares.php?msg=' . |
| − | if ( | + | urlencode( |
| + | 'Libro e exemplares dados de alta correctamente.' | ||
| + | ) | ||
| + | ); | ||
| + | exit; | ||
| + | |||
| + | } catch (PDOException $e) { | ||
| + | |||
| + | if ( | ||
| + | isset($conex) && | ||
| + | $conex->inTransaction() | ||
| + | ) { | ||
$conex->rollBack(); | $conex->rollBack(); | ||
} | } | ||
| − | $ | + | |
| − | } catch ( | + | error_log($e->getMessage()); |
| − | if ( | + | |
| + | $codigoMySQL = | ||
| + | $e->errorInfo[1] ?? null; | ||
| + | |||
| + | if ($codigoMySQL === 1062) { | ||
| + | |||
| + | $mensaxe = | ||
| + | 'Xa existe un rexistro cos mesmos datos clave.'; | ||
| + | |||
| + | } elseif ($codigoMySQL === 1452) { | ||
| + | |||
| + | $mensaxe = | ||
| + | 'Algún dos datos relacionados non existe.'; | ||
| + | |||
| + | } else { | ||
| + | |||
| + | $mensaxe = | ||
| + | 'Erro relacionado coa base de datos.'; | ||
| + | } | ||
| + | |||
| + | header( | ||
| + | 'Location: alta_libro_exemplares.php?error=' . | ||
| + | urlencode($mensaxe) | ||
| + | ); | ||
| + | exit; | ||
| + | |||
| + | } catch (Throwable $e) { | ||
| + | |||
| + | if ( | ||
| + | isset($conex) && | ||
| + | $conex->inTransaction() | ||
| + | ) { | ||
$conex->rollBack(); | $conex->rollBack(); | ||
} | } | ||
| − | + | ||
| − | + | error_log($e->getMessage()); | |
| − | + | ||
| − | + | header( | |
| + | 'Location: alta_libro_exemplares.php?error=' . | ||
| + | urlencode('Produciuse un erro na aplicación.') | ||
| + | ); | ||
| + | exit; | ||
} | } | ||
| − | + | </syntaxhighlight> | |
| − | |||
| − | </syntaxhighlight> | ||
| + | Neste exemplo unha excepción producida ao inserir calquera exemplar provoca o salto ao <code>catch</code> e o posterior <code>rollBack()</code>. | ||
| + | Polo tanto, non debemos escribir lóxica do tipo: | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | if (!$stmt->execute()) { | ||
| + | $error = '...'; | ||
| + | break; | ||
| + | } | ||
| + | |||
| + | $conex->commit(); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | porque, con <code>PDO::ERRMODE_EXCEPTION</code>, o mecanismo natural é deixar que a excepción interrompa a secuencia e desfacer a transacción no <code>catch</code>. | ||
<br /> | <br /> | ||
| − | == | + | === Formulario para os exemplares === |
| − | + | A lista de estados debe consultarse unha única vez: | |
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $stmt = $conex->query( | |
| − | + | CONSULTAR_ESTADOS | |
| − | + | ); | |
| − | + | $estados = $stmt->fetchAll(); | |
| − | + | </syntaxhighlight> | |
| − | + | ||
| − | : | + | Despois reutilizamos o array para todos os exemplares: |
| + | |||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | <?php for ( | ||
| + | $i = 0; | ||
| + | $i < $numExemplares; | ||
| + | $i++ | ||
| + | ): ?> | ||
| + | <tr> | ||
| + | <td> | ||
| + | <select | ||
| + | name="lstEstado[]" | ||
| + | required | ||
| + | > | ||
| − | < | + | <?php foreach ( |
| − | + | $estados as $estado | |
| − | + | ): ?> | |
| − | </ | + | |
| + | <option | ||
| + | value="<?= (int) $estado['id'] ?>" | ||
| + | > | ||
| + | <?= htmlspecialchars( | ||
| + | $estado['descripcion'], | ||
| + | ENT_QUOTES | | ||
| + | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) ?> | ||
| + | </option> | ||
| + | |||
| + | <?php endforeach; ?> | ||
| + | |||
| + | </select> | ||
| + | </td> | ||
| + | <td> | ||
| + | <input | ||
| + | type="text" | ||
| + | name="txtImaxe[]" | ||
| + | maxlength="100" | ||
| + | required | ||
| + | > | ||
| + | </td> | ||
| + | <td> | ||
| + | <input | ||
| + | type="number" | ||
| + | name="txtPrezo[]" | ||
| + | min="0.01" | ||
| + | step="0.01" | ||
| + | required | ||
| + | > | ||
| + | </td> | ||
| + | </tr> | ||
| − | < | + | <?php endfor; ?> |
| + | </syntaxhighlight> | ||
| − | + | Ao usar: | |
| − | |||
| − | + | ::<syntaxhighlight lang="text" enclose="div"> | |
| + | lstEstado[] | ||
| + | txtImaxe[] | ||
| + | txtPrezo[] | ||
| + | </syntaxhighlight> | ||
| − | + | os índices dos tres arrays permiten relacionar os datos do mesmo exemplar. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <br /> | |
| − | |||
| − | </ | ||
| + | == Alternativa: procedemento almacenado para libro e exemplares == | ||
| + | Se queremos que a transacción sexa controlada por MySQL, o ideal é que a unidade de traballo completa estea encapsulada no procedemento. | ||
| + | Para un número variable de exemplares isto require deseñar como se enviarán todos os datos ao servidor. | ||
| − | + | Un procedemento que unicamente insire o libro e outro que insire individualmente cada exemplar non encapsulan por si sós a unidade completa se PHP é quen realiza múltiples chamadas. | |
| − | |||
| − | |||
| − | + | Por iso temos dúas opcións: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | :* manter a transacción en PDO, como no exemplo anterior; | |
| − | + | :* deseñar un procedemento capaz de recibir e procesar todos os exemplares dentro dunha única chamada. | |
| − | + | A primeira opción é máis sinxela para este exercicio didáctico. | |
| − | |||
| − | |||
| − | |||
| + | <br /> | ||
| + | === Exemplo sinxelo de transacción dentro dun procedemento === | ||
| + | Para un caso cun número fixo de operacións: | ||
| − | + | ::<syntaxhighlight lang="sql" enclose="div"> | |
| − | ::<syntaxhighlight lang=" | ||
DELIMITER $$ | DELIMITER $$ | ||
| − | CREATE | + | |
| + | CREATE PROCEDURE INSERIR_LIBRO_E_EXEMPLAR( | ||
| + | IN p_titulo VARCHAR(300), | ||
| + | IN p_imaxe VARCHAR(100), | ||
| + | IN p_prezo DECIMAL(6,2), | ||
| + | IN p_estado_id INT | ||
| + | ) | ||
BEGIN | BEGIN | ||
| − | |||
| − | |||
| − | + | DECLARE v_libro_id INT; | |
| − | |||
| − | |||
| + | DECLARE EXIT HANDLER FOR SQLEXCEPTION | ||
| + | BEGIN | ||
| + | ROLLBACK; | ||
| + | RESIGNAL; | ||
| + | END; | ||
| + | START TRANSACTION; | ||
| − | + | INSERT INTO libros (titulo) | |
| − | + | VALUES (p_titulo); | |
| − | + | SET v_libro_id = | |
| − | + | LAST_INSERT_ID(); | |
| − | |||
| − | |||
| − | |||
| − | |||
| + | INSERT INTO exemplares ( | ||
| + | libro_id, | ||
| + | id_exemplar, | ||
| + | imaxe, | ||
| + | prezo, | ||
| + | estado_id | ||
| + | ) | ||
| + | VALUES ( | ||
| + | v_libro_id, | ||
| + | 1, | ||
| + | p_imaxe, | ||
| + | p_prezo, | ||
| + | p_estado_id | ||
| + | ); | ||
| + | COMMIT; | ||
| + | END$$ | ||
| + | |||
| + | DELIMITER ; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | Neste caso PHP simplemente chama ao procedemento e non inicia outra transacción. | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | == mysqli como ampliación == | |
| + | |||
| + | PDO será o mecanismo principal empregado nos apuntes. | ||
| − | + | Con <code>mysqli</code> o patrón equivalente é: | |
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | <?php | |
| − | |||
| − | |||
| − | |||
| − | + | try { | |
| − | + | ||
| − | + | require '/var/www/confBD/bd.php'; | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | mysqli_report( | |
| − | + | MYSQLI_REPORT_ERROR | | |
| − | + | MYSQLI_REPORT_STRICT | |
| − | + | ); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $conex = new mysqli( | |
| − | + | SERVIDOR, | |
| − | + | USUARIO, | |
| − | + | PASSWORD, | |
| − | + | BD | |
| − | + | ); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $conex->set_charset( | |
| − | + | 'utf8mb4' | |
| + | ); | ||
| + | $conex->begin_transaction(); | ||
| − | + | /* | |
| − | + | * Operacións preparadas. | |
| − | + | */ | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $conex->commit(); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | } catch (mysqli_sql_exception $e) { | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | if (isset($conex)) { | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | try { | |
| − | + | $conex->rollback(); | |
| − | + | } catch (Throwable) { | |
| − | + | // Non ocultamos a excepción orixinal. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | + | } | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | error_log($e->getMessage()); | |
| − | |||
| + | die('Erro relacionado coa base de datos.'); | ||
| − | + | } catch (Throwable $e) { | |
| + | if (isset($conex)) { | ||
| + | try { | ||
| + | $conex->rollback(); | ||
| + | } catch (Throwable) { | ||
| + | // Non ocultamos a excepción orixinal. | ||
| + | } | ||
| + | } | ||
| − | + | error_log($e->getMessage()); | |
| − | |||
| − | ' | + | die('Produciuse un erro na aplicación.'); |
| − | + | } | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | </syntaxhighlight> | ||
| + | <br /> | ||
| + | === Exemplo mysqli con consultas preparadas === | ||
| + | ::<syntaxhighlight lang="php" enclose="div"> | ||
| + | $conex->begin_transaction(); | ||
| − | + | $stmt = $conex->prepare( | |
| − | ' | + | 'INSERT INTO pedidos (cliente_id) |
| − | + | VALUES (?)' | |
| − | + | ); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $stmt->bind_param( | |
| + | 'i', | ||
| + | $clienteId | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| − | $ | + | $pedidoId = |
| − | $ | + | $conex->insert_id; |
| − | + | $stmtLina = $conex->prepare( | |
| − | + | 'INSERT INTO lina_pedido ( | |
| − | + | pedido_id, | |
| − | + | produto_id, | |
| − | + | cantidade | |
| + | ) | ||
| + | VALUES (?, ?, ?)' | ||
| + | ); | ||
| − | + | foreach ($produtos as $produto) { | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $produtoId = | |
| − | + | $produto['produto_id']; | |
| − | |||
| − | |||
| − | |||
| − | + | $cantidade = | |
| + | $produto['cantidade']; | ||
| − | + | $stmtLina->bind_param( | |
| − | + | 'iii', | |
| − | + | $pedidoId, | |
| + | $produtoId, | ||
| + | $cantidade | ||
| + | ); | ||
| − | + | $stmtLina->execute(); | |
| − | + | } | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | $conex->commit(); | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | + | Non é necesario empregar o patrón antigo: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $conex->autocommit(false); | |
| − | |||
| − | |||
| − | |||
| − | + | // ... | |
| − | |||
| − | + | $conex->autocommit(true); | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | como forma principal de explicar as transaccións. | |
| − | |||
| − | + | Para código novo resulta máis claro iniciar explicitamente a unidade de traballo con: | |
| − | |||
| − | < | + | ::<syntaxhighlight lang="php" enclose="div"> |
| − | + | $conex->begin_transaction(); | |
| − | + | </syntaxhighlight> | |
| − | </ | ||
| − | + | e rematala con: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $conex->commit(); | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | ou: | ||
| − | + | ::<syntaxhighlight lang="php" enclose="div"> | |
| − | + | $conex->rollback(); | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | + | <br /> | |
| − | |||
| − | |||
| − | |||
| − | + | == Exercicios propostos == | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Subida de imaxes === | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | Modificar o exercicio de libros e exemplares para que a imaxe deixe de ser simplemente un campo de texto e poida subirse un ficheiro ao servidor. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | A subida do ficheiro e a transacción da base de datos son dous mecanismos diferentes. | |
| − | < | + | Un <code>ROLLBACK</code> da base de datos non elimina automaticamente un ficheiro que xa se gardase no sistema de ficheiros. |
| − | </ | ||
| + | Polo tanto haberá que deseñar tamén como desfacer ou limpar os ficheiros creados se a operación da base de datos falla. | ||
| + | <br /> | ||
| + | === Asociar autores durante a alta do libro === | ||
| − | + | Ampliar o exercicio de alta dun libro para permitir seleccionar tamén os autores. | |
| − | |||
| − | |||
| + | A operación completa podería incluír: | ||
| + | :# inserir o libro; | ||
| + | :# obter o identificador xerado; | ||
| + | :# inserir os exemplares; | ||
| + | :# inserir as relacións en <code>LIBRO_AUTOR</code>; | ||
| + | :# confirmar todas as operacións mediante un único <code>COMMIT</code>. | ||
| − | + | Se falla calquera operación deberá realizarse <code>ROLLBACK</code>. | |
| − | |||
| − | |||
| − | |||
| + | <br /> | ||
| + | === Actualización dunha relación N:M === | ||
| + | Seleccionar unha atracción e amosar todos os artistas mediante checkbox. | ||
| + | Os artistas actualmente relacionados aparecerán marcados. | ||
| + | Ao gardar: | ||
| + | :# eliminar as asociacións anteriores; | ||
| + | :# inserir as asociacións seleccionadas; | ||
| + | :# realizar todo dentro dunha única transacción. | ||
| + | <br /> | ||
| + | == Resumo == | ||
| + | * Unha transacción permite agrupar varias modificacións nunha única unidade lóxica. | ||
| + | * As operacións confírmanse mediante <code>COMMIT</code>. | ||
| + | * Se se produce un erro, desfanse mediante <code>ROLLBACK</code>. | ||
| + | * Con PDO iniciamos a transacción mediante <code>beginTransaction()</code>. | ||
| + | * <code>beginTransaction()</code> desactiva temporalmente o autocommit para esa conexión. | ||
| + | * Antes de facer <code>rollBack()</code> podemos comprobar <code>inTransaction()</code>. | ||
| + | * PDO será o mecanismo principal empregado nos exemplos. | ||
| + | * As consultas con valores variables realizaranse con <code>prepare()</code>. | ||
| + | * Os datos deben validarse antes de iniciar a transacción sempre que sexa posible. | ||
| + | * As transaccións deben manterse abertas o menor tempo posible. | ||
| + | * Un erro SQL con <code>PDO::ERRMODE_EXCEPTION</code> lanza unha <code>PDOException</code>. | ||
| + | * As excepcións permiten interromper a secuencia e realizar o <code>ROLLBACK</code> no <code>catch</code>. | ||
| + | * Non mostraremos directamente <code>$e->getMessage()</code> aos usuarios. | ||
| + | * Empregaremos <code>error_log()</code> para rexistrar o detalle técnico. | ||
| + | * Debemos empregar un motor transaccional como InnoDB. | ||
| + | * As sentenzas DDL poden provocar un <code>COMMIT</code> implícito en MySQL. | ||
| + | * InnoDB soporta os niveis <code>READ UNCOMMITTED</code>, <code>READ COMMITTED</code>, <code>REPEATABLE READ</code> e <code>SERIALIZABLE</code>. | ||
| + | * O nivel habitual por defecto de InnoDB é <code>REPEATABLE READ</code>. | ||
| + | * Os deadlocks poden producirse mesmo con transaccións correctamente deseñadas e unha aplicación concorrente debe estar preparada para tratalos. | ||
| + | * MySQL non admite transaccións anidadas reais. | ||
| + | * Se a transacción é controlada polo procedemento almacenado, PHP non debe iniciar outra arredor da mesma operación. | ||
| + | * Nun procedemento almacenado podemos empregar un <code>EXIT HANDLER</code>, <code>ROLLBACK</code> e <code>RESIGNAL</code>. | ||
| + | * Nunha relación N:M, borrar as asociacións antigas e inserir as novas é un caso típico para empregar unha transacción. | ||
| + | * Se un conxunto de checkboxes pode quedar totalmente desmarcado, empregaremos <code>$_POST['campo'] ?? []</code>. | ||
| + | * Para inserir varias filas coa mesma consulta é recomendable preparar a sentenza unha soa vez e reutilizala. | ||
| + | * <code>lastInsertId()</code> permite obter o identificador xerado para empregalo nas seguintes operacións da mesma unidade de traballo. | ||
| + | * Un <code>ROLLBACK</code> da base de datos non desfai automaticamente cambios externos, como ficheiros gardados no sistema de ficheiros. | ||
| + | * <code>mysqli</code> queda como ampliación e tamén debe empregar consultas preparadas. | ||
| − | <br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- ( | + | <br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2026). |
Revisión del 07:51 23 sep 2026
Sumario
- 1 Introdución
- 2 Requirimentos e consideracións
- 3 Funcionamento dunha transacción con PDO
- 4 Que operacións deben incluírse na transacción?
- 5 Sentenzas DDL e COMMIT implícito
- 6 Niveis de illamento
- 7 Interbloqueos ou deadlocks
- 8 Transaccións en procedementos almacenados
- 9 Exercicio proposto 1: asociar libros e autores
- 10 Exercicio proposto 2: alta dun libro e varios exemplares
- 11 Alternativa: procedemento almacenado para libro e exemplares
- 12 mysqli como ampliación
- 13 Exercicios propostos
- 14 Resumo
Introdución
En certas ocasións unha operación da aplicación require realizar varias modificacións relacionadas na base de datos.
Por exemplo:
- dar de alta un libro e varios exemplares;
- asociar un libro con varios autores;
- borrar un libro xunto cos datos dependentes que corresponda eliminar;
- gardar un pedido e todas as súas liñas.
Nestes casos normalmente queremos que se cumpra unha regra:
- ou se realizan correctamente todas as operacións, ou non se garda ningunha.
Para conseguir este comportamento empregamos unha transacción.
Que é unha transacción?
Unha transacción é un conxunto de operacións que se consideran unha única unidade lóxica de traballo.
O esquema básico é:
- iniciar a transacción;
- realizar as operacións;
- se todo é correcto, executar
COMMIT; - se se produce un erro, executar
ROLLBACK.
Por exemplo:
INICIO INSERT libro INSERT exemplar 1 INSERT exemplar 2 INSERT exemplar 3 SE TODO FOI CORRECTO COMMIT SE HOUBO UN ERRO ROLLBACK
Se falla a inserción do terceiro exemplar, o ROLLBACK desfará tamén as insercións anteriores realizadas dentro da mesma transacción.
Propiedades ACID
As transaccións adoitan explicarse mediante as propiedades ACID:
- Atomicidade (Atomicity): todas as operacións da transacción se realizan ou non se realiza ningunha.
- Consistencia (Consistency): unha transacción debe levar a base de datos dun estado válido a outro estado válido.
- Illamento (Isolation): as transaccións concorrentes deben interferir entre si de forma controlada.
- Durabilidade (Durability): unha vez realizado o
COMMIT, os cambios deben permanecer gardados.
Autocommit
Normalmente MySQL traballa con autocommit activado.
Isto significa que unha orde individual como:
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
queda confirmada automaticamente cando remata correctamente.
Cando iniciamos explicitamente unha transacción con PDO:
$conex->beginTransaction();
PDO desactiva temporalmente o modo de confirmación automática para esa conexión.
Os cambios non se confirman ata executar:
$conex->commit();
Se executamos:
$conex->rollBack();
desfaranse os cambios realizados desde o inicio da transacción.
Ao finalizar mediante commit() ou rollBack(), a conexión volve ao funcionamento normal con autocommit.
Requirimentos e consideracións
Para empregar transaccións debemos traballar cun motor de almacenamento que as soporte.
En MySQL empregaremos normalmente:
InnoDB
As táboas implicadas na operación deben empregar un motor transaccional.
Nos exemplos desta unidade empregaremos:
- PHP 8.5.
- PDO como mecanismo principal de acceso á base de datos.
- MySQL/MariaDB con táboas InnoDB.
- consultas preparadas para os valores variables.
- excepcións para detectar erros.
error_log()para rexistrar a información técnica.
O usuario da base de datos deberá ter unicamente os permisos necesarios para as operacións da aplicación.
Arquivo de configuración
Como nas seccións anteriores, os datos de conexión e as consultas estarán nun arquivo situado fóra de /var/www/html:
/var/www/confBD/bd.php
Por exemplo:
<?php define('SERVIDOR', 'localhost'); define('USUARIO', 'user_php'); define('PASSWORD', 'user_php'); define('BD', 'php');
Conexión PDO empregada nos exemplos
$dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] );
Ao empregar:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
un erro SQL provoca unha PDOException, o que facilita realizar o ROLLBACK desde o bloque catch.
Funcionamento dunha transacción con PDO
O patrón básico será:
try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); $conex->beginTransaction(); // Operación 1 // Operación 2 // Operación 3 $conex->commit(); } catch (PDOException $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); }
beginTransaction()
Para iniciar a transacción empregamos:
$conex->beginTransaction();
A partir dese momento, as modificacións realizadas pola conexión non quedan confirmadas definitivamente ata executar commit().
commit()
Se todas as operacións finalizaron correctamente:
$conex->commit();
confirma os cambios.
O commit() debe executarse unicamente cando todas as operacións que forman a unidade lóxica finalizaron correctamente.
rollBack()
Se se produce unha excepción:
$conex->rollBack();
desfai os cambios realizados dentro da transacción.
Antes de chamar a rollBack() resulta conveniente comprobar:
$conex->inTransaction()
Por exemplo:
if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); }
Isto evita intentar desfacer unha transacción que xa non está activa.
Exemplo completo
No arquivo bd.php poderiamos ter:
define( 'INSERIR_PEDIDO', 'INSERT INTO pedidos (cliente_id) VALUES (:cliente_id)' ); define( 'INSERIR_LINA_PEDIDO', 'INSERT INTO lina_pedido ( pedido_id, produto_id, cantidade ) VALUES ( :pedido_id, :produto_id, :cantidade )' );
E a páxina PHP:
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); $clienteId = 10; $produtos = [ [ 'produto_id' => 5, 'cantidade' => 2 ], [ 'produto_id' => 8, 'cantidade' => 1 ] ]; $conex->beginTransaction(); /* * Creamos o pedido. */ $stmtPedido = $conex->prepare( INSERIR_PEDIDO ); $stmtPedido->execute([ ':cliente_id' => $clienteId ]); /* * Recuperamos o identificador antes * de finalizar a transacción. */ $pedidoId = (int) $conex->lastInsertId(); /* * Preparamos unha única vez a consulta * que imos executar varias veces. */ $stmtLina = $conex->prepare( INSERIR_LINA_PEDIDO ); foreach ($produtos as $produto) { $stmtLina->execute([ ':pedido_id' => $pedidoId, ':produto_id' => $produto['produto_id'], ':cantidade' => $produto['cantidade'] ]); } /* * Todas as operacións remataron ben. */ $conex->commit(); } catch (PDOException $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); }
Fixarse en varios detalles:
- as consultas variables son preparadas;
- a consulta das liñas se prepara unha soa vez e se reutiliza dentro do bucle;
lastInsertId()se consulta antes de finalizar a transacción;- se calquera
execute()lanza unha excepción, non se chega aocommit(); - o bloque
catchrealiza orollBack(); - non se mostra ao usuario o texto interno da excepción.
Que operacións deben incluírse na transacción?
Unha transacción debe ser o máis curta posible, pero debe conter todas as operacións que forman unha única unidade lóxica.
Por exemplo, non deberiamos facer:
$conex->beginTransaction(); INSERT ... // Procesamento moi longo. // Chamadas externas. // Interacción co usuario. // Esperas. UPDATE ... $conex->commit();
porque durante unha transacción poden manterse bloqueos sobre filas e índices.
O recomendable é:
- validar previamente todos os datos que poidamos validar en PHP;
- preparar os datos necesarios;
- iniciar a transacción;
- realizar unicamente as operacións da base de datos necesarias;
- confirmar ou desfacer;
- finalizar a transacción canto antes.
Sentenzas DDL e COMMIT implícito
Debemos evitar executar sentenzas de definición de datos dentro dunha transacción MySQL.
Por exemplo:
CREATE TABLE ... ALTER TABLE ... DROP TABLE ...
MySQL pode realizar un COMMIT implícito con este tipo de sentenzas.
Isto significa que non debemos esperar que:
$conex->rollBack();
poida desfacer sempre cambios anteriores se durante a transacción executamos sentenzas DDL.
As transaccións dos exemplos desta unidade estarán formadas fundamentalmente por operacións:
SELECT INSERT UPDATE DELETE
Niveis de illamento
Cando varias transaccións se executan simultaneamente debemos considerar como ven unhas transaccións os cambios realizados polas outras.
Isto está relacionado co nivel de illamento.
InnoDB soporta os niveis:
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
En MySQL/InnoDB o nivel por defecto é normalmente:
REPEATABLE READ
Problemas de concorrencia
Os niveis de illamento están relacionados con problemas como:
- lecturas sucias;
- lecturas non repetibles;
- lecturas fantasma;
- bloqueos entre transaccións.
Non debemos entender o illamento simplemente como "bloquear toda a táboa".
InnoDB utiliza mecanismos de concorrencia, instantáneas e bloqueos sobre rexistros e índices dependendo:
- do nivel de illamento;
- do tipo de consulta;
- dos índices empregados;
- das filas que se modifican ou bloquean.
Modificar o nivel de illamento
Normalmente non necesitaremos cambialo nos primeiros exemplos.
Se fose necesario, MySQL permite configurar o nivel dunha transacción mediante SQL.
Por exemplo:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
Debe facerse comprendendo previamente as consecuencias sobre a concorrencia e a consistencia.
Interbloqueos ou deadlocks
Un deadlock pode producirse cando dúas transaccións esperan mutuamente por recursos bloqueados pola outra.
Un exemplo simplificado:
TRANSACCIÓN A bloquea fila 1 espera pola fila 2 TRANSACCIÓN B bloquea fila 2 espera pola fila 1
InnoDB pode detectar o interbloqueo e desfacer unha das transaccións para permitir que a outra continúe.
Por este motivo, unha aplicación con moita concorrencia debe estar preparada para que unha transacción poida fallar e, cando resulte apropiado, repetir a operación.
Para reducir os deadlocks resulta útil:
- acceder ás táboas e filas nunha orde consistente;
- manter as transaccións curtas;
- utilizar índices axeitados;
- bloquear unicamente os datos necesarios.
Transaccións en procedementos almacenados
Unha transacción tamén pode controlarse desde un procedemento almacenado MySQL.
Neste caso debemos decidir onde se encontra o límite da transacción:
- alternativa A: a transacción é controlada por PHP/PDO;
- alternativa B: a transacción é controlada polo procedemento almacenado.
Non debemos iniciar unha transacción en PHP e, dentro dela, chamar a un procedemento que executa outro START TRANSACTION.
MySQL non admite transaccións anidadas reais. Un novo START TRANSACTION pode finalizar implicitamente a transacción anterior.
Alternativa A: transacción controlada por PDO
O procedemento almacenado non executa:
START TRANSACTION COMMIT ROLLBACK
PHP controla todo:
$conex->beginTransaction(); try { // CALL procedemento 1 // CALL procedemento 2 // OUTRAS OPERACIÓNS $conex->commit(); } catch (Throwable $e) { if ($conex->inTransaction()) { $conex->rollBack(); } throw $e; }
Esta opción é útil cando a unidade de traballo abrangue varias chamadas ou operacións diferentes realizadas desde PHP.
Alternativa B: transacción controlada polo procedemento
O procedemento pode encapsular completamente a operación.
Por exemplo:
DELIMITER $$ CREATE PROCEDURE PROCEDEMENTO_CON_TRANSACCION( IN p_param1 INT, IN p_param2 VARCHAR(50), IN p_param3 VARCHAR(200) ) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; INSERT INTO TABLE_1 ( col1, col2 ) VALUES ( p_param2, p_param3 ); INSERT INTO TABLE_2 ( col1, col2 ) VALUES ( p_param1, LAST_INSERT_ID() ); COMMIT; END$$ DELIMITER ;
Neste caso:
START TRANSACTIONinicia a transacción dentro do procedemento;- o
EXIT HANDLERcaptura calquera excepción SQL; ROLLBACKdesfai os cambios;RESIGNALvolve propagar o erro cara a PHP;COMMITsó se executa se todas as operacións anteriores rematan correctamente.
Desde PHP non iniciaremos outra transacción:
$stmt = $conex->prepare( 'CALL PROCEDEMENTO_CON_TRANSACCION( :param1, :param2, :param3 )' ); $stmt->execute([ ':param1' => $param1, ':param2' => $param2, ':param3' => $param3 ]); $stmt->closeCursor();
Se o procedemento lanza novamente o erro mediante RESIGNAL, PDO recibirá unha PDOException.
Exercicio proposto 1: asociar libros e autores
Queremos xestionar unha relación N:M entre libros e autores.
A idea é:
- seleccionar un libro nun
<select>; - amosar todos os autores;
- marcar mediante
checkboxos autores xa asociados ao libro; - permitir marcar ou desmarcar autores;
- ao premer ASOCIAR, substituír as asociacións actuais polas seleccionadas.
- seleccionar un libro nun
A operación final implica:
- borrar as asociacións existentes do libro;
- inserir unha fila por cada autor seleccionado.
Estas operacións deben formar unha transacción.
Arquivos
Empregaremos:
alta_libro_autores.php procesar_alta_libro_autores.php
- Libros-Autores
Arquivo bd.php
define( 'CONSULTAR_LIBROS', 'SELECT id, titulo FROM libros ORDER BY titulo' ); define( 'CONSULTAR_AUTORES', 'SELECT id, apelidos, nome FROM autores ORDER BY apelidos, nome' ); define( 'CONSULTAR_AUTORES_POR_LIBRO', 'SELECT autores.id, autores.apelidos, autores.nome FROM autores INNER JOIN libro_autor ON libro_autor.autor_id = autores.id WHERE libro_autor.libro_id = :libro_id ORDER BY autores.apelidos, autores.nome' ); define( 'INSERIR_LIBRO_AUTOR', 'INSERT INTO libro_autor ( libro_id, autor_id ) VALUES ( :libro_id, :autor_id )' ); define( 'BORRAR_LIBRO_AUTOR_POR_LIBRO', 'DELETE FROM libro_autor WHERE libro_id = :libro_id' );
Arrays enviados por formularios
Cada autor pode representarse cun checkbox:
<input type="checkbox" name="autores[]" value="10" >
Ao repetir:
name="autores[]"
PHP recibirá os autores seleccionados como un array:
$_POST['autores']
Se o usuario non selecciona ningún autor, o campo pode non chegar na petición.
Por iso empregaremos:
$autoresRecibidos = $_POST['autores'] ?? [];
Isto tamén permite eliminar todas as asociacións dun libro deixando todos os checkbox sen marcar.
Arquivo alta_libro_autores.php
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); /* * Lista de libros. */ $stmt = $conex->query( CONSULTAR_LIBROS ); $libros = $stmt->fetchAll(); $libroId = null; $autores = []; $autoresLibro = []; /* * Se o usuario seleccionou un libro, * cargamos todos os autores e as * asociacións existentes. */ if (isset($_POST['btnLibro'])) { $libroId = filter_input( INPUT_POST, 'lstLibros', FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); if ( $libroId === false || $libroId === null ) { throw new RuntimeException( 'O libro seleccionado non é válido.' ); } $stmt = $conex->query( CONSULTAR_AUTORES ); $autores = $stmt->fetchAll(); $stmt = $conex->prepare( CONSULTAR_AUTORES_POR_LIBRO ); $stmt->execute([ ':libro_id' => $libroId ]); $autoresLibro = array_map( 'intval', array_column( $stmt->fetchAll(), 'id' ) ); } } catch (PDOException $e) { error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); } ?> <!DOCTYPE html> <html lang="gl"> <head> <meta charset="UTF-8"> <title>Xestión de libros e autores</title> </head> <body> <h1>Asociar libros e autores</h1> <form method="post"> <label for="lstLibros"> Libro: </label> <select id="lstLibros" name="lstLibros" required > <option value=""> Escolle un libro </option> <?php foreach ($libros as $libro): ?> <?php $idLibro = (int) $libro['id']; ?> <option value="<?= $idLibro ?>" <?= $libroId === $idLibro ? 'selected' : '' ?> > <?= htmlspecialchars( $libro['titulo'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </option> <?php endforeach; ?> </select> <button type="submit" name="btnLibro" value="BUSCAR" > Buscar autores </button> </form> <?php if ($libroId !== null): ?> <form action="procesar_alta_libro_autores.php" method="post" > <input type="hidden" name="libro_id" value="<?= $libroId ?>" > <table> <thead> <tr> <th>Autor</th> <th>Asociado</th> </tr> </thead> <tbody> <?php foreach ($autores as $autor): ?> <?php $autorId = (int) $autor['id']; ?> <tr> <td> <?= htmlspecialchars( $autor['apelidos'] . ', ' . $autor['nome'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </td> <td> <input type="checkbox" name="autores[]" value="<?= $autorId ?>" <?= in_array( $autorId, $autoresLibro, true ) ? 'checked' : '' ?> > </td> </tr> <?php endforeach; ?> </tbody> </table> <button type="submit" name="btnAsociar" value="ASOCIAR" > Asociar </button> </form> <?php endif; ?> <?php if (isset($_GET['msg'])): ?> <p> <?= htmlspecialchars( $_GET['msg'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </p> <?php endif; ?> <?php if (isset($_GET['error'])): ?> <p> <?= htmlspecialchars( $_GET['error'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </p> <?php endif; ?> </body> </html>
Non empregamos rowCount() para comprobar se un SELECT devolveu filas.
Recuperamos os resultados mediante:
$stmt->fetchAll()
e traballamos directamente co array obtido.
Arquivo procesar_alta_libro_autores.php
Esta é a operación realmente transaccional.
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); if ( !isset($_POST['btnAsociar']) || $_POST['btnAsociar'] !== 'ASOCIAR' ) { throw new RuntimeException( 'Operación non válida.' ); } /* * Validamos o libro. */ $libroId = filter_input( INPUT_POST, 'libro_id', FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); if ( $libroId === false || $libroId === null ) { throw new RuntimeException( 'O libro non é válido.' ); } /* * Pode chegar un array baleiro * se non hai ningún autor seleccionado. */ $autoresRecibidos = $_POST['autores'] ?? []; if (!is_array($autoresRecibidos)) { throw new RuntimeException( 'Os autores non teñen un formato válido.' ); } /* * Validamos todos os identificadores * ANTES de iniciar a transacción. */ $autores = []; foreach ($autoresRecibidos as $autor) { $autorId = filter_var( $autor, FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); if ($autorId === false) { throw new RuntimeException( 'Existe un autor non válido.' ); } $autores[] = $autorId; } /* * Evitamos repetidos recibidos * nunha petición manipulada. */ $autores = array_values( array_unique($autores) ); /* * INICIO DA TRANSACCIÓN. */ $conex->beginTransaction(); /* * Eliminamos as asociacións anteriores. */ $stmtBorrar = $conex->prepare( BORRAR_LIBRO_AUTOR_POR_LIBRO ); $stmtBorrar->execute([ ':libro_id' => $libroId ]); /* * Preparamos unha única vez o INSERT. */ $stmtInserir = $conex->prepare( INSERIR_LIBRO_AUTOR ); foreach ($autores as $autorId) { $stmtInserir->execute([ ':libro_id' => $libroId, ':autor_id' => $autorId ]); } /* * Se chegamos aquí, todas as operacións * se realizaron correctamente. */ $conex->commit(); header( 'Location: alta_libro_autores.php?msg=' . urlencode( 'As asociacións foron actualizadas correctamente.' ) ); exit; } catch (PDOException $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); header( 'Location: alta_libro_autores.php?error=' . urlencode('Erro relacionado coa base de datos.') ); exit; } catch (Throwable $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); header( 'Location: alta_libro_autores.php?error=' . urlencode('Produciuse un erro na aplicación.') ); exit; }
Fixarse en que validamos todos os identificadores antes de iniciar a transacción.
Desta forma a transacción permanece aberta o menor tempo posible.
Exercicio proposto 2: alta dun libro e varios exemplares
Queremos crear unha páxina que permita:
- indicar o título dun libro;
- indicar cantos exemplares queremos dar de alta;
- amosar un segundo formulario con datos para cada exemplar;
- dar de alta o libro;
- obter o identificador autonumérico xerado;
- inserir todos os exemplares asociados ao novo libro.
Os datos de cada exemplar serán:
- estado;
- imaxe;
- prezo.
A operación debe ser atómica:
- se falla a alta de calquera exemplar, non debe quedar gardado nin o libro nin ningún dos exemplares.
- Libros-Exemplares
Deseño recomendado
Neste exercicio hai dúas alternativas válidas:
- realizar toda a transacción desde PDO;
- crear un único procedemento almacenado que encapsule a transacción completa.
Non é recomendable crear un procedemento para inserir o libro, outro para inserir cada exemplar e ademais iniciar unha transacción tanto en PHP como dentro dos procedementos.
Nesta solución principal faremos a transacción desde PDO.
Despois amosaremos unha alternativa cun procedemento almacenado.
Consultas necesarias
No arquivo bd.php:
define( 'CONSULTAR_ESTADOS', 'SELECT id, descripcion FROM estados ORDER BY descripcion' ); define( 'INSERIR_LIBRO', 'INSERT INTO libros (titulo) VALUES (:titulo)' ); define( 'INSERIR_EXEMPLAR', 'INSERT INTO exemplares ( libro_id, id_exemplar, imaxe, prezo, estado_id ) VALUES ( :libro_id, :id_exemplar, :imaxe, :prezo, :estado_id )' );
Se o título debe ser único, esta regra debería estar garantida tamén mediante unha restrición UNIQUE na base de datos.
Validación dos prezos
Un valor decimal procedente dun formulario chega a PHP como texto.
Podemos normalizar unha coma decimal a punto:
$prezoTexto = str_replace( ',', '.', trim($prezoTexto) );
e validalo:
$prezo = filter_var( $prezoTexto, FILTER_VALIDATE_FLOAT ); if ( $prezo === false || $prezo <= 0 ) { // Prezo non válido. }
PDO non dispón de PDO::PARAM_FLOAT.
Por iso, unha vez validado o decimal, podemos envialo como cadea:
$stmt->bindValue( ':prezo', (string) $prezo, PDO::PARAM_STR );
A columna MySQL correspondente seguirá sendo de tipo numérico, por exemplo DECIMAL(6,2).
Non sanitizamos a imaxe para gardala
Se neste exercicio o campo imaxe contén un nome ou ruta textual, validaremos a súa lonxitude e formato.
Por exemplo:
$imaxe = trim( $imaxeRecibida ); if ( $imaxe === '' || mb_strlen($imaxe, 'UTF-8') > 100 ) { // Dato non válido. }
Non empregaremos:
FILTER_SANITIZE_FULL_SPECIAL_CHARS
para transformar o dato antes de gardalo.
Se posteriormente se mostra nun documento HTML, empregaremos htmlspecialchars() nese momento.
Procesamento transaccional do libro e os exemplares
Supoñemos que o segundo formulario envía:
txtTitulo lstEstado[] txtImaxe[] txtPrezo[]
Antes de iniciar a transacción validaremos que os arrays existen e teñen o mesmo número de elementos.
<?php try { require '/var/www/confBD/bd.php'; $dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=utf8mb4', SERVIDOR, BD ); $conex = new PDO( $dsn, USUARIO, PASSWORD, [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] ); if ( !isset($_POST['btnAlta']) || $_POST['btnAlta'] !== 'ALTA' ) { throw new RuntimeException( 'Operación non válida.' ); } /* * Validamos o título. */ $titulo = trim( $_POST['txtTitulo'] ?? '' ); if ( $titulo === '' || mb_strlen($titulo, 'UTF-8') > 300 ) { throw new RuntimeException( 'O título non é válido.' ); } /* * Recuperamos os arrays. */ $estadosRecibidos = $_POST['lstEstado'] ?? []; $imaxesRecibidas = $_POST['txtImaxe'] ?? []; $prezosRecibidos = $_POST['txtPrezo'] ?? []; if ( !is_array($estadosRecibidos) || !is_array($imaxesRecibidas) || !is_array($prezosRecibidos) ) { throw new RuntimeException( 'Os datos dos exemplares non son válidos.' ); } $numExemplares = count($estadosRecibidos); if ( $numExemplares < 1 || $numExemplares > 9 || count($imaxesRecibidas) !== $numExemplares || count($prezosRecibidos) !== $numExemplares ) { throw new RuntimeException( 'O número de exemplares non é válido.' ); } /* * VALIDAMOS TODO ANTES DA TRANSACCIÓN. */ $exemplares = []; for ( $indice = 0; $indice < $numExemplares; $indice++ ) { $estadoId = filter_var( $estadosRecibidos[$indice], FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => 1 ] ] ); if ($estadoId === false) { throw new RuntimeException( 'Existe un estado non válido.' ); } $imaxe = trim( $imaxesRecibidas[$indice] ); if ( $imaxe === '' || mb_strlen( $imaxe, 'UTF-8' ) > 100 ) { throw new RuntimeException( 'Existe un nome de imaxe non válido.' ); } $prezoTexto = str_replace( ',', '.', trim( $prezosRecibidos[$indice] ) ); $prezo = filter_var( $prezoTexto, FILTER_VALIDATE_FLOAT ); if ( $prezo === false || $prezo <= 0 ) { throw new RuntimeException( 'Existe un prezo non válido.' ); } $exemplares[] = [ 'estado_id' => $estadoId, 'imaxe' => $imaxe, 'prezo' => $prezo ]; } /* * A PARTIR DE AQUÍ EMPEZA * A TRANSACCIÓN. */ $conex->beginTransaction(); /* * Inserimos o libro. */ $stmtLibro = $conex->prepare( INSERIR_LIBRO ); $stmtLibro->execute([ ':titulo' => $titulo ]); $libroId = (int) $conex->lastInsertId(); /* * Preparamos unha única vez * o INSERT dos exemplares. */ $stmtExemplar = $conex->prepare( INSERIR_EXEMPLAR ); foreach ( $exemplares as $indice => $exemplar ) { $stmtExemplar->bindValue( ':libro_id', $libroId, PDO::PARAM_INT ); $stmtExemplar->bindValue( ':id_exemplar', $indice + 1, PDO::PARAM_INT ); $stmtExemplar->bindValue( ':imaxe', $exemplar['imaxe'], PDO::PARAM_STR ); $stmtExemplar->bindValue( ':prezo', (string) $exemplar['prezo'], PDO::PARAM_STR ); $stmtExemplar->bindValue( ':estado_id', $exemplar['estado_id'], PDO::PARAM_INT ); $stmtExemplar->execute(); } /* * Todo foi correcto. */ $conex->commit(); header( 'Location: alta_libro_exemplares.php?msg=' . urlencode( 'Libro e exemplares dados de alta correctamente.' ) ); exit; } catch (PDOException $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); $codigoMySQL = $e->errorInfo[1] ?? null; if ($codigoMySQL === 1062) { $mensaxe = 'Xa existe un rexistro cos mesmos datos clave.'; } elseif ($codigoMySQL === 1452) { $mensaxe = 'Algún dos datos relacionados non existe.'; } else { $mensaxe = 'Erro relacionado coa base de datos.'; } header( 'Location: alta_libro_exemplares.php?error=' . urlencode($mensaxe) ); exit; } catch (Throwable $e) { if ( isset($conex) && $conex->inTransaction() ) { $conex->rollBack(); } error_log($e->getMessage()); header( 'Location: alta_libro_exemplares.php?error=' . urlencode('Produciuse un erro na aplicación.') ); exit; }
Neste exemplo unha excepción producida ao inserir calquera exemplar provoca o salto ao catch e o posterior rollBack().
Polo tanto, non debemos escribir lóxica do tipo:
if (!$stmt->execute()) { $error = '...'; break; } $conex->commit();
porque, con PDO::ERRMODE_EXCEPTION, o mecanismo natural é deixar que a excepción interrompa a secuencia e desfacer a transacción no catch.
Formulario para os exemplares
A lista de estados debe consultarse unha única vez:
$stmt = $conex->query( CONSULTAR_ESTADOS ); $estados = $stmt->fetchAll();
Despois reutilizamos o array para todos os exemplares:
<?php for ( $i = 0; $i < $numExemplares; $i++ ): ?> <tr> <td> <select name="lstEstado[]" required > <?php foreach ( $estados as $estado ): ?> <option value="<?= (int) $estado['id'] ?>" > <?= htmlspecialchars( $estado['descripcion'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ) ?> </option> <?php endforeach; ?> </select> </td> <td> <input type="text" name="txtImaxe[]" maxlength="100" required > </td> <td> <input type="number" name="txtPrezo[]" min="0.01" step="0.01" required > </td> </tr> <?php endfor; ?>
Ao usar:
lstEstado[] txtImaxe[] txtPrezo[]
os índices dos tres arrays permiten relacionar os datos do mesmo exemplar.
Alternativa: procedemento almacenado para libro e exemplares
Se queremos que a transacción sexa controlada por MySQL, o ideal é que a unidade de traballo completa estea encapsulada no procedemento.
Para un número variable de exemplares isto require deseñar como se enviarán todos os datos ao servidor.
Un procedemento que unicamente insire o libro e outro que insire individualmente cada exemplar non encapsulan por si sós a unidade completa se PHP é quen realiza múltiples chamadas.
Por iso temos dúas opcións:
- manter a transacción en PDO, como no exemplo anterior;
- deseñar un procedemento capaz de recibir e procesar todos os exemplares dentro dunha única chamada.
A primeira opción é máis sinxela para este exercicio didáctico.
Exemplo sinxelo de transacción dentro dun procedemento
Para un caso cun número fixo de operacións:
DELIMITER $$ CREATE PROCEDURE INSERIR_LIBRO_E_EXEMPLAR( IN p_titulo VARCHAR(300), IN p_imaxe VARCHAR(100), IN p_prezo DECIMAL(6,2), IN p_estado_id INT ) BEGIN DECLARE v_libro_id INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; INSERT INTO libros (titulo) VALUES (p_titulo); SET v_libro_id = LAST_INSERT_ID(); INSERT INTO exemplares ( libro_id, id_exemplar, imaxe, prezo, estado_id ) VALUES ( v_libro_id, 1, p_imaxe, p_prezo, p_estado_id ); COMMIT; END$$ DELIMITER ;
Neste caso PHP simplemente chama ao procedemento e non inicia outra transacción.
mysqli como ampliación
PDO será o mecanismo principal empregado nos apuntes.
Con mysqli o patrón equivalente é:
<?php try { require '/var/www/confBD/bd.php'; mysqli_report( MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT ); $conex = new mysqli( SERVIDOR, USUARIO, PASSWORD, BD ); $conex->set_charset( 'utf8mb4' ); $conex->begin_transaction(); /* * Operacións preparadas. */ $conex->commit(); } catch (mysqli_sql_exception $e) { if (isset($conex)) { try { $conex->rollback(); } catch (Throwable) { // Non ocultamos a excepción orixinal. } } error_log($e->getMessage()); die('Erro relacionado coa base de datos.'); } catch (Throwable $e) { if (isset($conex)) { try { $conex->rollback(); } catch (Throwable) { // Non ocultamos a excepción orixinal. } } error_log($e->getMessage()); die('Produciuse un erro na aplicación.'); }
Exemplo mysqli con consultas preparadas
$conex->begin_transaction(); $stmt = $conex->prepare( 'INSERT INTO pedidos (cliente_id) VALUES (?)' ); $stmt->bind_param( 'i', $clienteId ); $stmt->execute(); $pedidoId = $conex->insert_id; $stmtLina = $conex->prepare( 'INSERT INTO lina_pedido ( pedido_id, produto_id, cantidade ) VALUES (?, ?, ?)' ); foreach ($produtos as $produto) { $produtoId = $produto['produto_id']; $cantidade = $produto['cantidade']; $stmtLina->bind_param( 'iii', $pedidoId, $produtoId, $cantidade ); $stmtLina->execute(); } $conex->commit();
Non é necesario empregar o patrón antigo:
$conex->autocommit(false); // ... $conex->autocommit(true);
como forma principal de explicar as transaccións.
Para código novo resulta máis claro iniciar explicitamente a unidade de traballo con:
$conex->begin_transaction();
e rematala con:
$conex->commit();
ou:
$conex->rollback();
Exercicios propostos
Subida de imaxes
Modificar o exercicio de libros e exemplares para que a imaxe deixe de ser simplemente un campo de texto e poida subirse un ficheiro ao servidor.
A subida do ficheiro e a transacción da base de datos son dous mecanismos diferentes.
Un ROLLBACK da base de datos non elimina automaticamente un ficheiro que xa se gardase no sistema de ficheiros.
Polo tanto haberá que deseñar tamén como desfacer ou limpar os ficheiros creados se a operación da base de datos falla.
Asociar autores durante a alta do libro
Ampliar o exercicio de alta dun libro para permitir seleccionar tamén os autores.
A operación completa podería incluír:
- inserir o libro;
- obter o identificador xerado;
- inserir os exemplares;
- inserir as relacións en
LIBRO_AUTOR; - confirmar todas as operacións mediante un único
COMMIT.
Se falla calquera operación deberá realizarse ROLLBACK.
Actualización dunha relación N:M
Seleccionar unha atracción e amosar todos os artistas mediante checkbox.
Os artistas actualmente relacionados aparecerán marcados.
Ao gardar:
- eliminar as asociacións anteriores;
- inserir as asociacións seleccionadas;
- realizar todo dentro dunha única transacción.
Resumo
- Unha transacción permite agrupar varias modificacións nunha única unidade lóxica.
- As operacións confírmanse mediante
COMMIT. - Se se produce un erro, desfanse mediante
ROLLBACK. - Con PDO iniciamos a transacción mediante
beginTransaction(). beginTransaction()desactiva temporalmente o autocommit para esa conexión.- Antes de facer
rollBack()podemos comprobarinTransaction(). - PDO será o mecanismo principal empregado nos exemplos.
- As consultas con valores variables realizaranse con
prepare(). - Os datos deben validarse antes de iniciar a transacción sempre que sexa posible.
- As transaccións deben manterse abertas o menor tempo posible.
- Un erro SQL con
PDO::ERRMODE_EXCEPTIONlanza unhaPDOException. - As excepcións permiten interromper a secuencia e realizar o
ROLLBACKnocatch. - Non mostraremos directamente
$e->getMessage()aos usuarios. - Empregaremos
error_log()para rexistrar o detalle técnico. - Debemos empregar un motor transaccional como InnoDB.
- As sentenzas DDL poden provocar un
COMMITimplícito en MySQL. - InnoDB soporta os niveis
READ UNCOMMITTED,READ COMMITTED,REPEATABLE READeSERIALIZABLE. - O nivel habitual por defecto de InnoDB é
REPEATABLE READ. - Os deadlocks poden producirse mesmo con transaccións correctamente deseñadas e unha aplicación concorrente debe estar preparada para tratalos.
- MySQL non admite transaccións anidadas reais.
- Se a transacción é controlada polo procedemento almacenado, PHP non debe iniciar outra arredor da mesma operación.
- Nun procedemento almacenado podemos empregar un
EXIT HANDLER,ROLLBACKeRESIGNAL. - Nunha relación N:M, borrar as asociacións antigas e inserir as novas é un caso típico para empregar unha transacción.
- Se un conxunto de checkboxes pode quedar totalmente desmarcado, empregaremos
$_POST['campo'] ?? []. - Para inserir varias filas coa mesma consulta é recomendable preparar a sentenza unha soa vez e reutilizala.
lastInsertId()permite obter o identificador xerado para empregalo nas seguintes operacións da mesma unidade de traballo.- Un
ROLLBACKda base de datos non desfai automaticamente cambios externos, como ficheiros gardados no sistema de ficheiros. mysqliqueda como ampliación e tamén debe empregar consultas preparadas.
-- Ángel D. Fernández González -- (2026).