Dia 14 — Transações

Informatica · Conteudo · publicado em 30/09/2026
Dia 14 de 16

Transações

Aula 1

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().
Aula 2

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