Dia 3 — SQLite: o que é um banco relacional no celular
O que é SQLite e por que ele cabe no celular
O que é SQLite e por que ele cabe no celular
SQLite é um banco de dados relacional inteiro dentro de uma biblioteca, sem servidor nenhum. Não há processo do banco rodando, não há porta, não há usuário e senha: o aplicativo abre um arquivo, o motor está dentro do próprio app, e a comunicação é uma chamada de função. É a razão de ele caber em um celular — o arquivo inteiro fica no armazenamento do aparelho e a biblioteca entra no aplicativo com poucos megabytes.
O termo que resume a arquitetura é que o SQLite é embarcado: o motor viaja dentro do aplicativo, e não em um processo separado que o aplicativo procure na rede. A alternativa é o servidor de banco de dados, que é o que o MySQL do 2º trimestre é: um programa rodando sozinho, esperando conexão, com usuário e senha.
A diferença entre o MySQL do 2º trimestre e o SQLite é só essa, e ela muda a forma de escrever o código. Com MySQL existia um servidor: uma conexão, um host, um usuário, e a consulta atravessava a rede. Com SQLite a consulta é uma chamada local que o motor resolve lendo o arquivo.
| MySQL | SQLite | |
|---|---|---|
| Onde roda | processo separado | dentro do aplicativo |
| Endereço | host, porta, usuário, senha | caminho do arquivo |
| Arquivos | um por tabela | um só, .db ou .sqlite |
| Conexões | servidor aguenta muitas | o aplicativo tem a sua |
| Consultas | as mesmas | as mesmas |
A última linha da tabela é a que economiza o trimestre inteiro: a linguagem é a mesma. Tudo que o aluno aprendeu sobre SELECT, WHERE, INSERT e chave estrangeira vale aqui sem tradução. O comparar com MySQL não serve para aprender SQL — serve para entender o que muda quando o banco sai do computador e entra no celular, que é só a linha de "onde roda".
Um arquivo, não vários
Todo o banco é um arquivo único: as tabelas, os índices, as linhas, tudo dentro do mesmo arquivo. Copiar o banco é copiar o arquivo; apagar o aplicativo é apagar o banco, porque ele mora no diretório dele. Isso resolve um problema que o MySQL não tem — onde o dado fica no celular — e cria outro: dois aplicativos não compartilham o mesmo arquivo, e por isso não existe um servidor central respondendo consultas de vários aplicativos ao mesmo tempo.
O arquivo do banco costuma chamar app.db ou banco.sqlite, e o aplicativo escolhe o nome. O que importa é que ele fica no armazenamento interno do app, que o sistema só libera para aquele aplicativo.
A razão de o arquivo único ser possível é que não existe comunicação com rede, portanto não existe estado espalhado em dois lugares. O preço é que o acesso é exclusivo: enquanto o aplicativo tem o arquivo aberto para escrever, mais ninguém o escreve. Com o MySQL, dois aplicativos podiam apontar para o mesmo servidor e ambos escrever; aqui, cada um tem o seu arquivo, e o compartilhamento de dados entre aparelhos passa a ser assunto de rede — assunto do ano seguinte.
Por que relacional no celular
O mesmo motivo que fez o banco relacional ganhar o computador vale aqui: os dados têm forma — uma tarefa tem título, prazo e estado; o estado é um entre poucos valores; duas coleções se ligam por um identificador. Desenhar colunas com tipo declarado e regra de integridade resolve o que o aplicativo não deve resolver sozinho: impedir prazo vazio, impedir dois registros com o mesmo identificador, impedir referência a uma linha que não existe.
O que SQLite é leve é o motivo de ele entrar no celular sem discussão: ele não instala nada no aparelho, não pede permissão ao usuário e não roda serviço em segundo plano. O que ele é rápido vem da mesma origem — sem rede, a consulta é tempo de leitura de arquivo, e a leitura de arquivo é rápida.
Quando a pergunta que o app faz ao dado é "quais tarefas da categoria trabalho estão vencidas", a estrutura relacional responde isso sem trazer o resto. Esse é o critério: banco entra quando o dado é coleção e a pergunta tem forma.
O exemplo desta página monta o schema como o banco receberia, mostra o CREATE TABLE que sai da descrição de colunas, e depois trata o "arquivo" como um objeto cujas tabelas são listas de linhas. É assim que o resto do trimestre escreve: a consulta vira chamada de função, e o resultado vira array de objetos que o map do React sabe desenhar.
Exemplo
// SQLite e um banco relacional dentro de uma biblioteca: sem servidor, // sem host, sem senha. O arquivo e um so, e a consulta e uma chamada de // funcao local. O exemplo monta o schema e a tabela, e mostra que a // estrutura mora no arquivo, nao em um processo separado. const caminhoDoArquivo = '/data/app.db'; // o schema: nome da tabela e a lista de colunas, cada uma com tipo e regra const schema = { tarefas: { colunas: [ { nome: 'id', tipo: 'INTEGER', regra: 'PRIMARY KEY' }, { nome: 'titulo', tipo: 'TEXT', regra: 'NOT NULL' }, { nome: 'prazo', tipo: 'TEXT', regra: 'NOT NULL' }, { nome: 'feita', tipo: 'INTEGER', regra: 'DEFAULT 0' }, ], }, }; // o texto que o SQLite receberia para criar a tabela const colunasEmSql = schema.tarefas.colunas .map((coluna) => `${coluna.nome} ${coluna.tipo} ${coluna.regra}`.trim()) .join(', '); console.log('CREATE TABLE tarefas (' + colunasEmSql + ');'); // o "arquivo" e um objeto: cada tabela e um array de linhas 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 }, { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0 }, ], }; console.log('arquivo do banco:', caminhoDoArquivo); console.log('tabelas que existem nele:', Object.keys(banco).join(', ')); console.log('linhas gravadas em tarefas:', banco.tarefas.length); // a consulta e uma chamada local: nao ha rede, nao ha servidor const vencidas = banco.tarefas.filter((linha) => linha.prazo <= '2026-09-09'); console.log('SELECT ... WHERE prazo <= ... devolveu', vencidas.map((l) => l.titulo)); // apagar o aplicativo apaga o arquivo, e o arquivo e o banco inteiro delete banco.tarefas; console.log('depois que o arquivo vai embora:', Object.keys(banco).length, 'tabelas');
Saída real
CREATE TABLE tarefas (id INTEGER PRIMARY KEY, titulo TEXT NOT NULL, prazo TEXT NOT NULL, feita INTEGER DEFAULT 0); arquivo do banco: /data/app.db tabelas que existem nele: tarefas linhas gravadas em tarefas: 3 SELECT ... WHERE prazo <= ... devolveu [ 'Enviar o relatorio' ] depois que o arquivo vai embora: 0 tabelas
Abrir o banco e criar a tabela
Abrir o banco e criar a tabela
O acesso ao SQLite no React Native vem de uma biblioteca, e a biblioteca expõe uma função só para começar: openDatabase. Ela devolve o objeto do banco, e esse objeto é o que recebe todas as consultas do aplicativo inteiro.
import SQLite from 'react-native-sqlite-storage'; SQLite.enablePromise(true); const banco = await SQLite.openDatabase({ name: 'app.db', location: 'default', }); await banco.executeSql('CREATE TABLE IF NOT EXISTS tarefas (...)');
enablePromise(true) é o que faz executeSql devolver promessa em vez de receber função de retorno. Sem essa linha, todo o código do aplicativo vira banco.executeSql(sql, [], funcaoDeRetorno) — o estilo de retorno, que é mais antigo e mais difícil de acertar.
name é o nome do arquivo. location: 'default' escolhe onde ele fica dentro do aparelho, e é o valor certo na esmagadora maioria dos casos — o Android guarda em /data/data/<pacote>/databases e o iOS em Library. Abrir o mesmo nome duas vezes devolve o mesmo banco: o nome é a identidade do arquivo.
Conectar ao banco acontece uma vez por aplicativo, e o objeto que volta é o mesmo para todas as telas. É a decisão de arquitetura que evita o problema clássico: se cada tela abrir o seu banco de dados, cada tela terá o seu arquivo, e a gravação de uma não aparece na outra. O padrão é abrir no ponto de entrada e passar o objeto para baixo.
O detalhe do await no openDatabase é o mesmo do setItem do chave-valor: sem ele, a próxima linha roda antes de o arquivo existir, e o primeiro CREATE TABLE falha com um erro de arquivo inexistente.
A primeira tabela
CREATE TABLE descreve a estrutura uma vez. O que aparece entre parênteses é a lista de colunas, e cada coluna é nome, tipo e regra, nessa ordem.
CREATE TABLE tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, prazo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0 );
O nome da tabela é a primeira decisão de modelagem, e vale a regra de não usar acento nem espaço: tarefas funciona em toda consulta, Lista de Tarefas obriga o aluno a cercar o nome em crase em cada uso. O mesmo vale para o tipo de coluna: INTEGER, TEXT, REAL e BLOB são os quatro do SQLite, escritos em maiúsculas, e a diferença entre eles vai aparecer quando a coluna receber um número com casas decimais ou um arquivo.
A ordem importa em dois pontos: PRIMARY KEY e NOT NULL têm que vir logo depois do tipo, e DEFAULT também. Uma coluna NOT NULL escrita sem tipo antes da regra é aceita pelo SQLite, porque o tipo de coluna em SQLite é uma recomendação e não uma imposição — mas o código fica enganoso.
IF NOT EXISTS faz o comando não falhar quando a tabela já existe. Sem ele, o segundo lançamento do aplicativo em uma tela que recria o banco quebra com "table already exists" — e o aluno vê isso só na segunda vez que abre o app, o pior lugar para achar um erro de digitação.
O exemplo desta página mostra que a descrição da tabela pode ser montada em JavaScript e transformada em texto: cada coluna entra com nome, tipo, obrigatoriedade, valor padrão e a marcação de chave. É assim que um aplicativo sério guarda o schema — como dado que ele pode conferir, e não como texto colado no meio da tela.
Ler a estrutura depois de criada
PRAGMA table_info(nomeDaTabela) devolve as colunas da tabela que já existe: cid, name, type, notnull, dflt_value e pk. É a forma de o aplicativo conferir, em tempo de execução, se o arquivo em disco é a versão que o código espera — a base de toda migração do dia 9.
Cada coluna do PRAGMA responde uma pergunta: cid é a posição, name o nome, type o tipo gravado, notnull se é obrigatória, dflt_value o valor padrão e pk se é a chave primária. O pk vale 1 na coluna de identidade e 0 nas outras — é por ele que o aplicativo descobre, lendo o arquivo, qual coluna é o id.
A mesma consulta que cria a tabela, em JavaScript, é a que vai para o executeSql — e o que volta é o resultado da última linha executada, o que o aplicativo normalmente ignora num CREATE TABLE, porque não há linha para ler. O PRAGMA é o caso contrário: existe para ser lido, e a linha que ele devolve é a informação.
O PRAGMA também responde sobre o que não existe: perguntar por uma tabela que nunca foi criada devolve lista vazia, e é assim que o aplicativo descobre que precisa criar banco do zero em vez de tentar usar o que veio gravado. É a mesma pergunta feita de dois jeitos que o dia 9 usa — o arquivo como fonte, o código como expectativa.
Exemplo
// A tabela se descreve uma vez, em `CREATE TABLE`. O exemplo monta a // estrutura a partir de uma descricao de colunas e mostra as tres informacoes // que `PRAGMA table_info` devolve: nome, tipo e se a coluna e obrigatoria. const descricao = [ { nome: 'id', tipo: 'INTEGER', obrigatoria: true, padrao: null, chave: true }, { nome: 'titulo', tipo: 'TEXT', obrigatoria: true, padrao: null, chave: false }, { nome: 'prazo', tipo: 'TEXT', obrigatoria: true, padrao: null, chave: false }, { nome: 'feita', tipo: 'INTEGER', obrigatoria: false, padrao: 0, chave: false }, ]; function criarTabela(nome) { const partes = descricao.map((coluna) => { let sql = coluna.nome + ' ' + coluna.tipo; if (coluna.chave) sql += ' PRIMARY KEY AUTOINCREMENT'; if (coluna.obrigatoria) sql += ' NOT NULL'; if (coluna.padrao !== null) sql += ' DEFAULT ' + coluna.padrao; return sql; }); return 'CREATE TABLE IF NOT EXISTS ' + nome + ' (' + partes.join(', ') + ');'; } console.log(criarTabela('tarefas')); // `PRAGMA table_info(tarefas)`: o que o aplicativo le para conferir a // estrutura do arquivo em disco function tableInfo(nome) { const existe = nome === 'tarefas'; if (!existe) return []; return descricao.map((coluna, posicao) => ({ cid: posicao, name: coluna.nome, type: coluna.tipo, notnull: coluna.obrigatoria ? 1 : 0, dflt_value: coluna.padrao, pk: coluna.chave ? 1 : 0, })); } const colunas = tableInfo('tarefas'); for (const coluna of colunas) { console.log( coluna.name, '-> tipo', coluna.type, '| obrigatoria:', coluna.notnull === 1, '| padrao:', coluna.dflt_value, '| chave:', coluna.pk === 1 ); } console.log('tabelas que o PRAGMA devolveu:', tableInfo('categorias').length, 'colunas'); // a tabela criada, com a linha gravada em cada coluna const tabela = { tarefas: [{ id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }] }; console.log('primeira linha:', tabela.tarefas[0]);
Saída real
CREATE TABLE IF NOT EXISTS tarefas (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, titulo TEXT NOT NULL, prazo TEXT NOT NULL, feita INTEGER DEFAULT 0);
id -> tipo INTEGER | obrigatoria: true | padrao: null | chave: true
titulo -> tipo TEXT | obrigatoria: true | padrao: null | chave: false
prazo -> tipo TEXT | obrigatoria: true | padrao: null | chave: false
feita -> tipo INTEGER | obrigatoria: false | padrao: 0 | chave: false
tabelas que o PRAGMA devolveu: 0 colunas
primeira linha: { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }