Dia 12 — mysql2: ligar o Node no MySQL

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

mysql2: ligar o Node no MySQL

Aula 1

Instalar mysql2 e conectar

mysql2 e a conexão que o Node faz

mysql2 é o pacote que fala com o MySQL a partir do Node. Ele vem em duas

formas, e a distinção importa: require('mysql2') é a API com callbacks, e

require('mysql2/promise') é a mesma coisa devolvendo promises. Todo exemplo

de Node com banco usa a segunda.

A conexão com banco que o Node faz é um objeto, e ela nasce de uma função.

Há duas delas, e a escolha muda o custo:

funçãoo que devolvequando
createConnection (assinatura: mysql.createConnection(config))uma promessa que resolve na conexãoscript de uma vez, que abre e fecha
createPool (assinatura: mysql.createPool(config))uma promessa que resolve no poolservidor HTTP, que atende requisição o dia inteiro

O createPool resolve a espera de uma vez: o pool já vem com as conexões

prontas, e quem chama não espera nenhuma. Com o createConnection, o await

existe para esperar conectar — sem ele, conexao ainda é uma promessa, e

conexao.query dá TypeError.

O que entra na conexão

Cinco valores, e quatro deles vêm do ambiente:

CampoDe ondeObservação
hostprocess.env.DB_HOST127.0.0.1 quando o banco é local
portprocess.env.DB_PORTNumber(...): o .env dá texto, o driver quer número
userprocess.env.DB_USERo usuário, nunca a senha
passwordprocess.env.DB_PASSvem do .env, nunca escrito no arquivo
databaseprocess.env.DB_NAMEo banco onde as tabelas do exemplo vivem

O Number(process.env.DB_PORT) não é detalhe: sem ele o driver recebe a

string "3306" e falha ao conectar.

createConnection devolve uma promessa

createConnection é async, e quem espera é o await. Depois do await o

que se tem é a conexão, com os métodos que importam nesta aula:

MétodoAssinaturaO que devolve
queryquery(sql, valores)[linhas, campos]
executeexecute(sql, valores)o mesmo, com o driver preparado
beginTransactionbeginTransaction()promessa vazia
commitcommit()confirma a transação
rollbackrollback()desfaz a transação
endend()fecha a conexão

O query devolve dois valores, e isso é a fonte de erro mais comum da

aula: [linhas, campos]. Destruir com const [versao] pega as linhas; a

primeira linha é versao[0]. Escrever versao.versao devolve undefined sem

nenhum erro, e a página mostra undefined no lugar do dado.

Fechar a conexão

end() encerra a conexão. Sem ele o processo fica segurado pelo socket e não

termina sozinho. O exemplo fecha com await conexao.end() no fim.

O close é o nome do mesmo gesto no outro objeto: pool.end() fecha todas as

conexões do pool de uma vez, e é ele que o servidor HTTP chama no finally junto

com o close() do servidor. Já a conexão emprestada do pool se devolve com

connection.release() — devolver ao pool não é fechar, e fechar uma conexão

emprestada deixa o pool com uma conexão a menos sem querer.

Exemplo

async function main() {
  // Descarta a conexao que o harness entregou e abre a propria, para provar a
  // aula de "instalar mysql2 e conectar" do dia 12 do 2º trimestre.
  const { createConnection } = require('mysql2/promise');

  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,
  });

  // A versao e capturada do banco, nunca escrita a mao: a maquina de quem
  // estuda devolve o que ela tem.
  const [versao] = await conexao.query('SELECT VERSION() AS versao');
  console.log('Banco:', versao[0].versao);
  console.log('Banco em uso:', process.env.DB_NAME);

  // `poolSize` e 1 aqui porque o exemplo roda uma vez e sai; numa API de
  // verdade o pool tem varias conexoes.
  const [consulta] = await conexao.query('SELECT DATABASE() AS banco, USER() AS usuario');
  console.log('Conectado como:', consulta[0].usuario, 'em', consulta[0].banco);

  await conexao.end();
  console.log('Conexao encerrada com end().');
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

