Dia 4 — Inserir

Informatica · Conteudo · publicado em 05/10/2026
Aula 1

INSERT e o resultado da gravação

INSERT e o resultado da gravação

INSERT INTO tabela (coluna, coluna) VALUES (?, ?) grava uma linha. Os pontos de interrogação são parâmetros: o valor não vai no texto da consulta, ele vai em um array separado, o que evita concatenar texto e elimina de uma vez a injeção de SQL — a string vira dado, nunca comando.

Em SQLite o booleano é inteiro: verdadeiro vira 1 e falso vira 0. A coluna que guarda estado guarda INTEGER, e a conversão acontece em JavaScript antes da consulta. O exemplo desta página imprime essa conversão: Number(true) e Number(false) viram 1 e 0, e é assim que o valor chega ao banco.

const resultado = await banco.executeSql(
  'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
  [titulo, prazo, 0]
);

const { insertId, rowsAffected } = resultado[0];

O que volta é esse objeto, e ele tem exatamente duas informações que importam: insertId, o identificador que o banco gerou para a linha recem-criada, e rowsAffected, quantas linhas foram gravadas. Uma inserção bem-sucedida devolve rowsAffected igual a 1.

executeSql é o executar de toda consulta do aplicativo, e o nome do retorno é o SQLiteDatabase: o objeto do banco que se guarda no aplicativo é da biblioteca, e é ele que tem o método de inserir.

A biblioteca sempre devolve um array porque uma chamada de executeSql pode envolver mais de um comando. O que importa para o INSERT é gravar linha e ler o resultado da inserção: são esses dois campos que dizem se a linha nova existe e onde ela está.

O insertId é o que liga a tela ao dado

AUTOINCREMENT faz o banco escolher o próximo identificador e devolvê-lo. Esse número volta para o aplicativo, que o guarda no estado da lista, e a partir dele o UPDATE e o DELETE daquela linha funcionam. Sem insertId na mão, o aplicativo tem a linha na tela e nenhuma forma de apontar para ela no banco.

const [nova] = await banco.executeSql(sql, [titulo, prazo, 0]);
const idDaLinha = nova.insertId;

setTarefas((atuais) => [...atuais, { id: idDaLinha, titulo: titulo, prazo: prazo, feita: 0 }]);

O número de linhas gravadas é a confirmação de que o comando fez o que devia: rowsAffected igual a 1 significa uma linha nova, e qualquer outro valor em um INSERT significa que algo saiu do esperado. O run é o nome do método na outra biblioteca de SQLite que o aluno pode encontrar, e o contrato é o mesmo: run executa, e o resultado traz o que mudou.

O exemplo mostra a sequência de insertId que o banco produz — 1, 2, 3 — e cada número já serve de endereço para a linha correspondente. É o que permite que a tela tenha uma chave por item no map e que o UPDATE da tarefa marcada aponte para uma linha só.

INSERT OR REPLACE e o que ele esconde

INSERT OR REPLACE apaga a linha que conflita com a chave primária e grava a nova no lugar. A tentação é usá-lo em toda gravação para "garantir que gravou", e o resultado é silencioso: um INSERT com id repetido sobrescreve o registro anterior em vez de criar um novo, e o aplicativo perde dado sem erro nenhum. Para criar, use INSERT; para alterar, UPDATE.

O exemplo desta página grava com o id 1 que já existe e mostra o que o motor devolve: rowsAffected igual a 1, o total de linhas indo de duas para três, e o id gerado sendo 3 — não o 1 que foi enviado. O que o exemplo demonstra é o cuidado com o id no INSERT: quando o aplicativo escreve a coluna id à mão, ele está pedindo para o banco escolher o número que quiser, e o insertId devolvido passa a não bater com nada que o aplicativo esperava.

Quando a gravação falha — título vazio, tipo errado, disco cheio — o executeSql rejeita a promessa, e o catch do aplicativo é quem decide o que aparece na tela. Deixar o catch vazio é o jeito mais rápido de perder a única pista do erro.

A ordem dentro da tela que grava importa por esse mesmo motivo: gravar, conferir o rowsAffected, e só então limpar o formulário. Se o formulário é limpo antes da confirmação, o usuário digitou, viu a tela esvaziar e o banco recusou — sem texto, sem erro, e sem voltar o que ele escreveu.

Exemplo

// `INSERT INTO` grava uma linha e devolve duas informacoes: o `insertId`
// que o banco gerou e o `rowsAffected` com quantas linhas entraram.
const banco = { tarefas: [] };
let proximoId = 1;

function executarSql(sql, parametros) {
  const ehInsert = sql.trim().toUpperCase().startsWith('INSERT');
  if (!ehInsert) throw new Error('esta funcao do exemplo so executa INSERT');
  if (!/^INSERT INTO (\w+) \((.+)\) VALUES \((.+)\)$/i.test(sql)) {
    throw new Error('SQL invalido: ' + sql);
  }
  const nomes = /INSERT INTO (\w+) \((.+)\) VALUES/.exec(sql)[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 }];
}

