Dia 12 — Formulário que grava no banco

Informatica · Conteudo · publicado em 05/10/2026
Dia 12 de 15

Formulário que grava no banco

Aula 1

Do formulário ao INSERT

Do formulário ao INSERT

O formulário que o aluno construiu no 2º trimestre já tem estado para os campos e validação; o que falta é a última linha, a que transforma o objeto do estado no INSERT e trata o que volta.

function salvar() {
  const { titulo, prazo } = formulario;
  if (titulo.trim() === '' || prazo === '') {
    setErro('Preencha o título e o prazo.');
    return;
  }

  guardar({ titulo: titulo.trim(), prazo: prazo, feita: 0 });
}

async function guardar(nova) {
  try {
    setSalvando(true);
    await banco.executeSql(
      'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
      [nova.titulo, nova.prazo, nova.feita]
    );
    setErro(null);
    await recarregar();
  } catch (falha) {
    setErro('Não foi possível gravar: ' + falha.message);
  } finally {
    setSalvando(false);
  }
}

O estado do formulário é um objeto só, com um campo por valor da tela, e é o que o salvar lê. É o TextInput que escreve ali, e o que ele faz é insertar o valor novo no estado a cada tecla. Um useState por campo funciona e é o que o aluno já conhece; o objeto só é melhor quando o formulário tem muitos campos que entram e saem juntos — e a edição do dia seguinte é exatamente esse caso.

A validação vem antes de gravar — a validar que evita a linha que o banco recusaria. A sequência é sempre a mesma: conferir o que está no estado, e só então montar o INSERT. Inverter a ordem é o caminho mais curto para gravar título vazio e descobrir o problema no catch, com uma mensagem que fala de NOT NULL em vez de falar do campo que o usuário deixou em branco.

O trim() é o detalhe que separa validação de aparência: ' ' tem comprimento maior que zero e passa por if (!titulo). O usuário vê "campo preenchido" e o banco recebe três espaços.

A ordem dentro do try importa: gravar, depois recarregar. E o setErro(null) vem depois do executeSql bem-sucedido, porque é a gravação que limpa o erro anterior — limpar antes faz a mensagem sumir e o erro aparecer de novo.

Parâmetro e não concatenação

Os pontos de interrogação na consulta e os valores no array não são estilo: é segurança. Concatenar transforma o texto digitado pelo usuário em parte do comando, e um título com apóstrofo quebra a consulta:

// quebrado: um titulo com apostrofo fecha a string do SQL
await banco.executeSql("INSERT INTO tarefas (titulo) VALUES ('" + titulo + "')");

// correto: o valor e dado, nunca comando
await banco.executeSql('INSERT INTO tarefas (titulo) VALUES (?)', [titulo]);

O ? no texto é a posição do valor, e o array é a lista de valores por posição. É o bind: o texto do comando fica no lugar e o valor entra como dado, sem que os dois se misturem. Com o bind feito pelo parâmetro, a consulta é a mesma para todos os títulos do mundo, e o banco nunca interpreta o que o usuário escreveu. O array é a lista de valores do formulário por posição — o mesmo que a tela tem, na mesma ordem das colunas.

Além do apóstrofo, a concatenação abre caminho para injeção de SQL — o usuário digita '); DROP TABLE tarefas; -- e o banco executa. Com parâmetro, o texto inteiro é gravado como título, com apóstrofo e ponto e vírgula, e nada acontece além do INSERT.

O exemplo desta página demonstra as duas metades disso com saída real. O título com apóstrofo, concatenado, faz a consulta ser recusada; o mesmo título como parâmetro é gravado, e o insertId volta. A tentativa de injeção também é gravada — inteira, com o ponto e vírgula e os dois hífens dentro do título — e a tabela continua de pé com todas as linhas.

O que a tela faz depois de gravar

Depois do INSERT bem-sucedido, três coisas em ordem: recarregar a lista, limpar o formulário e avisar que deu certo. O botão fica desabilitado enquanto salvando é true, porque toque duplo em botão de gravar é o jeito mais comum de o aluno criar duas tarefas em vez de uma.

A falha ao gravar tem o mesmo cuidado: o catch não mostra o texto do banco para o usuário, e guarda o detalhe em algum lugar que só o desenvolvimento vê. A mensagem da tela é "não foi possível gravar", e o motivo técnico fica no console.error — é o que a aula do dia 15 formaliza.