Banco: 10.11.14-MariaDB-0ubuntu0.24.04.1
Banco em uso: materiais_teste
Conectado como: materiais@localhost em materiais_teste
Conexao encerrada com end().
Aula 2

Executar consultas pelo Node

execute, query e o [linhas, campos]

A pegadinha central desta aula: query devolve dois valores, e usar o

primeiro como se fosse a linha devolve undefined sem nenhum erro.

const [linhas, campos] = await conexao.query('SELECT VERSION() AS versao');
//        ^^^^^^ array de linhas
//                          ^^^^^^ metadados das colunas

A forma errada, e o que ela devolve

const [versao] = await conexao.query('SELECT VERSION() AS versao');
console.log(versao.versao);        // undefined
console.log(versao[0].versao);     // o valor

versao é um array. Array não tem propriedade versao, então o acesso devolve

undefined em vez de lançar — o programa continua rodando e a página mostra

undefined no lugar do dado. É o pior tipo de bug: nada acusa.

A regra: nunca usar o resultado de um SELECT sem o [0], e perguntar

"achou algo?" com linhas.length === 0, nunca com undefined.

O que cada comando devolve

Executar SQL do Node é sempre o mesmo gesto: await da chamada, e o que volta

depende do comando. Um SELECT pelo Node e um INSERT pelo Node têm formatos

de retorno diferentes, e é essa diferença que o código precisa conhecer.

O resultado da consulta é o primeiro valor da desestruturação, e é ele que o

código lê. Para o SELECT, ler resultado é ler linhas[0]; para os outros,

é ler affectedRows.

ComandoPrimeiro valorSegundo
SELECT[linhas, campos] — linhas é array de objetosmetadados
INSERT{ affectedRows, insertId, warningStatus }undefined
UPDATE{ affectedRows, changedRows, info }undefined
DELETE{ affectedRows }undefined

Por isso const [algo] = await funciona igual em qualquer um: o que muda é o que

o objeto traz dentro.

Com o pool, é o mesmo query — o objeto é outro, o formato do retorno não muda:

const [linhas] = await pool.query('SELECT id FROM tb_curso WHERE turma = ?', [3]);

É o pool.query que o servidor HTTP usa em cada requisição, e a vantagem é a

mesma: quem chama não abre nem fecha conexão. O pool empresta uma, a consulta

roda, e a conexão volta para a fila sozinho.

execute e query

Como funcionaQuando ganha
querytexto pronto, o driver traduzconsulta de uma vez
executePREPARE, EXECUTE, CLOSE; o servidor guarda o planomesma consulta repetida com parâmetros diferentes

Os dois devolvem [linhas, campos] e o conteúdo é idêntico. O execute não

aceita ? onde o SQL espera identificador (nome de tabela ou coluna): o ?

do prepared statement é só para valor, porque identificador muda o plano de

execução — e o plano é justamente o que o servidor preparou. A tentativa dá

ER_PARSE_ERROR.

O multipleStatements: true (vários comandos separados por ; num só query)

só vale no query, porque no execute isso seria dois comandos no mesmo plano.

O que chega em Node, por tipo

OrigemChega como
INTnumber
VARCHARstring
DECIMALstring (o tipo é exato)
DOUBLEnumber
BOOLEAN (TRUE)number 1, não true
NULLnull
DATEobjeto Date, com fuso aplicado

TRUE voltar 1 é o MySQL guardando booleano como TINYINT. DECIMAL voltar

texto é o driver preservando a exatidão; quem precisa de número converte com

Number() e aceita o arredondamento.

dateStrings: a opção que muda o tipo sem mudar o SQL

const conexao = await mysql.createConnection({ /* ... */ dateStrings: true });

Com dateStrings: true, a coluna date volta como texto YYYY-MM-DD em vez de

objeto Date. O SQL é idêntico — o que muda é uma opção da conexão, e ela

muda o tipo que chega no Node. É por isso que a data lida como Date sai com um

