Dia 12 — mysql2: ligar o Node no MySQL
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ção | o que devolve | quando |
|---|---|---|
createConnection (assinatura: mysql.createConnection(config)) | uma promessa que resolve na conexão | script de uma vez, que abre e fecha |
createPool (assinatura: mysql.createPool(config)) | uma promessa que resolve no pool | servidor 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:
| Campo | De onde | Observação |
|---|---|---|
host | process.env.DB_HOST | 127.0.0.1 quando o banco é local |
port | process.env.DB_PORT | Number(...): o .env dá texto, o driver quer número |
user | process.env.DB_USER | o usuário, nunca a senha |
password | process.env.DB_PASS | vem do .env, nunca escrito no arquivo |
database | process.env.DB_NAME | o 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étodo | Assinatura | O que devolve |
|---|---|---|
query | query(sql, valores) | [linhas, campos] |
execute | execute(sql, valores) | o mesmo, com o driver preparado |
beginTransaction | beginTransaction() | promessa vazia |
commit | commit() | confirma a transação |
rollback | rollback() | desfaz a transação |
end | end() | 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().
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.
| Comando | Primeiro valor | Segundo |
|---|---|---|
SELECT | [linhas, campos] — linhas é array de objetos | metadados |
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 funciona | Quando ganha | |
|---|---|---|
query | texto pronto, o driver traduz | consulta de uma vez |
execute | PREPARE, EXECUTE, CLOSE; o servidor guarda o plano | mesma 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
| Origem | Chega como |
|---|---|
INT | number |
VARCHAR | string |
DECIMAL | string (o tipo é exato) |
DOUBLE | number |
BOOLEAN (TRUE) | number 1, não true |
NULL | null |
DATE | objeto 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().