Dia 14 — Transações
BEGIN, COMMIT e ROLLBACK
O que uma transação faz
A transação é a promessa de que um grupo de operações vai todas entrar, ou
nenhuma entra. Isso se chama atomicidade, e a tradução é tudo ou nada: não
existe estado intermediário em que a metade foi gravada.
São três comandos — START TRANSACTION, COMMIT e ROLLBACK. Confirmar é o
COMMIT; desfazer é o ROLLBACK. No SQL eles são escritos à mão; no Node, a
transação no Node é a mesma coisa com nomes de método:
await conexao.beginTransaction(); // ... as operações ... await conexao.commit(); // grava tudo await conexao.rollback(); // desfaz tudo
beginTransaction() não recebe nada e não devolve nada de útil: ela só marca o
começo. O que fecha o grupo é sempre o commit ou o rollback, e sem um dos
dois o processo pode encerrar com a transação aberta — o end() fecha a conexão,
e conexão fechada com transação aberta faz o servidor descartar o trabalho.
Ela vê o próprio trabalho; os outros só depois do commit
O exemplo abre uma transação, insere duas contas, e consulta de dois lugares
diferentes:
Duas conexões do mesmo banco, discordando até o commit. **É por isso que a
transação é da conexão** — não é do banco nem do servidor.
O rollback desfaz o grupo inteiro
A carla sumiu e a ana voltou a 100.00. As duas operações foram desfeitas,
não só a última — e é essa a parte que um INSERT seguido de UPDATE deixa
confuso.
O caso de uso: a transferência
Sacar 5000.00 de uma conta com 100.00:
const [saque] = await conexao.query( 'UPDATE tb_conta SET vl_saldo = vl_saldo - ? WHERE nm_titular = ? AND vl_saldo >= ?', [5000.00, 'ana', 5000.00] ); // affectedRows: 0
O filtro AND vl_saldo >= ? está no próprio SQL. O banco decide, não o código:
é ele que lê o saldo e compara no mesmo instante do UPDATE. Se o código
fizesse "lê o saldo, se ≥ 5000 então escreve", dois saques simultâneos poderiam
ler o mesmo saldo e passar os dois. **A condição dentro do UPDATE é o que fecha
essa porta.**
O erro não desfaz nada sozinho
O banco só sabe que a instrução falhou; **quem desfaz o grupo é o rollback no
catch**. E é por isso que o rollback fica no catch e não depois do
commit: se ele sumisse, o primeiro INSERT ficaria gravado e a transação
ficaria aberta.
TRUNCATE e CREATE fazem commit implícito
Esta é a medição que desmente a crença mais comum. O exemplo faz TRUNCATE
dentro da transação, chama rollback, e conta:
O rollback não pode ter desfeito o TRUNCATE, porque ele desfaria. Logo o
TRUNCATE fez commit implícito: a transação terminou ali, e o rollback
seguinte não tinha mais transação aberta para desfazer.
O mesmo vale para CREATE TABLE, ALTER TABLE e DROP TABLE. É por isso que o
exemplo cria a tabela antes do beginTransaction() — se criasse dentro, o
CREATE finalizaria a transação e o rollback não teria o que desfazer.
A transação é da conexão
O pool do mysql2 tem a mesma regra: pool.getConnection() entrega uma
conexão, a transação é dela, e ela precisa voltar ao pool com
connection.release(). Uma transação begun em uma conexão e commitada em outra
não existe — o COMMIT sai na conexão que não tem transação aberta, e o dado
some quando ela fecha sozinha.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 14: o que uma transacao faz. // // A transacao e a promessa de que um grupo de operacoes vai TODAS entrar, ou // NENHUMA entra. O exemplo faz tres coisas na mesma transacao — criar tabela, // inserir, ler — e mostra que, ate o `commit`, ninguem de fora ve nada. // // Duas coisas que a aula mede e que a intuicao erra: // // 1. A transacao e da CONEXAO, nao do banco nem do servidor. Duas queries // separadas por `await` na MESMA conexao estao na mesma transacao; a mesma // query em DUAS conexoes nao esta. // // 2. `TRUNCATE` e `CREATE TABLE` causam IMPLICIT COMMIT. A tabela da secao 5 // prova isso: se o `TRUNCATE` fosse desfeito pelo `rollback`, a tabela // continuaria vazia; ela volta com as linhas, o que prova que o `TRUNCATE` // gravou fora da transacao. const { createConnection } = require('mysql2/promise'); async function main() { const conexao = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); // Uma segunda conexao, so para provar que "nao commitou" quer dizer alguma // coisa para quem olha de fora. Se as duas enxergassem o mesmo estado nao // haveria transacao nenhuma. const outra = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); try { // =================================================================== // 1. a transacao mais simples que existe // =================================================================== console.log('=== 1. begin, faz, commit ==='); await conexao.query('DROP TABLE IF EXISTS tb_d14a1_conta'); await conexao.query(` CREATE TABLE tb_d14a1_conta ( id INT AUTO_INCREMENT PRIMARY KEY, nm_titular VARCHAR(30) NOT NULL, vl_saldo DECIMAL(10,2) NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); console.log('tabela criada FORA de transacao (o CREATE ja commitou)'); const [vazioAntes] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d14a1_conta'); console.log('linhas antes da transacao:', vazioAntes[0].n); await conexao.beginTransaction(); console.log('beginTransaction(): a transacao comecou'); await conexao.query('INSERT INTO tb_d14a1_conta (nm_titular, vl_saldo) VALUES (?, ?)', ['ana', 100.00]); console.log(" INSERT da ana, 100.00 — dentro da transacao"); await conexao.query('INSERT INTO tb_d14a1_conta (nm_titular, vl_saldo) VALUES (?, ?)', ['bruno', 50.00]); console.log(" INSERT do bruno, 50.00 — dentro da transacao"); // A CONTA propria, de dentro da transacao: ve as duas insercoes. const [deDentro] = await conexao.query('SELECT nm_titular, vl_saldo FROM tb_d14a1_conta ORDER BY id'); console.log(' quem ve de DENTRO da transacao:', deDentro.length, 'linha(s):', deDentro.map((c) => c.nm_titular).join(', ')); // A OUTRA conexao: nao ve nada, porque nao houve commit. const [deFora] = await outra.query('SELECT COUNT(*) AS n FROM tb_d14a1_conta'); console.log(' quem ve de FORA, na outra conexao:', deFora[0].n, 'linha(s)'); await conexao.commit(); console.log('commit(): as duas insercoes gravaram'); const [deForaDepois] = await outra.query('SELECT COUNT(*) AS n FROM tb_d14a1_conta'); console.log(' quem ve de FORA, depois do commit:', deForaDepois[0].n, 'linha(s)'); console.log(''); console.log('Repara no que o exemplo mediu: a transacao ve o proprio trabalho na'); console.log('hora, e os outros so veem depois do commit. E por isso que a'); console.log('transacao e da CONEXAO: a conta de dentro e a conta de fora sao'); console.log('conexoes diferentes do mesmo banco, e discordam ate o commit.'); // =================================================================== // 2. o rollback: a mesma coisa, escrita ao contrario // =================================================================== console.log(''); console.log('=== 2. o rollback: o mesmo grupo de operacoes, desfeito ==='); await conexao.beginTransaction(); await conexao.query('INSERT INTO tb_d14a1_conta (nm_titular, vl_saldo) VALUES (?, ?)', ['carla', 999.00]); await conexao.query('UPDATE tb_d14a1_conta SET vl_saldo = vl_saldo * 2 WHERE nm_titular = ?', ['ana']); const [deDentro2] = await conexao.query('SELECT nm_titular, vl_saldo FROM tb_d14a1_conta ORDER BY id'); console.log('de dentro da transacao, depois dos dois comandos:'); for (const c of deDentro2) console.log(' ' + c.nm_titular.padEnd(6) + ' ' + c.vl_saldo); console.log(' a carla existe e a ana esta com o saldo dobrado — AINDA DENTRO.'); await conexao.rollback(); console.log('rollback(): o grupo inteiro foi desfeito'); const [depoisRollback] = await conexao.query('SELECT nm_titular, vl_saldo FROM tb_d14a1_conta ORDER BY id'); console.log('depois do rollback:'); for (const c of depoisRollback) console.log(' ' + c.nm_titular.padEnd(6) + ' ' + c.vl_saldo); console.log(''); console.log('A carla sumiu e a ana voltou a 100.00. As DUAS operacoes foram'); console.log('desfeitas, e nao so a ultima — e essa e a parte que um `INSERT` seguido'); console.log('de um `UPDATE` deixa confuso.'); // =================================================================== // 3. o caso de uso: a transferencia // =================================================================== // // Este e o motivo de a transacao existir. Sem ela, o `UPDATE` da conta A roda // e o da conta B falha: dinheiroSome do banco sem entrar na outra conta. Com // ela, ou os dois entram, ou nenhum. console.log(''); console.log('=== 3. o caso de uso: a transferencia ==='); await conexao.beginTransaction(); // Saque da conta da ana: o banco RECUSA saldo insuficiente, e o exemplo // mede a recusa. const [saque] = await conexao.query( 'UPDATE tb_d14a1_conta SET vl_saldo = vl_saldo - ? WHERE nm_titular = ? AND vl_saldo >= ?', [5000.00, 'ana', 5000.00] ); console.log("tentou sacar 5000.00 de uma conta com 100.00:"); console.log(' affectedRows:', saque.affectedRows, '<-- a clausula `vl_saldo >= ?` nao casou'); await conexao.rollback(); console.log(' rollback, e a conta da ana nem chegou a ser tocada.'); console.log(''); console.log('Repara que o filtro `AND vl_saldo >= ?` esta no proprio SQL. O banco'); console.log('decide, nao o codigo: e ele que le o saldo e compara no mesmo instante'); console.log('do `UPDATE`. Se o codigo fizesse "le o saldo, se >= 5000 entao'); console.log('escreve", dois saques simultaneos poderiam ler o mesmo saldo e passar'); console.log('os dois. A condicao dentro do `UPDATE` e o que fecha essa porta.'); // =================================================================== // 4. o erro que dispara o rollback // =================================================================== console.log(''); console.log('=== 4. o erro que dispara o rollback ==='); await conexao.beginTransaction(); try { await conexao.query('INSERT INTO tb_d14a1_conta (nm_titular, vl_saldo) VALUES (?, ?)', ['dani', 10.00]); console.log(' gravou a dani'); // Viola a chave primaria de proposito: o `id` 1 ja existe. await conexao.query('INSERT INTO tb_d14a1_conta (id, nm_titular, vl_saldo) VALUES (1, ?, ?)', ['erro', 0]); console.log(' isso NAO deveria imprimir: o INSERT tem que falhar'); await conexao.commit(); } catch (erro) { await conexao.rollback(); console.error(' ' + erro.code + ': ' + erro.message); console.log(' o erro NAO foi tratado dentro da transacao — ele subiu para o `catch`,'); console.log(' e o `catch` e quem decide o destino do grupo.'); } const [temDani] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d14a1_conta WHERE nm_titular = ?', ['dani']); console.log(' a dani esta na tabela?', temDani[0].n > 0 ? 'SIM (erro grave)' : 'nao — o rollback pegou ela tambem'); console.log(''); console.log('O erro nao desfaz nada sozinho. O banco so sabe que a instrucao'); console.log('falhou; quem desfaz o grupo e o `rollback` no `catch`. E por isso que'); console.log('o `rollback` fica no `catch` e nao depois do `commit`: se ele sumisse,'); console.log('o primeiro `INSERT` ficaria gravado e a transacao ficaria aberta.'); // =================================================================== // 5. TRUNCATE e CREATE fazem commit implicito // =================================================================== // // Esta secao e a medicao que desmente a crenca mais comum sobre transacao. // Se `TRUNCATE` fosse desfeito pelo `rollback`, a tabela voltaria com as // linhas. Ela volta VAZIA, e isso prova que o `TRUNCATE` gravou fora. console.log(''); console.log('=== 5. TRUNCATE e CREATE: commit implicito ==='); await conexao.query('DROP TABLE IF EXISTS tb_d14a1_implicito'); await conexao.query('CREATE TABLE tb_d14a1_implicito (id INT PRIMARY KEY, nm VARCHAR(20))'); await conexao.query("INSERT INTO tb_d14a1_implicito VALUES (1, 'a'), (2, 'b'), (3, 'c')"); console.log('tabela de 3 linhas, fora de transacao'); await conexao.beginTransaction(); await conexao.query('TRUNCATE TABLE tb_d14a1_implicito'); const [dentroTrunc] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d14a1_implicito'); console.log('dentro da transacao, depois do TRUNCATE:', dentroTrunc[0].n, 'linha(s)'); await conexao.rollback(); console.log('rollback chamado'); const [depoisTrunc] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d14a1_implicito'); console.log('depois do rollback:', depoisTrunc[0].n, 'linha(s)'); console.log(''); if (depoisTrunc[0].n === 0) { console.log('MEDIDO: a tabela continua VAZIA. O `rollback` nao pode ter'); console.log('desfeito o `TRUNCATE`, porque ele desfaria. Logo o `TRUNCATE` fez'); console.log('COMMIT IMPLICITO: a transacao terminou ali, e o `rollback` seguinte'); console.log('nao tinha mais transacao aberta para desfazer.'); } else { console.log('MEDIDO: a tabela voltou com as linhas, o que significaria que o'); console.log('TRUNCATEParticipou da transacao.'); } console.log(''); console.log('E o mesmo vale para `CREATE TABLE`, `ALTER TABLE` e `DROP TABLE`:'); console.log('todos fecham a transacao. Por isso o exemplo cria a tabela ANTES do'); console.log('`beginTransaction()` e nao dentro dele — se criasse dentro, o `CREATE`'); console.log('finalizaria a transacao e o `rollback` nao teria o que desfazer.'); // =================================================================== // 6. a transacao e da conexao // =================================================================== console.log(''); console.log('=== 6. a transacao e da CONEXAO ==='); console.log('esta transacao:'); await conexao.beginTransaction(); await conexao.query('INSERT INTO tb_d14a1_conta (nm_titular, vl_saldo) VALUES (?, ?)', ['eva', 5.00]); const [evaA] = await conexao.query("SELECT COUNT(*) AS n FROM tb_d14a1_conta WHERE nm_titular = 'eva'"); console.log(' a conexao que comecou ve a eva?', evaA[0].n > 0 ? 'sim' : 'nao'); await conexao.commit(); const [evaB] = await outra.query("SELECT COUNT(*) AS n FROM tb_d14a1_conta WHERE nm_titular = 'eva'"); console.log(' depois do commit, a outra conexao ve?', evaB[0].n > 0 ? 'sim' : 'nao'); console.log(''); console.log('O `pool` do mysql2 tem a mesma regra: `pool.getConnection()` entrega uma'); console.log('conexao, a transacao e DELA, e ela precisa voltar ao pool com'); console.log('`connection.release()`. Uma transacao begun em uma conexao e commitada'); console.log('em outra nao existe — o `COMMIT` sai na conexao que nao tem transacao'); console.log('aberta, e o dado some da transacao quando ela fecha sozinha.'); } finally { await conexao.query('DROP TABLE IF EXISTS tb_d14a1_conta'); await conexao.query('DROP TABLE IF EXISTS tb_d14a1_implicito'); await outra.query('DROP TABLE IF EXISTS tb_d14a1_conta'); await outra.query('DROP TABLE IF EXISTS tb_d14a1_implicito'); await outra.end(); await conexao.end(); console.log(''); console.log('tabelas de teste removidas; as duas conexoes encerradas com end().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
=== 1. begin, faz, commit ===
tabela criada FORA de transacao (o CREATE ja commitou)
linhas antes da transacao: 0
beginTransaction(): a transacao comecou
INSERT da ana, 100.00 — dentro da transacao
INSERT do bruno, 50.00 — dentro da transacao
quem ve de DENTRO da transacao: 2 linha(s): ana, bruno
quem ve de FORA, na outra conexao: 0 linha(s)
commit(): as duas insercoes gravaram
quem ve de FORA, depois do commit: 2 linha(s)
Repara no que o exemplo mediu: a transacao ve o proprio trabalho na
hora, e os outros so veem depois do commit. E por isso que a
transacao e da CONEXAO: a conta de dentro e a conta de fora sao
conexoes diferentes do mesmo banco, e discordam ate o commit.
=== 2. o rollback: o mesmo grupo de operacoes, desfeito ===
de dentro da transacao, depois dos dois comandos:
ana 200.00
bruno 50.00
carla 999.00
a carla existe e a ana esta com o saldo dobrado — AINDA DENTRO.
rollback(): o grupo inteiro foi desfeito
depois do rollback:
ana 100.00
bruno 50.00
A carla sumiu e a ana voltou a 100.00. As DUAS operacoes foram
desfeitas, e nao so a ultima — e essa e a parte que um `INSERT` seguido
de um `UPDATE` deixa confuso.
=== 3. o caso de uso: a transferencia ===
tentou sacar 5000.00 de uma conta com 100.00:
affectedRows: 0 <-- a clausula `vl_saldo >= ?` nao casou
rollback, e a conta da ana nem chegou a ser tocada.
Repara que o filtro `AND vl_saldo >= ?` esta no proprio SQL. O banco
decide, nao o codigo: e ele que le o saldo e compara no mesmo instante
do `UPDATE`. Se o codigo fizesse "le o saldo, se >= 5000 entao
escreve", dois saques simultaneos poderiam ler o mesmo saldo e passar
os dois. A condicao dentro do `UPDATE` e o que fecha essa porta.
=== 4. o erro que dispara o rollback ===
gravou a dani
o erro NAO foi tratado dentro da transacao — ele subiu para o `catch`,
e o `catch` e quem decide o destino do grupo.
a dani esta na tabela? nao — o rollback pegou ela tambem
O erro nao desfaz nada sozinho. O banco so sabe que a instrucao
falhou; quem desfaz o grupo e o `rollback` no `catch`. E por isso que
o `rollback` fica no `catch` e nao depois do `commit`: se ele sumisse,
o primeiro `INSERT` ficaria gravado e a transacao ficaria aberta.
=== 5. TRUNCATE e CREATE: commit implicito ===
tabela de 3 linhas, fora de transacao
dentro da transacao, depois do TRUNCATE: 0 linha(s)
rollback chamado
depois do rollback: 0 linha(s)
MEDIDO: a tabela continua VAZIA. O `rollback` nao pode ter
desfeito o `TRUNCATE`, porque ele desfaria. Logo o `TRUNCATE` fez
COMMIT IMPLICITO: a transacao terminou ali, e o `rollback` seguinte
nao tinha mais transacao aberta para desfazer.
E o mesmo vale para `CREATE TABLE`, `ALTER TABLE` e `DROP TABLE`:
todos fecham a transacao. Por isso o exemplo cria a tabela ANTES do
`beginTransaction()` e nao dentro dele — se criasse dentro, o `CREATE`
finalizaria a transacao e o `rollback` nao teria o que desfazer.
=== 6. a transacao e da CONEXAO ===
esta transacao:
a conexao que comecou ve a eva? sim
depois do commit, a outra conexao ve? sim
O `pool` do mysql2 tem a mesma regra: `pool.getConnection()` entrega uma
conexao, a transacao e DELA, e ela precisa voltar ao pool com
`connection.release()`. Uma transacao begun em uma conexao e commitada
em outra nao existe — o `COMMIT` sai na conexao que nao tem transacao
aberta, e o dado some da transacao quando ela fecha sozinha.
tabelas de teste removidas; as duas conexoes encerradas com end().
Quando usar transação num caso real
Transação que dá errado e volta atrás
A aula 1 mostrou a transação pelo lado feliz. Esta é pelo lado que importa: o
erro no meio do grupo, e o que precisa acontecer para o banco não ficar pela
metade.
O exemplo de transação desta aula é uma venda com estoque, que é o caso em que
o erro no meio é garantido: a primeira linha entra e a segunda viola a chave
primária. O motivo de o exemplo ser esse e não um erro simulado é que a
falha de pagamento real acontece exatamente assim — o pagamento chega, o pedido
é gravado, e a linha seguinte falha por uma chave que já existe.
A semente: por que o exemplo grava um id na mão
await conexao.query('DELETE FROM tb_d14a2_transacao'); await conexao.query( 'INSERT INTO tb_d14a2_transacao (id, nm_item, qtd) VALUES (1, ?, ?)', ['semente', 1] );
O id 1 é uma semente: existe antes da transação, e é o que garante que a
duplicata do meio do exemplo sempre falhe.
Chutar um id livre não funciona. DELETE não reinicia o AUTO_INCREMENT, então a
primeira execução acha um id livre, a segunda não — e o exemplo "funciona" na
primeira vez e ensina errado na segunda. A semente é o que torna a duplicata
um fato, e não uma aposta.
O grupo que falha no meio
A primeira inserção (teclado, 2) foi gravada e printou
affectedRows: 1. Só depois veio a segunda, que viola a chave primária. O erro
não é tratado ali dentro: é a exceção que dispara o catch lá embaixo.
O rollback é o que faz a primeira linha sumir
} catch (erro) { await conexao.rollback(); console.error('rollback por ' + erro.code + ': ' + erro.message); }
Sem essa linha o exemplo ensinaria que a transação "salvou" algo que não
salvou. O ponto inteiro do exemplo está no número do fim:
O teclado não está lá. **O rollback no catch é a única coisa que desfaz o
grupo** — o banco não adivinha que você queria desfazer, e o erro sozinho não
volta nada. É sempre essa linha que faz o trabalho de desfazer tudo: sem ela,
a primeira inserção fica gravada e o banco guarda o grupo pela metade.
Por que o exemplo fecha contando as linhas
O caso real que a transação protege é o de uma transferência entre duas contas:
debitar de uma e creditar na outra, e as duas escritas têm de ser inseparáveis.
START TRANSACTION; UPDATE tb_conta SET vl_saldo = vl_saldo - ? WHERE id_origem = ?; UPDATE tb_conta SET vl_saldo = vl_saldo + ? WHERE id_destino = ?; COMMIT;
Duas tabelas, na prática — e o que faz a consistência não é o COMMIT, é o
rollback. Se a segunda escrita, creditar, falha, o debitar que já tinha dado
affectedRows: 1 precisa voltar atrás. Sem transação, o dinheiro sai de uma
conta e não chega na outra: o saldo total do conjunto diminui e nada denuncia.
O detalhe que separa o exemplo de verdade do exemplo de aula é o saldo: a
condição de saldo suficiente vai dentro do próprio UPDATE, e não em um if
antes dele. Quem lê o saldo, decide e escreve em três passos separados deixa
duas requisições competirem pelo mesmo valor — é a condição dentro do UPDATE
que fecha a porta.
O affectedRows do commit desaparece quando há rollback: não existe
linha afetada, porque nada foi gravado. Por isso a medição final é uma
SELECT COUNT(*), e não o retorno da inserção. O número que prova o
comportamento é o da contagem, e é por isso que ele está no fim.
console.error vai para o stderr
Repare nas duas linhas do erro:
A primeira é a lição, e usa console.log de propósito: é ela que aparece na
página da aula. A segunda é o detalhe técnico do driver. Se a primeira fosse
console.error, a página mostraria o exemplo "rodando" sem a parte que explica o
ponto.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 14: transacao que da errado e volta atrás. // // O ponto da aula e o `rollback`: sem ele, a segunda linha ficaria gravada e o // exemplo ensinaria o oposto do que prega. O `affectedRows` do `commit` // desaparece, e e por isso que o exemplo fecha contando as linhas. const { createConnection } = require('mysql2/promise'); async function main() { const conexao = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); await conexao.query(` CREATE TABLE IF NOT EXISTS tb_d14a2_transacao ( id INT AUTO_INCREMENT PRIMARY KEY, nm_item VARCHAR(40) NOT NULL, qtd INT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); // `TRUNCATE` no COMECO e nao no fim: a aula seguinte precisa achar a tabela // no estado que esta deixou. await conexao.query('DELETE FROM tb_d14a2_transacao'); // A linha com `id` 1 e uma SEMENTE: ela existe ANTES da transacao e e o que // garante que a duplicata do meio do exemplo sempre falhe. Chutar o id livre // nao funciona — `DELETE` nao reinicia o `AUTO_INCREMENT`, entao a primeira // execucacao acha um id livre, a segunda nao, e o exemplo "funciona" na // primeira vez e ensina errado na segunda. A semente e o que torna a // duplicata um fato, e nao uma aposta. await conexao.query( 'INSERT INTO tb_d14a2_transacao (id, nm_item, qtd) VALUES (1, ?, ?)', ['semente', 1]); const [antes] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d14a2_transacao'); console.log('linhas antes:', antes[0].n); console.log('o id 1 ja existe, gravado como semente'); try { await conexao.beginTransaction(); const [primeira] = await conexao.query( 'INSERT INTO tb_d14a2_transacao (nm_item, qtd) VALUES (?, ?)', ['teclado', 2]); console.log('dentro da transacao, gravou:', primeira.affectedRows, 'linha(s)'); // Esta segunda insercao viola a chave primaria de proposito: o `id` 1 ja // foi gravado pela semente. O erro NAO e tratado aqui: e a excecao que // dispara o `catch` la embaixo. await conexao.query( 'INSERT INTO tb_d14a2_transacao (id, nm_item, qtd) VALUES (1, ?, ?)', ['duplicado', 9]); await conexao.commit(); console.log('commit feito'); } catch (erro) { // O rollback e o que faz a primeira linha sumir. Sem esta linha o exemplo // ensinaria que a transacao "salvou" algo que nao salvou. await conexao.rollback(); console.error('rollback por ' + erro.code + ': ' + erro.message); console.log(' transacao desfeita por', erro.code); } const [depois] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d14a2_transacao'); console.log('linhas depois do rollback:', depois[0].n); console.log('voltou ao estado inicial: so a semente ficou, o teclado sumiu'); await conexao.end(); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
linhas antes: 1 o id 1 ja existe, gravado como semente dentro da transacao, gravou: 1 linha(s) transacao desfeita por ER_DUP_ENTRY linhas depois do rollback: 1 voltou ao estado inicial: so a semente ficou, o teclado sumiu