dia a menos no toISOString(): o fuso entra antes do valor, e o servidor está em

UTC enquanto a máquina não.

Para mostrar data, o caminho é DATE_FORMAT(coluna, '%Y-%m-%d') no SQL, ou

dateStrings: true na conexão.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 12: `execute` e `query`, e o `[linhas, campos]`.
//
// A pegadinha que o portao pegou nesta aula: `query` devolve DOIS valores.
// Escrever `const [versao] = await conexao.query(...)` pega as LINHAS; a primeira
// delas e `versao[0]`. Escrever `versao.versao` devolve `undefined` sem erro
// nenhum, e a pagina mostra `undefined` no lugar do dado.
//
// O exemplo existe para deixar isso medido: ele mostra a forma errada, o que ela
// devolve, e a forma certa lado a lado. E mede a diferenca entre `execute` e
// `query`, que e a escolha real do dia.

const mysql = require('mysql2/promise');

async function main() {
  const conexao = await mysql.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,
    multipleStatements: true,
  });

  try {
    await conexao.query(`
      CREATE TABLE IF NOT EXISTS tb_d12a2_data (
        id          INT PRIMARY KEY,
        dt_cadastro DATE NOT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    await conexao.query("REPLACE INTO tb_d12a2_data (id, dt_cadastro) VALUES (1, '2026-05-04')");
    await conexao.query(`
      CREATE TABLE IF NOT EXISTS tb_d12a2_aluno (
        id       INT AUTO_INCREMENT PRIMARY KEY,
        nm_aluno VARCHAR(40) NOT NULL,
        turma    INT NOT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    // `TRUNCATE` no COMECO: a aula 15 usa a mesma tabela e precisa do estado que
    // este exemplo deixou.
    await conexao.query('TRUNCATE TABLE tb_d12a2_aluno');

    // --- 1. o que `query` devolve: DOIS valores ---
    //
    // A forma ERRADA, medida. `versao` e o ARRAY DE LINHAS, e `versao.versao`
    // nao existe: array nao tem propriedade `versao`, entao o JavaScript devolve
    // `undefined` sem lançar erro. E o pior tipo de bug — o programa continua
    // rodando e mostra `undefined` na pagina.
    const [versaoErrado] = await conexao.query('SELECT VERSION() AS versao');
    console.log('--- a pegadinha do [linhas, campos] ---');
    console.log("const [versao] = await query('SELECT VERSION() AS versao')");
    console.log('typeof versao:            ', typeof versaoErrado, '<-- e um ARRAY de linhas');
    console.log('Array.isArray(versao):    ', Array.isArray(versaoErrado));
    console.log('versao.versao:            ', versaoErrado.versao, '<-- undefined, SEM erro nenhum');
    console.log('versao[0].versao:         ', versaoErrado[0].versao, '<-- o valor esta no [0]');
    console.log('JSON.stringify(versao):   ', JSON.stringify(versaoErrado));
    console.log('');
    console.log('um array nao tem propriedade `versao`, entao o acesso devolve undefined');
    console.log('em vez de lancar. O programa continua e a pagina mostra undefined.');

    // A forma CERTA, com o segundo valor.
    const [versaoCerto, campos] = await conexao.query('SELECT VERSION() AS versao');
    console.log('');
    console.log('--- a forma certa, com os dois valores ---');
    console.log('const [linhas, campos] = await query(...)');
    console.log('linhas[0].versao:', versaoCerto[0].versao);
    console.log('campos: ' + campos.length + ' coluna(s), e o segundo valor so importa no SELECT');
    console.log('  campos[0].name =', campos[0].name, '| type =', campos[0].type);

    // --- 2. `INSERT`, `UPDATE`, `DELETE`: o que devolvem ---
    //
    // Fora do `SELECT`, o primeiro valor e um objeto de METADADOS e o segundo e
    // `undefined`. E por isso que `const [resultado] = await` funciona igual em
    // qualquer comando: o que muda e o que o objeto traz dentro.
    const [inserido] = await conexao.query(
      'INSERT INTO tb_d12a2_aluno (nm_aluno, turma) VALUES (?, ?), (?, ?), (?, ?)',
      ['Ana', 3, 'Bruno', 3, 'Carla', 4]
    );
    console.log('');
    console.log('--- INSERT ---');
    console.log('affectedRows:', inserido.affectedRows, '| insertId:', inserido.insertId);

    const [alterado] = await conexao.query(
      'UPDATE tb_d12a2_aluno SET turma = ? WHERE turma = ?', [5, 3]
    );
    console.log('--- UPDATE ---');
    console.log('affectedRows:', alterado.affectedRows, '| changedRows:', alterado.changedRows);
    console.log('info:', JSON.stringify(alterado.info));

    const [contagem] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d12a2_aluno');
    console.log('--- SELECT com agregacao ---');
    console.log('linhas[0].n:', contagem[0].n, '(o primeiro valor e o array, e a linha e [0])');

    // --- 3. `execute` e `query` ---
    //
    // `execute` usa o protocolo de "prepared statement" do MySQL: o texto do SQL
    // e os valores sao enviados em mensagens separadas, e o banco PREPARE, EXECUTE
    // e fecha. O servidor guarda o plano de execucao e reaproveita, que e o que
    // torna o `execute` mais rapido em consulta repetida.
    //
    // `query` envia o texto pronto e o driver traduz. Para consulta de uma vez,
    // os dois dao o mesmo resultado — e o que muda e o caminho.
    console.log('');
    console.log('--- execute contra query ---');
    const sql = 'SELECT nm_aluno, turma FROM tb_d12a2_aluno WHERE turma = ?';
    const [porQuery] = await conexao.query(sql, [5]);
    const [porExecute] = await conexao.execute(sql, [5]);
    console.log('');
    console.log('  query   devolveu', porQuery.length, 'linha(s)');
    console.log('  execute devolveu', porExecute.length, 'linha(s)');
    console.log('  o conteudo e identico:', JSON.stringify(porQuery) === JSON.stringify(porExecute));
    console.log('');
    console.log('  query   | texto pronto; o driver traduz; mais simples');
    console.log('  execute | prepared statement: PREPARE, EXECUTE, CLOSE; o servidor');
    console.log('           guarda o plano e reaproveita, e por isso ganha em consulta');
    console.log('           repetida com parametros diferentes.');
    console.log('');
    console.log('O tempo nao entra na saida desta pagina de proposito: ele muda a cada');
    console.log('execucao, e uma saida que muda e uma pagina que parece errada. O que o');
    console.log('exemplo mede de verdade e que os DOIS devolvem [linhas, campos] e que o');
    console.log('conteudo e o mesmo. Para medir a diferenca de tempo de verdade, o jeito');
    console.log('e rodar cada um mil vezes com parametros diferentes — e o `EXPLAIN` que');
    console.log('diz se o servidor fez `EXECUTE` no plano preparado.');

    // --- 4. o que o `execute` NAO aceita ---
    //
    // O prepared statement nao aceita `?` onde o SQL espera identificador (nome de
    // tabela ou coluna), nem varios comandos separados por `;`. E o que
    // diferencia `query` de `execute` na pratica: o `?` do `execute` e so para
    // VALOR.
    console.log('');
    console.log('--- o que o execute nao aceita ---');
    try {
      await conexao.execute('SELECT * FROM ? WHERE id = ?', ['tb_d12a2_aluno', 1]);
    } catch (erro) {
      console.error('  ' + erro.code + ': ' + erro.message);
      console.log('  `?` no lugar do NOME DA TABELA -> recusado, code', erro.code);
    }
    console.log('  o `?` do `execute` e so para VALOR. Nome de tabela e de coluna sao');
    console.log('  identificador, e identificador nao pode ser parametro — ele muda o');
    console.log('  plano de execucao, e o plano e o que o servidor preparou.');

    // --- 5. os tipos que chegam em Node ---
    //
    // O driver traduz o tipo do protocolo para tipo do JavaScript, e o
    // `dateStrings` da conexao muda o resultado da coluna de data. E a opcao que
    // mais causa bug em API: a mesma consulta devolve `Date` ou texto, conforme
    // a conexao.
    console.log('');
    console.log('--- o que chega em Node, por tipo de coluna ---');
    const [tipos] = await conexao.query(`
      SELECT 1 AS inteiro,
             'texto' AS texto,
             1.5 AS decimal_lido,
             CAST(1.5 AS DOUBLE) AS double_lido,
             TRUE AS booleano,
             NULL AS nulo,
             '2026-05-04' AS data_texto
    `);
    const l = tipos[0];
    console.log('coluna          | typeof      | valor');
    for (const chave of Object.keys(l)) {
      console.log('  ' + chave.padEnd(14) + '| ' + (l[chave] === null ? 'null' : typeof l[chave]).padEnd(12) + '| ' +
        (l[chave] === null ? 'null' : (l[chave] instanceof Date ? l[chave].toISOString() : JSON.stringify(l[chave]))));
    }
    console.log('');
    console.log('Tres coisas que o exemplo mediu e que a intuicao erra:');
    console.log('  `TRUE` volta 1, nao true: o MySQL guarda booleano como TINYINT.');
    console.log('  `1.5` volta "1.5", texto: o driver preserva o que e exato em decimal,');
    console.log('    mesmo num literal sem coluna. `CAST(... AS DOUBLE)` volta number.');
    console.log('    Quem precisa de numero converte com Number() e aceita o arredondamento.');
    console.log('  `NULL` volta null, e nao a string "null" nem 0.');

    // --- 6. `dateStrings` de verdade, em duas conexoes ---
    const comData = await mysql.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,
    });
    const comTexto = await mysql.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,
      dateStrings: true,
    });
    const sqlData = 'SELECT dt_cadastro FROM tb_d12a2_data WHERE id = 1';
    const [a] = await comData.query(sqlData);
    const [b] = await comTexto.query(sqlData);
    console.log('--- a MESMA consulta, duas conexoes ---');
    console.log('conexao normal        ->', a[0].dt_cadastro.constructor.name, JSON.stringify(a[0].dt_cadastro.toISOString()));
    console.log('conexao dateStrings   ->', typeof b[0].dt_cadastro, JSON.stringify(b[0].dt_cadastro));
    console.log('  o SQL e identico. O que muda e uma opcao da CONEXAO, e ela muda o tipo');
    console.log('  que chega no Node — e por isso que a data da aula 1 do dia 10 saia');
    console.log('  com um dia a menos no toISOString(): o fuso entra antes do valor.');
    await comData.end();
    await comTexto.end();

    // --- 7. o resumo que a aula precisa deixar ---
    console.log('');
    console.log('--- o resumo ---');
    console.log('query(sql, valores)   -> [algo, campos]');
    console.log('  SELECT              -> algo = linhas (array de objetos), campos = metadados');
    console.log('  INSERT/UPDATE/DELETE-> algo = { affectedRows, ... }, campos = undefined');
    console.log('execute(sql, valores) -> o mesmo formato, com prepared statement');
    console.log('');
    console.log('a regra que evita o bug: NUNCA usar o resultado sem o [0] no SELECT.');
    console.log('  const [linhas] = await query(sql);   const primeira = linhas[0];');
    console.log('  linhas.length === 0 e o jeito correto de perguntar "achou algo?"');
  } finally {
    const [removidos] = await conexao.query(
      "DELETE FROM tb_d12a2_data WHERE id = 1"
    );
    if (removidos.affectedRows > 0) console.log('linha de teste da data apagada.');
    await conexao.end();
    console.log('');
    console.log('conexao encerrada com end().');
  }
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