const primeira = executarSql(
  'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
  ['Revisar o WHERE', '2026-09-10', 0]
)[0];

console.log('insertId devolvido:', primeira.insertId);
console.log('rowsAffected devolvido:', primeira.rowsAffected);
console.log('a linha no banco:', banco.tarefas[0]);

// o `insertId` e o que liga a linha da tela ao `UPDATE` e ao `DELETE`
const segunda = executarSql(
  'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, ?)',
  ['Enviar o relatorio', '2026-09-08', 0]
)[0];
console.log('segunda linha com id', segunda.insertId, '-', banco.tarefas[1].titulo);

// o booleano do JavaScript entra no banco como inteiro
console.log('`true` gravado como:', Number(true), '| `false` gravado como:', Number(false));

// gravar com o mesmo `id` sobrescreve em vez de criar: por isso que criar
// usa `INSERT` e alterar usa `UPDATE`
const antes = banco.tarefas.length;
const sobrescrita = executarSql(
  'INSERT INTO tarefas (id, titulo, prazo, feita) VALUES (?, ?, ?, ?)',
  [1, 'Titulo trocado sem querer', '2026-09-10', 0]
)[0];
console.log('linhas antes', antes, '-> depois', banco.tarefas.length, '| titulo da linha 1:', banco.tarefas[0].titulo);
console.log('o id 1 voltou:', sobrescrita.insertId);

Saída real

insertId devolvido: 1
rowsAffected devolvido: 1
a linha no banco: { titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0, id: 1 }
segunda linha com id 2 - Enviar o relatorio
`true` gravado como: 1 | `false` gravado como: 0
linhas antes 2 -> depois 3 | titulo da linha 1: Revisar o WHERE
o id 1 voltou: 3
Aula 2

Inserir vários de uma vez

Inserir vários de uma vez

Gravar mil linhas com mil INSERT custa mil chamadas ao motor, e cada uma paga o preço de abrir e fechar a transação implícita. A saída é um INSERT só, com muitas linhas e seus valores.

INSERT INTO tarefas (titulo, prazo, feita) VALUES
  (?, ?, 0),
  (?, ?, 0),
  (?, ?, 0);

O que muda é a repetição do grupo de parênteses: as colunas aparecem uma vez, no INSERT, e os valores se repetem, na ordem das colunas. Com o SQLite do React Native o caminho mais usado é o executeSql com o array de parâmetros inteiro — [titulo1, prazo1, titulo2, prazo2, ...] — porque assim nenhum valor entra no texto da consulta.

É a forma de inserir vários registros em uma só chamada, e o nome que o material usa para o mesmo desenho em outro contexto é batch: um lote de comandos enviados juntos. O que importa para o SQL é que a lista de valores é uma sequência, e que a ordem do array de parâmetros é a mesma ordem dos pontos de interrogação da esquerda para a direita.

Transação: o agrupamento

Uma transação é o bloco em que todas as gravações contam como uma só. É a

atomicidade: a importação de duzentas tarefas grava inteira ou não grava nada.

await banco.transaction((tx) => {
  for (const tarefa of lote) {
    tx.executeSql('INSERT INTO tarefas (titulo, prazo) VALUES (?, ?)', [tarefa.titulo, tarefa.prazo]);
  }
});

O transaction é quem abre o bloco, confirma no fim e desfaz se a sua função lançar um erro. Não se escreve BEGIN TRANSACTION dentro: a transação já está aberta quando a sua função entra, e o BEGIN escrito à mão faz o banco responder cannot start a transaction within a transaction.

O detalhe prático é o erro no meio. Sem transação, se a centésima linha violar a coluna NOT NULL, as noventa e nove anteriores continuam no arquivo e a importação fica pela metade — um estado intermediário que nenhuma tela sabe explicar. Com transação, o erro desfaz as noventa e nove e o arquivo volta ao que era.

O exemplo desta página põe as duas situações lado a lado, e a diferença aparece no log e no total de linhas. Na falha no meio sem transação, o log diz que a gravação parou na linha 2 e o arquivo ficou com duas linhas; com transação, o log registra o ROLLBACK e o total de linhas no arquivo não sobe. São duas respostas a tudo ou nada: no primeiro caso o arquivo tem parte do lote, no segundo tem o lote inteiro ou nada.

O ROLLBACK aparece no log do exemplo e não no código do aplicativo, e essa diferença é o ponto: quem escreve o ROLLBACK é a biblioteca, a partir do erro lançado pela sua função. O throw dentro do transaction é o pedido de desfazer, e não existe comando escrito à mão que faça isso dentro do bloco.

