Dia 7 — Modelar o dado

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

Modelar o dado

Aula 1

Chave primária e identidade da linha

Chave primária e identidade da linha

Toda tabela relacional precisa de uma coluna que identifique a linha, e essa coluna é a chave primária. Em SQLite ela se escreve como INTEGER PRIMARY KEY, e o efeito do tipo combinado é o AUTOINCREMENT: o banco passa a escolher o próximo número sozinho.

CREATE TABLE tarefas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  prazo TEXT NOT NULL,
  feita INTEGER NOT NULL DEFAULT 0
);

Com AUTOINCREMENT, o INSERT não escreve a coluna id: ele devolve o número escolhido no insertId, e a linha existe. Sem AUTOINCREMENT — só INTEGER PRIMARY KEY — o SQLite ainda preenche o id com o próximo inteiro livre, e a diferença é que ele pode reaproveitar o número de uma linha apagada. Com AUTOINCREMENT o número nunca volta, o que garante que o id da tarefa 7 nunca vá parar em outra tarefa.

O que a chave primária resolve é o identificador: a capacidade de dizer "esta linha" e nada mais. Um identificador que aceita repetição não é identificador — é um campo. A distinção importa em tudo que vem depois, porque UPDATE e DELETE são construídos sobre ela.

O exemplo desta página monta o CREATE TABLE a partir de uma descrição de colunas e mostra o insertId que o banco devolve a cada gravação: 1, 2, 3. Depois usa o id para localizar uma linha específica e compara com o resultado de filtrar por título, que pega duas.

Por que o id e necessário

O identificador é o que permite UPDATE, DELETE e WHERE apontarem para uma linha exata. Sem ele, a única forma de localizar uma tarefa é por conteúdo — e conteúdo muda. O aplicativo que filtra por título cria duas tarefas com o mesmo título, e passa a alterar as duas; o que apaga por título apaga tarefas que o usuário não escolheu.

É o por que id em uma frase: sem ele, o filtro do UPDATE e do DELETE deixa de ser exato, e a única alternativa é reescrever o comando errado para todo caso. A linha única que o id garante é o que permite que o filtro seja id = ? e que ele receba o valor da linha que o usuário tocou.

Uma tabela sem chave tem um comportamento que o SQL não proíbe e que o aplicativo sempre sofre: duas linhas com o mesmo conteúdo são indistinguíveis, e o UPDATE que altera "a linha do título X" altera as duas. O rowsAffected denuncia — ele volta 2 onde o aplicativo esperava 1, e esse 2 é o sinal de que o filtro está atrapalhando.

IdentificadorComportamento
INTEGER PRIMARY KEY AUTOINCREMENTo banco gera, nunca repete, cresce
INTEGER PRIMARY KEYo banco gera, pode reaproveitar número apagado
TEXT PRIMARY KEY com uuido aplicativo gera, formato é texto
sem chavelinhas iguais não se distinguem

O uuid é a alternativa quando o identificador precisa existir antes de gravar — fila de sincronização, dado que vem de outro aparelho, importação que combina dois bancos. Em SQLite ele é gerado em JavaScript, porque o banco não tem gerador de uuid embutido.

A escolha entre o inteiro e o uuid é uma escolha de quando o identificador nasce. Com AUTOINCREMENT, ele nasce no momento da gravação, e o aplicativo só precisa do insertId. Com uuid, ele nasce antes — no botão, na fila, na tela de importação — e a gravação vira uma das muitas coisas que podem referenciar a linha.

O exemplo mostra o lado prático dessa escolha: a fila de sincronização do aplicativo gera os identificadores em JavaScript, e o console confirma que nenhum deles é número. É esse o teste prático — se a fila precisa existir antes de gravar, é uuid; se só precisa apontar para o que já está gravado, é inteiro.

Exemplo

// `INTEGER PRIMARY KEY AUTOINCREMENT`: o banco escolhe o proximo `id` e
// nunca reaproveita o numero de uma linha apagada. O exemplo mostra a
// diferenca entre os tres jeitos de identificar uma linha.
function criarTabela(colunas) {
  const corpo = colunas
    .map((c) => c.nome + ' ' + c.tipo + (c.regra ? ' ' + c.regra : ''))
    .join(', ');
  return 'CREATE TABLE tarefas (' + corpo + ');';
}

console.log('com AUTOINCREMENT:');
console.log(criarTabela([
  { nome: 'id', tipo: 'INTEGER', regra: 'PRIMARY KEY AUTOINCREMENT' },
  { nome: 'titulo', tipo: 'TEXT', regra: 'NOT NULL' },
]));

// o `insertId` e o numero que o banco escolheu
let contador = 0;
function inserir(banco, titulo) {
  contador += 1;
  banco.tarefas.push({ id: contador, titulo: titulo });
  return { insertId: contador, rowsAffected: 1 };
}

const banco = { tarefas: [] };
console.log('insertId da 1a gravacao:', inserir(banco, 'Revisar o WHERE').insertId);
console.log('insertId da 2a gravacao:', inserir(banco, 'Ler o capitulo 4').insertId);
console.log('insertId da 3a gravacao:', inserir(banco, 'Enviar o relatorio').insertId);