--- a pegadinha do [linhas, campos] ---
const [versao] = await query('SELECT VERSION() AS versao')
typeof versao:             object <-- e um ARRAY de linhas
Array.isArray(versao):     true
versao.versao:             undefined <-- undefined, SEM erro nenhum
versao[0].versao:          10.11.14-MariaDB-0ubuntu0.24.04.1 <-- o valor esta no [0]
JSON.stringify(versao):    [{"versao":"10.11.14-MariaDB-0ubuntu0.24.04.1"}]

um array nao tem propriedade `versao`, entao o acesso devolve undefined
em vez de lancar. O programa continua e a pagina mostra undefined.

--- a forma certa, com os dois valores ---
const [linhas, campos] = await query(...)
linhas[0].versao: 10.11.14-MariaDB-0ubuntu0.24.04.1
campos: 1 coluna(s), e o segundo valor so importa no SELECT
  campos[0].name = versao | type = 253

--- INSERT ---
affectedRows: 3 | insertId: 1
--- UPDATE ---
affectedRows: 2 | changedRows: 2
info: "Rows matched: 2  Changed: 2  Warnings: 0"
--- SELECT com agregacao ---
linhas[0].n: 3 (o primeiro valor e o array, e a linha e [0])

--- execute contra query ---

  query   devolveu 2 linha(s)
  execute devolveu 2 linha(s)
  o conteudo e identico: true

  query   | texto pronto; o driver traduz; mais simples
  execute | prepared statement: PREPARE, EXECUTE, CLOSE; o servidor
           guarda o plano e reaproveita, e por isso ganha em consulta
           repetida com parametros diferentes.