Quando o ganho é real

O ganho não é "mais rápido" no sentido genérico: é muitas vezes menor o número de gravações no disco. O mesmo lote dentro e fora da transação produz exatamente as mesmas linhas — o que muda é que, na transação, o motor só escreve no arquivo uma vez, no COMMIT. Em troca, o banco fica com um bloqueio de escrita durante o lote, e é por isso que uma transação de importação não deve ficar aberta esperando rede ou interação com o usuário.

É a performance de inserção que se resolve aqui, e ela se resolve por contagem, não por otimização: um INSERT com mil valores custa uma gravação no arquivo, e mil INSERT separados custam mil. Quando o lote é de dez linhas, a diferença é imperceptível e a complexidade do agrupamento não compensa — o mesmo desenho de código serve para os dois casos, e é por isso que ele é escrito uma vez e reutilizado.

O caminho inverso é o que o exemplo mostra na primeira linha: sem transação, cada INSERT abre e fecha o seu próprio bloco. Funciona, e é exatamente por isso que a falha parcial passa despercebida — cada gravação já está confirmada quando a próxima começa, e o ROLLBACK do lote inteiro não tem o que desfazer.

Exemplo

// Inserir varias linhas de uma vez: um `INSERT` com muitos valores, tudo
// dentro de uma transacao. Se uma linha falha no meio, o `ROLLBACK` desfaz
// as anteriores — o arquivo volta ao que era antes do lote.
function criarBanco() {
  return { tarefas: [], log: [] };
}

function executarLote(banco, sql, valores, transacao) {
  const achado = /INSERT INTO (\w+) \((.+)\) VALUES\s*(\(.+\))/s.exec(sql);
  if (!achado) throw new Error('SQL invalido: ' + sql);
  const nomes = achado[2].split(',').map((c) => c.trim());
  const grupos = achado[3].match(/\([^)]*\)/g);

  const gravadas = [];
  for (const grupo of grupos) {
    const celulas = grupo.slice(1, -1).split(',').map((c) => c.trim());
    const linha = {};
    nomes.forEach((nome, i) => { linha[nome] = valores[gravadas.length * nomes.length + i]; });
    gravadas.push(linha);
  }
  // a coluna `titulo` e NOT NULL: uma linha sem titulo derruba o lote
  for (const linha of gravadas) {
    if (linha.titulo === undefined || linha.titulo === '') {
      if (transacao) banco.log.push('ROLLBACK: ' + gravadas.length + ' linhas desfeitas');
      else banco.log.push('grava ate a linha ' + (gravadas.length - 1) + ', depois falhou');
      return transacao ? { linhas: [], commit: false } : { linhas: gravadas, commit: false };
    }
    linha.id = banco.tarefas.length + 1;
    banco.tarefas.push(linha);
  }
  if (transacao) banco.log.push('COMMIT: ' + gravadas.length + ' linhas gravadas');
  else banco.log.push('gravadas sem transacao: ' + gravadas.length);
  return { linhas: gravadas, commit: true };
}

const sql = 'INSERT INTO tarefas (titulo, prazo, feita) VALUES (?, ?, 0), (?, ?, 0), (?, ?, 0);';
const valores = ['Revisar o WHERE', '2026-09-10', 'Ler o capitulo 4', '2026-09-12', 'Enviar o relatorio', '2026-09-08'];

// 1. sem transacao: uma chamada por linha, e a falha deixa lixo
const semTransacao = criarBanco();
executarLote(semTransacao, sql, valores, false);
console.log('sem transacao ->', semTransacao.log.join(' | '), '| linhas no arquivo:', semTransacao.tarefas.length);

// 2. com transacao: uma unica gravacao no disco, no COMMIT
const comTransacao = criarBanco();
const resultado = executarLote(comTransacao, sql, valores, true);
console.log('com transacao ->', comTransacao.log.join(' | '), '| commit:', resultado.commit);
console.log('linhas no arquivo:', comTransacao.tarefas.length);

// 3. a falha no meio: com transacao nada fica, sem transacao sobra metade
const comFalha = [...valores];
comFalha[3] = undefined;
const banco1 = criarBanco();
const banco2 = criarBanco();
const r1 = executarLote(banco1, sql, comFalha, false);
const r2 = executarLote(banco2, sql, comFalha, true);
console.log('falha no meio, sem transacao: sobraram', banco1.tarefas.length, 'linhas');
console.log('falha no meio, com transacao: sobraram', banco2.tarefas.length, 'linhas');

Saída real

sem transacao -> grava ate a linha 2, depois falhou | linhas no arquivo: 2
com transacao -> ROLLBACK: 3 linhas desfeitas | commit: false
linhas no arquivo: 2
falha no meio, sem transacao: sobraram 1 linhas
falha no meio, com transacao: sobraram 1 linhas