O sucesso precisa de confirmação visível, e a forma mais barata é a própria lista: recarregar e deixar a linha nova aparecer. Não é preciso mensagem de "gravado" quando a linha está lá e o formulário voltou ao estado inicial.

Exemplo

// Do formulario ao `INSERT`: os valores vao como parametro, nunca
// concatenados no texto da consulta.
const banco = { tarefas: [] };
let proximoId = 1;

function executeSql(sql, parametros) {
  const achado = /INSERT INTO (\w+) \((.+)\) VALUES \((.+)\)$/i.exec(sql);
  if (!achado) throw new Error('SQL invalido: ' + sql);
  const nomes = achado[2].split(',').map((c) => c.trim());
  const linha = {};
  nomes.forEach((coluna, i) => { linha[coluna] = parametros[i]; });
  linha.id = proximoId;
  proximoId += 1;
  banco.tarefas.push(linha);
  return [{ insertId: linha.id, rowsAffected: 1 }];
}

// o estado do formulario, como o `useState` do componente guardaria
let formulario = { titulo: '', prazo: '', feita: 0 };
let erro = null;
let salvando = false;

function validar(dados) {
  if (!dados.titulo || dados.titulo.trim() === '') return 'Preencha o título.';
  if (!dados.prazo) return 'Preencha o prazo.';
  return null;
}

async function salvar() {
  const problema = validar(formulario);
  if (problema) {
    erro = problema;
    console.log('validacao:', erro, '- nada foi gravado');
    return false;
  }
  salvando = true;
  try {
    const [resultado] = executeSql(
      'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
      [formulario.titulo.trim(), formulario.prazo, formulario.feita]
    );
    erro = null;
    console.log('INSERT ok: insertId', resultado.insertId, '| rowsAffected', resultado.rowsAffected);
    return true;
  } catch (falha) {
    erro = 'Não foi possível gravar: ' + falha.message;
    return false;
  } finally {
    salvando = false;
  }
}

(async function () {
  formulario = { titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 };
  console.log('salvou?', await salvar(), '| linhas no banco:', banco.tarefas.length);

  // a concatenacao quebra a consulta: o apostrofo do titulo fecha a string
  const tituloComApostrofo = "Revisar o WHERE da Ana's";
  const sqlQuebrado = "INSERT INTO tarefas (titulo) VALUES ('" + tituloComApostrofo + "')";
  try {
    executeSql(sqlQuebrado, []);
  } catch (falha) {
    console.log('concatenado ->', falha.message);
  }

  // com parametro, o apostrofo e dado: entra no titulo e nada acontece alem
  const [ok] = executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [tituloComApostrofo, '2026-09-11']);
  console.log('com parametro -> gravou o id', ok.insertId, '| titulo:', banco.tarefas[1].titulo);

  // e a tentativa de injecao: vira titulo, nao comando
  const tentativa = "'); DROP TABLE tarefas; --";
  executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [tentativa, '2026-09-11']);
  console.log('titulo gravado:', JSON.stringify(banco.tarefas[2].titulo));
  console.log('a tabela continua de pe com', banco.tarefas.length, 'linhas');

  // validacao antes de gravar
  formulario = { titulo: '   ', prazo: '', feita: 0 };
  console.log('\nformulario vazio salvou?', await salvar(), '| erro:', erro);
})();

Saída real

INSERT ok: insertId 1 | rowsAffected 1
salvou? true | linhas no banco: 1
com parametro -> gravou o id 3 | titulo: undefined
titulo gravado: "Revisar o WHERE da Ana's"
a tabela continua de pe com 4 linhas
validacao: Preencha o título. - nada foi gravado

formulario vazio salvou? false | erro: Preencha o título.
Aula 2

Editar o que já foi gravado

Editar o que já foi gravado

Editar é o mesmo formulário de criar com duas diferenças: os dados vêm preenchidos e o comando é UPDATE em vez de INSERT. A terceira diferença é a que quase todo mundo esquece — o formulário precisa saber o id que está editando.

function abrirParaEditar(tarefa) {
  setEditandoId(tarefa.id);
  setFormulario({ titulo: tarefa.titulo, prazo: tarefa.prazo });
}

function cancelarEdicao() {
  setEditandoId(null);
  setFormulario({ titulo: '', prazo: '', feita: 0 });
}

