Dia 4 — Inserir
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
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