Dia 8 — MySQL: o banco relacional
Banco de dados e MySQL
Banco de dados e MySQL
Banco de dados é o software que guarda dado de forma que ele sobreviva ao fim do
processo. Dados persistentes é o nome desse tipo de dado: o que está gravado
continua lá quando o Node desliga e volta. MySQL é um deles: um SGBD —
sistema gerenciador de banco de dados.
A coisa que mais confunde quem vem do navegador é que MySQL **não é uma
biblioteca. É um servidor**, que roda em uma máquina e escuta em uma porta —
o servidor de banco. O mysql2 não guarda nada: ele é o telefone, e o Node é
quem faz a ligação.
Node (cliente) --TCP--> porta 3306 --> MySQL (servidor) --> disco
Por isso dois fatos que parecem detalhe são estruturais:
- Se o servidor não está no ar, a conexão dá
ECONNREFUSED— e o erro não é do
Node nem do driver.
- A porta
3306é do servidor. Omysql2pergunta por ela, e em banco
remoto a porta pode ser outra.
O que perguntar ao banco antes de afirmar qualquer coisa
const [v] = await conexao.query('SELECT VERSION() AS versao'); const [i] = await conexao.query('SELECT DATABASE() AS banco, USER() AS usuario');
VERSION() devolve a versão da máquina que rodou — pode ser outra na máquina
de quem lê. DATABASE() e USER() respondem a pergunta que importa em
depuração: "o Node está falando com quem?".
@@port, @@hostname e @@max_connections dizem do lado do servidor. @@port
é a porta que o servidor escuta; a que o cliente procurou é outra informação.
NOW() é o relógio do banco, não da máquina — em servidor de produção, a
diferença entre o relógio do Node e o do banco é motivo de bug em relatório.
SHOW: o comando de administração
A linguagem SQL é a língua que o servidor de banco entende, e ela não é
JavaScript: é declarativa. O que se escreve é o resultado que se quer, e quem
decide como chegar nele é o SGBD — o Node diz "quero estes registros", não "faça
isto passo a passo".
SHOW não faz parte do SQL padrão. É comando de MySQL/MariaDB, e é a forma
curta de olhar a estrutura sem lembrar o nome dela.
| Comando | O que mostra |
|---|---|
SHOW TABLES | tabelas do banco conectado |
SHOW DATABASES | bancos visíveis para o usuário |
SHOW CREATE TABLE t | o SQL que recria a tabela |
SHOW COLUMNS FROM t | colunas, tipos e chaves |
SHOW INDEX FROM t | índices |
SHOW GRANTS | privilégios do usuário atual |
SHOW STATUS / SHOW VARIABLES | o que o servidor está fazendo agora |
SHOW GRANTS e a permissão mínima
const [g] = await conexao.query('SHOW GRANTS FOR CURRENT_USER()');
CURRENT_USER() dispensa saber o nome do usuário. É a forma de responder "por que
o INSERT deu denied?" quando o usuário existe mas não tem o privilégio — e de
verificar, num usuário de teste, que ele só enxerga o banco de teste e **não
alcança produção**. Note que a linha de privilégio vem com IDENTIFIED BY PASSWORD
e um hash: mesmo o SHOW GRANTS não devolve a senha, e nenhum exemplo deste
material imprime essa parte.
information_schema: o catálogo do banco
Existe um schema virtual chamado information_schema: não está no disco, e o
servidor monta na hora a partir do catálogo interno. Ele é a forma programática de
perguntar "que tabelas existem?" sem saber os nomes.
await conexao.query( `SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = ?`, [process.env.DB_NAME] );
O SHOW TABLES é a forma curta de consultar a mesma informação; information_schema
é a forma que entra em código, porque aceita filtro.
Docker e container de banco
A forma mais rápida de ter um MySQL na máquina de estudo é um container: a
imagem vem com o servidor pronto, e o banco vive num volume nomeado que
sobrevive ao docker stop. Os dois comandos de quem está começando:
docker run -d --name banco -e MYSQL_ROOT_PASSWORD=SUA_SENHA -p 3306:3306 mysql:8
A senha aqui é de ambiente e é fictícia; em máquina de verdade ela vai para o
.env (aula 2 do dia 2), nunca para o histórico do shell. E a porta -p 3306:3306
é o mapeamento: a 3306 da esquerda é na máquina, a da direita é no container —
é a diferença entre @@port e o que o cliente procura.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 8: o que existe do outro lado da porta 3306. // // A aula e sobre MySQL, e a tentacao e escrever a versao no texto. O exemplo faz // o contrario: ele PERGUNTA ao banco quem ele e, de onde veio, quantos schemas // ele enxerga e o que o usuario tem permissao de fazer. Tudo o que aparece na // saida saiu de uma consulta — a maquina de quem le pode devolver outro numero, e // a pagina nao pode afirmar um. // // O que fica provado: MySQL e um SERVIDOR, nao uma biblioteca. Node fala com // ele por TCP, o cliente (`mysql2`) e so o telefone. E o `SHOW GRANTS` mostra que // o usuario do exemplo so enxerga o banco de teste — nenhum exemplo deste // material escreve fora de `DB_NAME`. const mysql = require('mysql2/promise'); async function main() { // `createConnection` abre o socket TCP e espera o servidor responder. Sem o // `await`, a primeira consulta correria contra uma conexao que ainda nao // existe. 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, // do ambiente, nunca escrita aqui database: process.env.DB_NAME, }); try { console.log('--- quem e este banco ---'); // `SELECT VERSION()` e a forma de perguntar a versao. A versao e a da // maquina que rodou, e pode ser outra na maquina de quem le. const [versao] = await conexao.query('SELECT VERSION() AS versao'); console.log('versao:', versao[0].versao); // `SELECT DATABASE()` e `USER()` dizem onde estou e quem sou. Sao as duas // perguntas que respondem "o Node esta falando com quem?". const [identidade] = await conexao.query( 'SELECT DATABASE() AS banco, USER() AS usuario, CURRENT_USER() AS autenticado, CONNECTION_ID() AS conexao_id' ); console.log('banco:', identidade[0].banco); console.log('usuario:', identidade[0].usuario, '| autenticado como:', identidade[0].autenticado); console.log('id desta conexao no servidor:', identidade[0].conexao_id); console.log('(o id muda a cada execucao: e a conexao nova que o exemplo abriu agora)'); // --- o que o cliente e o que o servidor sao --- // // `@@port` e a porta que o SERVIDOR escuta; `process.env.DB_PORT` e a porta // que o CLIENTE procurou. Sao numeros iguais quando o banco e local, e // diferentes quando o cliente vem de outra maquina. // // `@@port` e a forma abreviada que o SQL usa; em `SELECT @@port AS x` o // driver pode entregar a coluna como `port`, e o alias e o que define o nome // que chega em Node. Por isso o exemplo usa as funcoes de servidor, cujo nome // ja vem completo e nao depende do alias. const [portas] = await conexao.query( `SELECT @@hostname AS maquina, @@max_connections AS max_conexoes, @@port AS porta_servidor, @@sql_mode AS modo_sql` ); console.log(''); console.log('--- cliente e servidor ---'); console.log('cliente procurou:', process.env.DB_HOST + ':' + process.env.DB_PORT); console.log('servidor escuta em:', portas[0].porta_servidor, '| maquina:', portas[0].maquina); console.log('conexoes simultaneas que o servidor aceita:', portas[0].max_conexoes); console.log('o Node e o cliente; o MySQL e o servidor. O `mysql2` e o telefone entre os dois.'); console.log(''); console.log('sql_mode:', portas[0].modo_sql); console.log('`sql_mode` define o que o SQL aceita: se a data entra como'); console.log('"2026-05-04" ou so como "2026-05-04 00:00:00", se o silencio vira erro.'); console.log('Duas maquinas com modos diferentes aceitam INSERTs diferentes, e por'); console.log('isso que o exemplo LÊ o modo em vez de afirmar qual ele e.'); // --- os schemas visiveis --- // // `SHOW DATABASES` lista o que o usuario enxerga. Um usuario com privilegio // minimo ve so o banco em que tem coisa — e ver so o banco de teste e a // prova de que nenhum exemplo deste material toca no banco de producao. const [schemas] = await conexao.query('SHOW DATABASES'); console.log(''); console.log('--- bancos visiveis para este usuario ---'); console.log(schemas.map((l) => ' ' + Object.values(l)[0]).join('\n')); console.log('o usuario do material so enxerga o banco de teste.'); // --- os privilegios: o que este usuario pode fazer --- // // `SHOW GRANTS FOR CURRENT_USER()` mostra a permissao sem precisar saber o // nome do usuario. E a forma de responder "por que o INSERT deu denied?" // quando o usuario existe mas nao tem o privilegio. // // A linha vem com `IDENTIFIED BY PASSWORD` e um hash. O exemplo corta essa // parte ANTES de imprimir: e o proprio `SHOW GRANTS` que nao devolve a // senha, e ainda assim a saida vira pagina publica. const [grants] = await conexao.query('SHOW GRANTS FOR CURRENT_USER()'); console.log(''); console.log('--- privilegios (SHOW GRANTS) ---'); for (const linha of grants) { console.log(' ' + Object.values(linha)[0].split(' IDENTIFIED BY')[0]); } console.log('ALL PRIVILEGES so no banco de teste: nenhum exemplo escreve fora de DB_NAME.'); console.log('(a parte do hash foi cortada antes de imprimir: a saida vira pagina)'); // --- SQL e o que ele e --- // // `SELECT 1` e o "ping" do SQL: prova que o interpretador respondeu sem // tocar em tabela nenhuma. O driver manda texto e o banco devolve o que // aquele texto quer dizer. const [ping] = await conexao.query( "SELECT 1 AS ok, 'texto' AS literal, NOW() AS agora_no_banco" ); console.log(''); console.log('--- o que o SQL devolve ---'); console.log('SELECT 1 ->', ping[0].ok, '(numero)'); console.log("SELECT 'texto' ->", ping[0].literal, '(texto)'); console.log('NOW() ->', ping[0].agora_no_banco.toISOString()); console.log('o relogio vem do SERVIDOR, nao da maquina: e o banco que fala que horas sao.'); // --- o catalogo: onde o banco guarda a si mesmo --- // // `information_schema` e um schema virtual: nao existe no disco, e o // servidor monta na hora a partir do catalogo interno. Consultar ele e o // jeito de perguntar "que tabelas existem?" sem saber os nomes. const [catalogo] = await conexao.query( `SELECT COUNT(*) AS tabelas FROM information_schema.tables WHERE table_schema = ?`, [process.env.DB_NAME] ); const [colunas] = await conexao.query( `SELECT COUNT(*) AS colunas FROM information_schema.columns WHERE table_schema = ?`, [process.env.DB_NAME] ); console.log(''); console.log('--- o catalogo (information_schema) ---'); console.log('tabelas no banco de teste:', catalogo[0].tabelas); console.log('colunas no banco de teste:', colunas[0].colunas); console.log('o catalogo e virtual: o servidor monta na consulta, e `SHOW TABLES`'); console.log('e a forma curta de consultar a mesma informacao.'); // --- SHOW, o comando de administracao --- console.log(''); console.log('--- SHOW: o comando de administracao ---'); console.log('SHOW TABLES | tabelas do banco conectado'); console.log('SHOW DATABASES | bancos visiveis'); console.log('SHOW CREATE TABLE t | o SQL que recria a tabela'); console.log('SHOW COLUMNS FROM t | colunas, tipos e chaves'); console.log('SHOW INDEX FROM t | indices'); console.log('SHOW GRANTS | privilegios do usuario'); console.log('SHOW STATUS / VARIABLES | o que o servidor esta fazendo agora'); } finally { // `end()` fecha a conexao. Sem ele o socket segura o event loop e o // processo nao termina sozinho. 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
--- quem e este banco --- versao: 10.11.14-MariaDB-0ubuntu0.24.04.1 banco: materiais_teste usuario: materiais@localhost | autenticado como: materiais@localhost id desta conexao no servidor: 7306 (o id muda a cada execucao: e a conexao nova que o exemplo abriu agora) --- cliente e servidor --- cliente procurou: 127.0.0.1:3306 servidor escuta em: 3306 | maquina: vmi3339533 conexoes simultaneas que o servidor aceita: 151 o Node e o cliente; o MySQL e o servidor. O `mysql2` e o telefone entre os dois. sql_mode: IGNORE_SPACE,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION `sql_mode` define o que o SQL aceita: se a data entra como "2026-05-04" ou so como "2026-05-04 00:00:00", se o silencio vira erro. Duas maquinas com modos diferentes aceitam INSERTs diferentes, e por isso que o exemplo LÊ o modo em vez de afirmar qual ele e. --- bancos visiveis para este usuario --- information_schema materiais_teste o usuario do material so enxerga o banco de teste. --- privilegios (SHOW GRANTS) --- GRANT USAGE ON *.* TO `materiais`@`localhost` GRANT ALL PRIVILEGES ON `materiais_teste`.* TO `materiais`@`localhost` ALL PRIVILEGES so no banco de teste: nenhum exemplo escreve fora de DB_NAME. (a parte do hash foi cortada antes de imprimir: a saida vira pagina) --- o que o SQL devolve --- SELECT 1 -> 1 (numero) SELECT 'texto' -> texto (texto) NOW() -> 2026-09-30T15:15:35.000Z o relogio vem do SERVIDOR, nao da maquina: e o banco que fala que horas sao. --- o catalogo (information_schema) --- tabelas no banco de teste: 86 colunas no banco de teste: 333 o catalogo e virtual: o servidor monta na consulta, e `SHOW TABLES` e a forma curta de consultar a mesma informacao. --- SHOW: o comando de administracao --- SHOW TABLES | tabelas do banco conectado SHOW DATABASES | bancos visiveis SHOW CREATE TABLE t | o SQL que recria a tabela SHOW COLUMNS FROM t | colunas, tipos e chaves SHOW INDEX FROM t | indices SHOW GRANTS | privilegios do usuario SHOW STATUS / VARIABLES | o que o servidor esta fazendo agora conexao encerrada com end().
Criar banco e tabela
Criar banco e tabela
CREATE DATABASE cria o schema e USE escolhe qual está em uso. A partir daí,
CREATE TABLE desenha a tabela — e o desenho é a decisão mais importante do
banco, porque é o que os outros quarenta exemplos vão ter de respeitar.
CREATE DATABASE materiais_teste; USE materiais_teste; CREATE TABLE tb_aluno ( id INT AUTO_INCREMENT PRIMARY KEY, nm_aluno VARCHAR(40) NOT NULL, email VARCHAR(80) NULL, turma INT NOT NULL, dt_cadastro DATE NOT NULL, vl_nota DECIMAL(4,2) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
As restrições, e o que cada uma garante
O tipo de dado é a escolha de cada coluna, e ele responde a duas perguntas: o
que o valor significa e quanto espaço ele ocupa. INT é número inteiro,
VARCHAR(40) é texto de até 40 caracteres, DATE é data sem hora — e o banco
passa a recusar o que não cabe antes de a consulta chegar ao Node.
| Restrição | O que garante |
|---|---|
PRIMARY KEY | valor único e nunca nulo; é a chave que o índice usa |
AUTO_INCREMENT | o banco numera sozinho, sequencial, sem repetir |
NOT NULL | a coluna não aceita NULL |
DEFAULT | valor usado quando o INSERT não manda o campo |
UNIQUE | valor único, diferente da chave primária |
ENGINE=InnoDB | transação e chave estrangeira; o padrão desde o MySQL 5.5 |
DEFAULT CHARSET=utf8mb4 | guarda o emoji e qualquer caractere Unicode |
AUTO_INCREMENT só funciona em coluna indexada. Pedir
AUTO_INCREMENT sem chave dá erro na hora do CREATE TABLE, e não na hora do
INSERT — o que confunde quem esperava o erro depois.
IF NOT EXISTS e o motivo de o material não usar DROP
CREATE TABLE IF NOT EXISTS não reclama se a tabela já existe. DROP TABLE apaga
a tabela e tudo dentro, e não é usado em nenhum exemplo do material — não por
medo, por repetibilidade: todo exemplo roda duas vezes seguidas e precisa dar a
mesma saída, e DROP + CREATE faria a segunda rodada perder o que a primeira
gravou.
A combinação padrão é CREATE TABLE IF NOT EXISTS na estrutura e TRUNCATE no
começo do exemplo, nunca no fim: a aula seguinte precisa achar a tabela no
estado que a anterior deixou.
SHOW CREATE TABLE e SHOW COLUMNS
SHOW CREATE TABLE tb_aluno; -- o SQL que recria a tabela SHOW COLUMNS FROM tb_aluno; -- colunas, tipos, nulabilidade, chave
SHOW CREATE TABLE é a forma de conferir o desenho depois de criado, e é nele que
aparecem as normalizações que o pedido fez: o NOT NULL vira atributo de coluna,
o AUTO_INCREMENT vira atributo de tabela, o VARCHAR(40) foi guardado como
varchar(40). Note que o servidor acrescenta o que foi omitido — aqui o
COLLATE=utf8mb4_general_ci, que ninguém pediu.
Em SHOW COLUMNS, o Default de uma coluna NOT NULL aparece como null. Isso
não é a mesma coisa que "aceita nulo": a coluna que aceita NULL é a que mostra
Null: YES, e a que mostra Null: NO com Default: null é obrigatória e sem
padrão — escrever nela sem valor dá ER_BAD_NULL_ERROR.
O Node manda um valor; o banco guarda um tipo
A distinção que a leitura de volta prova:
| O Node manda | O banco guarda | Volta em Node |
|---|---|---|
'Ana' | varchar(40) | string |
3 | int | number |
'2026-05-04' | date | objeto Date |
8.5 | decimal(4,2) | string "8.50" |
DECIMAL volta como string porque é exato, e é por isso que dinheiro em
FLOAT ou DOUBLE dá erro de arredondamento: eles guardam aproximação, e
0.1 + 0.2 não dá 0.3. A coluna date volta como Date do JavaScript, com
fuso horário aplicado — as 00:00:00 do banco viram 22:00:00 do dia anterior no
toISOString(). Para ler data como texto, a consulta precisa trazer no formato,
não no objeto.
O banco recusa o que não cabe
Com sql_mode estrito — o que a aula 1 mediu em @@sql_mode — escrever 60
caracteres em VARCHAR(40) dá ER_DATA_TOO_LONG, e escrever NULL em coluna
NOT NULL dá ER_BAD_NULL_ERROR. O erro.code é o que o Node compara; a
mensagem muda entre versões e não serve para decidir nada.
CREATE DATABASE, DROP DATABASE e as permissões
CREATE DATABASE exige privilégio no servidor, e o usuário do material não tem
— ele só enxerga o banco de teste, como a aula 1 mostrou no SHOW GRANTS. Por
isso nenhum exemplo deste material cria banco: ele usa o que já existe, dentro de
DB_NAME. Isso não é limitação do exemplo, é a garantia de que nenhum deles toca
no banco de produção.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 8: criar banco e tabela, e conferir o que o banco fez. // // `CREATE TABLE` e o desenho da tabela. O exemplo cria a tabela do dia inteiro, // consulta o catalogo para ver o que o SERVIDOR entendeu (tipo, nulo, chave) e // compara com o que foi pedido — e e essa comparacao que ensina: o tipo que o // Node manda (`'Ana'`, `3`) e o tipo que o SQL guardou (`varchar(40)`, `int`). // // O exemplo e idempotente: `CREATE TABLE IF NOT EXISTS` + `TRUNCATE` no COMECO. // E o mesmo exemplo roda duas vezes seguidas e da a mesma saida, que e o que o // material exige de todo exemplo. 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, }); try { // --- 1. o desenho da tabela --- // // O `IF NOT EXISTS` e o que torna o exemplo repetivel. Sem ele, a segunda // execucao quebra com `ER_TABLE_EXISTS_ERROR` — e um exemplo que funciona // uma vez e quebra na segunda esta ensinando errado. await conexao.query(` CREATE TABLE IF NOT EXISTS tb_aluno ( id INT AUTO_INCREMENT PRIMARY KEY, nm_aluno VARCHAR(40) NOT NULL, email VARCHAR(80) NULL, turma INT NOT NULL, dt_cadastro DATE NOT NULL, vl_nota DECIMAL(4,2) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); console.log('--- a tabela criada ---'); // `TRUNCATE` no COMECO e nao no fim: a aula seguinte precisa achar a tabela // no estado que este exemplo deixou. await conexao.query('TRUNCATE TABLE tb_aluno'); // --- 2. `SHOW CREATE TABLE`: o que o servidor entendeu --- // // Esta e a forma de conferir o desenho depois de criado. O que volta e o SQL // que recria a tabela — e e nele que aparecem as Normalidades que o pedido // fez: o `NOT NULL` virou atributo de coluna, o `AUTO_INCREMENT` virou // atributo de tabela, e o tipo `VARCHAR(40)` foi guardado. const [criada] = await conexao.query('SHOW CREATE TABLE tb_aluno'); console.log(''); console.log('SHOW CREATE TABLE tb_aluno:'); console.log(criada[0]['Create Table']); // --- 3. `SHOW COLUMNS`: coluna por coluna --- // // `Field` e o nome, `Type` o tipo guardado, `Null` se aceita nulo, `Key` o // papel da coluna (PRI = chave primaria), `Default` o valor padrao e // `Extra` o que mais o servidor guarda (auto_increment aqui). const [colunas] = await conexao.query('SHOW COLUMNS FROM tb_aluno'); console.log(''); console.log('--- as colunas, como o servidor as guardou ---'); console.log(' ' + ['campo', 'tipo', 'aceita nulo', 'chave', 'extra'].join(' | ')); for (const c of colunas) { console.log(' ' + [ c.Field, String(c.Type), c.Null === 'YES' ? 'sim' : 'nao', c.Key || '-', c.Extra || '-', ].join(' | ')); } console.log(' (PRI = chave primaria; auto_increment = a coluna que o banco numera)'); console.log(' (todo NOT NULL sem DEFAULT aparece como `null` no SHOW COLUMNS: o'); console.log(' padrao de coluna NOT NULL e a ausencia de valor, e o MySQL mostra'); console.log(' `null` — o que nao e o mesmo que "aceita nulo", que e a coluna `email`)'); // --- 4. a diferenca entre o que foi pedido e o que foi guardado --- // // O Node manda `'Ana'` e `3`, e o que chega no banco sao `varchar(40)` e // `int`. O tipo da coluna e um limite de tamanho, e o que entra dentro dele // e texto. E por isso que `VARCHAR(40)` de nome e `VARCHAR(255)` de email // nao sao a mesma coisa, mesmo sendo os dois "texto". const [grava] = await conexao.query( 'INSERT INTO tb_aluno (nm_aluno, email, turma, dt_cadastro, vl_nota) VALUES (?, ?, ?, ?, ?)', ['Ana', '[email protected]', 3, '2026-05-04', 8.5] ); console.log(''); console.log('--- o que o Node manda contra o que o banco guarda ---'); console.log("o Node mandou 'Ana' (texto), 3 (numero) e '2026-05-04' (texto)"); console.log('o banco guardou como varchar(40), int e date: linhas gravadas =', grava.affectedRows); const [lida] = await conexao.query('SELECT nm_aluno, turma, dt_cadastro, vl_nota FROM tb_aluno'); console.log('lendo de volta:', JSON.stringify(lida[0])); console.log('a data voltou como Date do JavaScript:', lida[0].dt_cadastro instanceof Date); console.log('e o vl_nota voltou como string, porque DECIMAL e exato:', typeof lida[0].vl_nota, JSON.stringify(lida[0].vl_nota)); console.log('DECIMAL guarda exato; FLOAT e DOUBLE sao aproximados e nao servem para dinheiro.'); // --- 5. o que o banco faz quando o dado nao cabe --- // // `VARCHAR(40)` com 50 caracteres: em modo estrito o banco RECUSA, e o // `sql_mode` visto na aula 1 e o que decide isso. O `ER_DATA_TOO_LONG` e o // erro que o Node tem de tratar — e ele e a prova de que o limite da coluna // vale. console.log(''); console.log('--- limite de coluna, respeitado pelo banco ---'); try { await conexao.query( 'INSERT INTO tb_aluno (nm_aluno, turma, dt_cadastro) VALUES (?, ?, ?)', ['x'.repeat(60), 3, '2026-05-04'] ); console.log(' o banco ACEITOU 60 caracteres em VARCHAR(40) — o modo nao esta estrito'); } catch (erro) { console.error(erro.code + ': ' + erro.message); console.log(' erro esperado:', erro.code, '- 60 caracteres em VARCHAR(40) nao cabem'); } // --- 6. `NOT NULL` e o valor que nao pode faltar --- console.log(''); console.log('--- NOT NULL ---'); try { await conexao.query( 'INSERT INTO tb_aluno (nm_aluno, turma, dt_cadastro) VALUES (?, ?, ?)', ['Sem Turma', null, '2026-05-04'] ); console.log(' o banco aceitou turma nula — a coluna nao tem NOT NULL'); } catch (erro) { console.error(erro.code + ': ' + erro.message); console.log(' erro esperado:', erro.code, '- a coluna turma e NOT NULL'); } // --- 7. `DROP TABLE` e por que o exemplo NAO usa --- // // `DROP TABLE` apaga a tabela e tudo que esta dentro. O material nao usa // `DROP` em nenhum exemplo, e o motivo e repetibilidade: o exemplo precisa // rodar duas vezes seguidas e dar a mesma saida, e `DROP` + `CREATE` faria a // segunda rodada perder o que a primeira gravou. `CREATE TABLE IF NOT EXISTS` // resolve o mesmo problema sem destruir estrutura. console.log(''); console.log('--- por que nao ha DROP TABLE neste exemplo ---'); console.log('DROP TABLE apaga tudo que esta dentro, e o exemplo precisa ser'); console.log('repetivel: rodar duas vezes tem que dar a mesma saida.'); console.log('IF NOT EXISTS + TRUNCATE resolvem sem destruir estrutura.'); // --- 8. os tipos, e o que cada um guarda --- console.log(''); console.log('--- os tipos de dado, e o que cada um guarda ---'); console.log('CHAR(n) | texto de tamanho fixo, preenchido com espaco'); console.log('VARCHAR(n) | texto de ate n caracteres'); console.log('TEXT | texto longo, sem limite pratico'); console.log('INT | numero inteiro, de -2 bilhoes a 2 bilhoes'); console.log('BIGINT | inteiro maior'); console.log('DECIMAL(p,s)| numero exato com p casas e s decimais — dinheiro'); console.log('FLOAT/DOUBLE| aproximacao, para medida e nao para dinheiro'); console.log('DATE | so a data'); console.log('DATETIME | data e hora'); console.log('BOOLEAN | TRUE/FALSE, guardado como TINYINT(1)'); console.log('JSON | documento JSON, com validacao do banco'); console.log(''); console.log('a regra do material: nome de tabela e coluna em snake_case com prefixo'); console.log('`tb_` (tb_aluno) e `dt_`/`vl_` para data e valor (dt_cadastro, vl_nota).'); } finally { 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 tabela criada ---
SHOW CREATE TABLE tb_aluno:
CREATE TABLE `tb_aluno` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nm_aluno` varchar(40) NOT NULL,
`email` varchar(80) DEFAULT NULL,
`turma` int(11) NOT NULL,
`dt_cadastro` date NOT NULL,
`vl_nota` decimal(4,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
--- as colunas, como o servidor as guardou ---
campo | tipo | aceita nulo | chave | extra
id | int(11) | nao | PRI | auto_increment
nm_aluno | varchar(40) | nao | - | -
email | varchar(80) | sim | - | -
turma | int(11) | nao | - | -
dt_cadastro | date | nao | - | -
vl_nota | decimal(4,2) | sim | - | -
(PRI = chave primaria; auto_increment = a coluna que o banco numera)
(todo NOT NULL sem DEFAULT aparece como `null` no SHOW COLUMNS: o
padrao de coluna NOT NULL e a ausencia de valor, e o MySQL mostra
`null` — o que nao e o mesmo que "aceita nulo", que e a coluna `email`)
--- o que o Node manda contra o que o banco guarda ---
o Node mandou 'Ana' (texto), 3 (numero) e '2026-05-04' (texto)
o banco guardou como varchar(40), int e date: linhas gravadas = 1
lendo de volta: {"nm_aluno":"Ana","turma":3,"dt_cadastro":"2026-05-03T22:00:00.000Z","vl_nota":"8.50"}
a data voltou como Date do JavaScript: true
e o vl_nota voltou como string, porque DECIMAL e exato: string "8.50"
DECIMAL guarda exato; FLOAT e DOUBLE sao aproximados e nao servem para dinheiro.
--- limite de coluna, respeitado pelo banco ---
erro esperado: ER_DATA_TOO_LONG - 60 caracteres em VARCHAR(40) nao cabem
--- NOT NULL ---
erro esperado: ER_BAD_NULL_ERROR - a coluna turma e NOT NULL
--- por que nao ha DROP TABLE neste exemplo ---
DROP TABLE apaga tudo que esta dentro, e o exemplo precisa ser
repetivel: rodar duas vezes tem que dar a mesma saida.
IF NOT EXISTS + TRUNCATE resolvem sem destruir estrutura.
--- os tipos de dado, e o que cada um guarda ---
CHAR(n) | texto de tamanho fixo, preenchido com espaco
VARCHAR(n) | texto de ate n caracteres
TEXT | texto longo, sem limite pratico
INT | numero inteiro, de -2 bilhoes a 2 bilhoes
BIGINT | inteiro maior
DECIMAL(p,s)| numero exato com p casas e s decimais — dinheiro
FLOAT/DOUBLE| aproximacao, para medida e nao para dinheiro
DATE | so a data
DATETIME | data e hora
BOOLEAN | TRUE/FALSE, guardado como TINYINT(1)
JSON | documento JSON, com validacao do banco
a regra do material: nome de tabela e coluna em snake_case com prefixo
`tb_` (tb_aluno) e `dt_`/`vl_` para data e valor (dt_cadastro, vl_nota).
conexao encerrada com end().