O tempo nao entra na saida desta pagina de proposito: ele muda a cada
execucao, e uma saida que muda e uma pagina que parece errada. O que o
exemplo mede de verdade e que os DOIS devolvem [linhas, campos] e que o
conteudo e o mesmo. Para medir a diferenca de tempo de verdade, o jeito
e rodar cada um mil vezes com parametros diferentes — e o `EXPLAIN` que
diz se o servidor fez `EXECUTE` no plano preparado.

--- o que o execute nao aceita ---
  `?` no lugar do NOME DA TABELA -> recusado, code ER_PARSE_ERROR
  o `?` do `execute` e so para VALOR. Nome de tabela e de coluna sao
  identificador, e identificador nao pode ser parametro — ele muda o
  plano de execucao, e o plano e o que o servidor preparou.

--- o que chega em Node, por tipo de coluna ---
coluna          | typeof      | valor
  inteiro       | number      | 1
  texto         | string      | "texto"
  decimal_lido  | string      | "1.5"
  double_lido   | number      | 1.5
  booleano      | number      | 1
  nulo          | null        | null
  data_texto    | string      | "2026-05-04"

Tres coisas que o exemplo mediu e que a intuicao erra:
  `TRUE` volta 1, nao true: o MySQL guarda booleano como TINYINT.
  `1.5` volta "1.5", texto: o driver preserva o que e exato em decimal,
    mesmo num literal sem coluna. `CAST(... AS DOUBLE)` volta number.
    Quem precisa de numero converte com Number() e aceita o arredondamento.
  `NULL` volta null, e nao a string "null" nem 0.
--- a MESMA consulta, duas conexoes ---
conexao normal        -> Date "2026-05-03T22:00:00.000Z"
conexao dateStrings   -> string "2026-05-04"
  o SQL e identico. O que muda e uma opcao da CONEXAO, e ela muda o tipo
  que chega no Node — e por isso que a data da aula 1 do dia 10 saia
  com um dia a menos no toISOString(): o fuso entra antes do valor.

--- o resumo ---
query(sql, valores)   -> [algo, campos]
  SELECT              -> algo = linhas (array de objetos), campos = metadados
  INSERT/UPDATE/DELETE-> algo = { affectedRows, ... }, campos = undefined
execute(sql, valores) -> o mesmo formato, com prepared statement

a regra que evita o bug: NUNCA usar o resultado sem o [0] no SELECT.
  const [linhas] = await query(sql);   const primeira = linhas[0];
  linhas.length === 0 e o jeito correto de perguntar "achou algo?"
linha de teste da data apagada.

conexao encerrada com end().