// o `id` localiza a linha exata: duas tarefas podem ter o mesmo titulo
banco.tarefas.push({ id: contador + 1, titulo: 'Revisar o WHERE' });
const alvo = banco.tarefas.filter((linha) => linha.titulo === 'Revisar o WHERE');
console.log('filtrar por titulo pega', alvo.length, 'linhas | por id pega 1');

// com AUTOINCREMENT o numero apagado nao volta
const ultimoId = banco.tarefas[banco.tarefas.length - 1].id;
const restantes = banco.tarefas.filter((linha) => linha.id !== ultimoId);
console.log('apagou o id', ultimoId, '| o proximo insertId sera', ultimoId + 1);
console.log('linhas que sobraram:', restantes.length, '| sem o id', ultimoId);

// a alternativa de texto: o aplicativo gera o identificador
function uuidSimples() {
  return 'tarefa-' + Math.random().toString(16).slice(2, 10);
}
const filaDeSincronizacao = [
  { id: uuidSimples(), titulo: 'Sincronizar do servidor', pendente: 1 },
  { id: uuidSimples(), titulo: 'Enviar pendencia', pendente: 1 },
];
console.log('fila com id gerado pelo app:', filaDeSincronizacao.map((linha) => linha.id));
console.log('nenhum deles e numero:', filaDeSincronizacao.every((linha) => !/^\d+$/.test(linha.id)));

Saída real

com AUTOINCREMENT:
CREATE TABLE tarefas (id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL);
insertId da 1a gravacao: 1
insertId da 2a gravacao: 2
insertId da 3a gravacao: 3
filtrar por titulo pega 2 linhas | por id pega 1
apagou o id 4 | o proximo insertId sera 5
linhas que sobraram: 3 | sem o id 4
fila com id gerado pelo app: [ 'tarefa-bc8e4906', 'tarefa-b380193b' ]
nenhum deles e numero: true
Aula 2

Tipos, tamanho e o que a coluna aceita

Tipos, tamanho e o que a coluna aceita

O SQLite é dinamicamente tipado: o tipo na definição da coluna é uma recomendação, e o valor que entra é gravado do jeito que vier. titulo TEXT aceita o número 123 e grava o inteiro. Nenhum erro aparece, a consulta volta a linha, e o aplicativo quebra mais adiante em titulo.toUpperCase().

O que essa falha tem de instrução é a ordem em que ela aparece. O INSERT deu certo, o SELECT deu certo, e o erro só aparece onde o aplicativo usa o valor como texto. Por isso a defesa não está na consulta: está no tipo de coluna que o aplicativo escolhe e no que ele grava.

Duas regras resolvem: NOT NULL e o tipo certo na escrita. NOT NULL proíbe a coluna vazia, e CHECK aceita a regra que for escrita:

CREATE TABLE tarefas (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  titulo TEXT NOT NULL,
  prazo TEXT NOT NULL,
  feita INTEGER NOT NULL DEFAULT 0 CHECK (feita IN (0, 1))
);
TipoAceitaExemplo
INTEGERinteiro de 64 bitsid, contagem, 0 ou 1 para booleano
REALnúmero com parte fracionárianota, preço, percentual
TEXTqualquer sequência de caracterestítulo, data em AAAA-MM-DD
BLOBsequência de bytesimagem, arquivo

Os quatro tipos são o vocabulário inteiro de tipo do SQLite, e não há tipo de data: data é TEXT com formato AAAA-MM-DD, e a ordenação funciona porque o formato ordena junto. BLOB é o tipo para o que não é texto, e o que o celular realmente grava nele é arquivo e imagem.

Valor padrão e coluna obrigatória

DEFAULT entra em ação quando o INSERT não escreve a coluna. INSERT INTO tarefas (titulo, prazo) VALUES (?, ?) numa tabela com feita INTEGER NOT NULL DEFAULT 0 grava feita igual a 0. Sem o DEFAULT, o mesmo INSERT falha, porque NOT NULL sem valor não tem o que gravar — e a falha aparece no catch, longe do INSERT que a causou.

A coluna obrigatória e o valor padrão são o par que resolve a mesma dificuldade por lados opostos. O NOT NULL diz que a coluna sempre vai ter valor; o DEFAULT diz qual valor entra quando ninguém escreveu. Uma coluna obrigatória sem padrão é uma coluna que obriga o aplicativo a escrever algo sempre, e é aí que aparece o DEFAULT 0 da coluna feita.

O exemplo desta página mostra os dois caminhos: o INSERT sem a coluna obrigatória é recusado com a mensagem de NOT NULL, e o INSERT com as duas colunas devolve uma linha gravada. O DEFAULT então aparece sozinho na leitura — a coluna que o INSERT não escreveu entra com 0.

O tamanho máximo não existe em SQLite: TEXT não tem limite declarado. O limite real é o espaço em disco e o tempo de leitura, e quem controla isso é o aplicativo, com validação antes de gravar.