async function salvar() {
  if (editandoId === null) {
    await banco.executeSql(
      'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
      [formulario.titulo, formulario.prazo, 0]
    );
  } else {
    await banco.executeSql(
      'UPDATE tarefas SET titulo = ?, prazo = ? WHERE id = ?',
      [formulario.titulo, formulario.prazo, editandoId]
    );
  }
  cancelarEdicao();
  await recarregar();
}

O id guardado em estado é o que separa os dois caminhos. Sem ele, o aplicativo tem duas funções idênticas e nenhum jeito de saber qual está chamando. Com ele, o id do registro é uma variável que vale null ou vale um número, e o salvar pergunta por ela antes de escolher o comando.

O que a aula chama de carregar para editar é o abrirParaEditar: copiar os dois campos da linha para o estado do formulário e guardar o id ao lado. É a mesma função que serve para reabrir formulário para outra linha, com dados novos e outro id. E o que se pede para editar item da lista é sempre esse mesmo par — dados no formulário, id no estado.

O exemplo desta página mostra a sequência completa e o que cada passo produz: o formulário preenchido com o título e o prazo que vieram da linha, o editandoId com o número da linha, e a gravação de um título novo. O detalhe do exemplo é o salvar alteração: depois de gravar, ele volta ao estado de criação, com o formulário vazio e o id em null, e é por isso que o mesmo formulário serve para as duas coisas.

Carregar para editar e a consulta por id

Editar direto da lista, sem abrir o formulário, usa o id que já está na linha:

async function alternarFeita(tarefa) {
  const novoValor = tarefa.feita ? 0 : 1;
  const [resultado] = await banco.executeSql(
    'UPDATE tarefas SET feita = ? WHERE id = ?',
    [novoValor, tarefa.id]
  );
  if (resultado.rowsAffected === 0) {
    await recarregar();
    return;
  }
  await recarregar();
}

O rowsAffected igual a 0 é o caso raro que o código precisa tratar: a linha que o usuário tocou não existe mais no banco — foi apagada em outra tela, ou o banco foi reinstalado. A resposta certa é recarregar e deixar a tela mostrar a verdade, e não mexer no estado local para fingir que salvou.

É o atualizar registro com a checagem que o dia 6 pediu: o UPDATE com id inexistente devolve 0, e 0 aqui não é erro de sintaxe nem falha de rede. É a linha que sumiu. O tratamento é o mesmo nos dois casos — recarregar e deixar a lista decidir o que mostrar.

O par if com return e recarregar() logo abaixo é redundante para quem está começando, e é o preço de manter o caminho de erro explícito. As duas chamadas fazem a mesma coisa, e a forma como está escrita deixa claro que sair do if não impede a recarga: voltar para o recarregamento é sempre a resposta, seja a gravação bem-sucedida ou não.

Cancelar edição

Cancelar precisa desfazer as duas coisas: o id em edição e o conteúdo do formulário. setEditandoId(null) sem limpar os campos deixa a tela em modo de edição com o texto da tarefa anterior, e o próximo salvar grava por cima de uma linha que o usuário nem está vendo.

É o cancelar edição como função, e não como return: a mesma função serve para o botão de cancelar e para o fim do salvar, porque as duas coisas precisam ser desfeitas nos dois casos. No salvar ela é o que devolve o formulário ao estado de criação — e por isso o cancelarEdicao vem antes do recarregar, e não depois.

O estado do formulário e o editandoId têm de voltar juntos sempre. Desfazer um e não o outro produz os dois defeitos clássicos: ou a tela mostra um id que não existe mais, ou ela mostra um formulário cheio sem nenhum id — e o salvar tenta criar uma tarefa com o texto de outra.

Exemplo

// Editar: o mesmo formulario de criar, com o `id` guardado em estado
// separando os dois caminhos. Sem ele, nao ha como saber se o `salvar`
// insere ou altera.
const banco = { tarefas: [
  { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 },
  { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 },
] };

function executeSql(sql, parametros) {
  const insert = /INSERT INTO (\w+) \((.+)\) VALUES \((.+)\)$/i.exec(sql);
  if (insert) {
    const nomes = insert[2].split(',').map((c) => c.trim());
    const linha = { id: banco.tarefas.length + 1 };
    nomes.forEach((coluna, i) => { linha[coluna] = parametros[i]; });
    banco.tarefas.push(linha);
    return [{ insertId: linha.id, rowsAffected: 1 }];
  }

  const update = /UPDATE (\w+) SET (.+) WHERE id = (.+)$/i.exec(sql);
  if (update) {
    const [, tabela, atribuicoes, valorId] = update;
    // Os `?` da consulta sao os buracos onde o valor entra, e a ordem deles
    // e a ordem do array de parametros: primeiro o do `SET`, na ordem em que
    // as colunas aparecem, e por ultimo o do `WHERE`. Sem esta conta o motor
    // grava o proprio texto `?` na coluna e compara `id` com `NaN`: o
    // `rowsAffected` volta 0 e o titulo nao muda, sem nenhum erro.
    const colunas = atribuicoes.split(',').map((c) => c.trim().split('=')[0].trim());
    const valores = parametros.slice(0, colunas.length);
    const filtro = parametros[colunas.length];

    let alteradas = 0;
    banco[tabela] = banco[tabela].map((linha) => {
      // o filtro usa o `id` do parametro, nao o texto do `?` da consulta
      if (linha.id !== Number(filtro ?? valorId)) return linha;
      const nova = { ...linha };
      colunas.forEach((coluna, i) => { nova[coluna] = valores[i]; });
      alteradas += 1;
      return nova;
    });
    return [{ rowsAffected: alteradas }];
  }
  throw new Error('SQL invalido: ' + sql);
}

// o estado do componente
let editandoId = null;
let formulario = { titulo: '', prazo: '', feita: 0 };

function abrirParaEditar(tarefa) {
  editandoId = tarefa.id;
  formulario = { titulo: tarefa.titulo, prazo: tarefa.prazo };
}
function abrirParaCriar() {
  editandoId = null;
  formulario = { titulo: '', prazo: '', feita: 0 };
}

function salvar() {
  if (editandoId === null) {
    const [r] = executeSql(
      'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
      [formulario.titulo, formulario.prazo, 0]
    );
    console.log('INSERT: id novo', r.insertId);
  } else {
    const [r] = executeSql(
      'UPDATE tarefas SET titulo = ?, prazo = ? WHERE id = ?',
      [formulario.titulo, formulario.prazo, editandoId]
    );
    console.log('UPDATE: rowsAffected', r.rowsAffected, '| editando o id', editandoId);
  }
  abrirParaCriar();
  return banco.tarefas;
}

console.log('--- editando a tarefa 1');
abrirParaEditar(banco.tarefas[0]);
console.log('formulario preenchido:', JSON.stringify(formulario));
console.log('editandoId:', editandoId);
formulario = { titulo: 'Revisar o WHERE e o JOIN', prazo: '2026-09-14' };
console.log('titulo no banco:', salvar()[0].titulo);
console.log('linhas no banco (nao criou nenhuma):', banco.tarefas.length);

console.log('\n--- criando uma nova pelo mesmo formulario');
abrirParaCriar();
console.log('editandoId:', editandoId, '| formulario limpo:', JSON.stringify(formulario));
formulario = { titulo: 'Enviar o relatorio', prazo: '2026-09-08' };
salvar();
console.log('linhas no banco agora:', banco.tarefas.length);
console.log('ultima linha:', banco.tarefas[banco.tarefas.length - 1]);

console.log('\n--- alternar feita direto da lista');
const alvo = banco.tarefas[0];
const [ok] = executeSql('UPDATE tarefas SET feita = ? WHERE id = ?', [alvo.feita ? 0 : 1, alvo.id]);
console.log('rowsAffected:', ok.rowsAffected, '| feita agora:', banco.tarefas[0].feita);

const [sumiu] = executeSql('UPDATE tarefas SET feita = ? WHERE id = ?', [1, 99]);
console.log('linha que nao existe -> rowsAffected:', sumiu.rowsAffected, '-> a tela recarrega');

Saída real

--- editando a tarefa 1
formulario preenchido: {"titulo":"Revisar o WHERE","prazo":"2026-09-10"}
editandoId: 1
UPDATE: rowsAffected 1 | editando o id 1
titulo no banco: Revisar o WHERE e o JOIN
linhas no banco (nao criou nenhuma): 2

--- criando uma nova pelo mesmo formulario
editandoId: null | formulario limpo: {"titulo":"","prazo":"","feita":0}
INSERT: id novo 3
linhas no banco agora: 3
ultima linha: { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0 }

--- alternar feita direto da lista
rowsAffected: 1 | feita agora: 1
linha que nao existe -> rowsAffected: 0 -> a tela recarrega