O CHECK fecha o conjunto de regras que o banco aplica no momento da escrita, e ele é o que transforma "a coluna deveria ser 0 ou 1" em uma garantia. Sem ele, feita aceita 7, e o === 1 do mapeamento do dia 5 trata 7 como pendente sem reclamar. Com ele, o valor errado é recusado na hora da gravação, e o erro de tipo no banco aparece com o nome da coluna e o da regra.

typeof diz o que foi gravado

A função typeof(coluna) devolve 'integer', 'real', 'text', 'blob' ou 'null'. É a forma mais direta de descobrir que uma coluna guardou o tipo errado, e ela funciona sobre dados que já estão gravados:

SELECT id, titulo, typeof(titulo) FROM tarefas;

A função é a resposta para o defeito do começo da aula: se a coluna TEXT guardou um inteiro, typeof mostra 'integer', e a consulta funciona sobre a tabela inteira em vez de linha por linha. O exemplo desta página percorre as linhas e imprime o tipo de cada título, e termina gravando um 123 em titulo TEXT — o typeof devolve integer, e o toUpperCase quebra com TypeError. É o defeito completo, das duas pontas: o banco aceitou e o aplicativo caiu.

O nome id é uma convenção do SQLite: as colunas rowid, oid e _rowid_ existem em toda tabela sem WITHOUT ROWID. Quando INTEGER PRIMARY KEY está declarado, id é a própria chave primária; sem essa declaração, id é uma coluna comum e o aluno perde a geração automática sem perceber.

Exemplo

// O SQLite e dinamicamente tipado: o tipo da coluna e recomendacao, e o
// valor entra como vier. `NOT NULL`, `DEFAULT` e `CHECK` sao as regras que
// impedem o dado errado de chegar a tela.
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: 'Comprar cafe', prazo: null, feita: 0 },
  ],
};

// `typeof`: o que foi de fato gravado na coluna
function tipoDo(valor) {
  if (valor === null || valor === undefined) return 'null';
  if (Number.isInteger(valor)) return 'integer';
  if (typeof valor === 'number') return 'real';
  if (typeof valor === 'string') return 'text';
  return 'blob';
}

console.log('id, titulo e o tipo REALMENTE gravado:');
for (const linha of banco.tarefas) {
  console.log(' ', linha.id, '|', linha.titulo, '-> typeof =', tipoDo(linha.titulo));
}

// `TEXT NOT NULL`: gravar sem a coluna obrigatoria falha, e o `catch`
// do aplicativo e quem ve a falha
function inserir(colunas, valores) {
  const obrigatorias = { titulo: true, prazo: true };
  for (const coluna of Object.keys(obrigatorias)) {
    if (!colunas.includes(coluna) && obrigatorias[coluna]) {
      throw new Error('NOT NULL constraint failed: ' + coluna);
    }
  }
  return { rowsAffected: 1, insertId: banco.tarefas.length + 1 };
}

try {
  inserir(['titulo'], ['Sem prazo']);
  console.log('gravou (nao deveria ter gravado)');
} catch (erro) {
  console.log('INSERT sem a coluna obrigatoria:', erro.message);
}

const comPrazo = inserir(['titulo', 'prazo'], ['Com prazo', '2026-09-15']);
console.log('INSERT com as duas:', comPrazo.rowsAffected, 'linha');

// `DEFAULT`: a coluna que o `INSERT` nao escreve entra com o valor padrao
function valorDaColuna(linha, coluna) {
  if (coluna in linha) return linha[coluna];
  return coluna === 'feita' ? 0 : null;
}
console.log('feita de quem o INSERT nao escreveu:', valorDaColuna({}, 'feita'));

// `CHECK`: so os valores permitidos entram
function confereCheck(valor) {
  if (valor !== 0 && valor !== 1) {
    throw new Error('CHECK constraint failed: feita');
  }
  return valor;
}
console.log('CHECK aceita 0 e 1:', confereCheck(0), confereCheck(1));
try {
  confereCheck(7);
} catch (erro) {
  console.log('CHECK recusa:', erro.message);
}

// e o tipo errado que passa sem erro: `TEXT` gravando um numero
const linhaEstranha = { id: 4, titulo: 123, prazo: '2026-09-20', feita: 0 };
console.log('titulo gravado como numero:', linhaEstranha.titulo, '-> typeof =', tipoDo(linhaEstranha.titulo));
try {
  console.log(linhaEstranha.titulo.toUpperCase());
} catch (erro) {
  console.log('o aplicativo quebra tarde:', erro.constructor.name + ':', erro.message);
}

Saída real

id, titulo e o tipo REALMENTE gravado:
  1 | Revisar o WHERE -> typeof = text
  2 | Ler o capitulo 4 -> typeof = text
  3 | Comprar cafe -> typeof = text
INSERT sem a coluna obrigatoria: NOT NULL constraint failed: prazo
INSERT com as duas: 1 linha
feita de quem o INSERT nao escreveu: 0
CHECK aceita 0 e 1: 0 1
CHECK recusa: CHECK constraint failed: feita
titulo gravado como numero: 123 -> typeof = integer
o aplicativo quebra tarde: TypeError: linhaEstranha.titulo.toUpperCase is not a function