Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migrations

Com o modelo definido, precisamos criar essas tabelas no banco de dados. Poderíamos abrir um terminal, conectar no PostgreSQL e executar os comandos CREATE TABLE manualmente. Funciona, uma vez. Mas quando um colega clonar o repositório e precisar criar o mesmo banco, ele precisaria repetir tudo.

Para resolver isso, usamos migrations (migrações) — arquivos SQL versionados que descrevem mudanças na estrutura do banco de dados. Cada migration é um passo: criar uma tabela, adicionar uma coluna, remover um constraint. A sequência completa das migrations reproduz o banco inteiro do zero.

A primeira migration: events

A primeira tabela que vamos criar é events — o recurso central do TamoJunto. Por enquanto, sem a coluna organizer_id: a tabela users ainda não existe, e o foreign key precisa referenciar uma tabela que já esteja criada.

Crie um diretório backend/migrations/ e dentro dele o arquivo 001_create_events.sql:

Arquivo: backend/migrations/001_create_events.sql
CREATE TABLE events (
    id          SERIAL PRIMARY KEY,
    title       VARCHAR(200)  NOT NULL,
    description TEXT,
    location    VARCHAR(200)  NOT NULL,
    date        TIMESTAMP     NOT NULL,
    capacity    INTEGER       NOT NULL CHECK (capacity > 0),
    created_at  TIMESTAMP     NOT NULL DEFAULT NOW()
);

Código 5-1: Migration que cria a tabela events (sem foreign key por enquanto).

Algumas decisões nesse SQL:

  • SERIAL PRIMARY KEY gera um inteiro auto-incrementado e o marca como chave primária — cada registro recebe um id único automaticamente.
  • NOT NULL impede que a coluna fique vazia. Um evento sem título ou sem local não faz sentido no TamoJunto.
  • CHECK (capacity > 0) é um constraint que impede eventos com capacidade zero ou negativa. O banco de dados valida isso a cada inserção e atualização.
  • DEFAULT NOW() preenche created_at automaticamente com a data/hora atual no momento da inserção.

A segunda migration: users e o foreign key

A segunda migration faz duas coisas: cria a tabela users e adiciona a coluna organizer_id à tabela events já existente.

Arquivo: backend/migrations/002_create_users_add_fk.sql
CREATE TABLE users (
    id         SERIAL PRIMARY KEY,
    name       VARCHAR(100)  NOT NULL,
    email      VARCHAR(200)  NOT NULL UNIQUE,
    created_at TIMESTAMP     NOT NULL DEFAULT NOW()
);

ALTER TABLE events
    ADD COLUMN organizer_id INTEGER NOT NULL DEFAULT 1 REFERENCES users(id);

Código 5-2: Migration que cria users e adiciona o foreign key em events.

O ALTER TABLE mostra algo importante: migrations não servem apenas para criar tabelas. Elas também modificam tabelas existentes — adicionar colunas, remover constraints, alterar tipos. Essa é a natureza incremental das migrations: cada arquivo descreve uma diferença em relação ao estado anterior do banco.

O DEFAULT 1 na coluna organizer_id existe porque a tabela events pode já conter registros (se o seed tivesse rodado antes, por exemplo). Sem um valor padrão, o ALTER TABLE ADD COLUMN ... NOT NULL falharia — o banco não saberia qual valor colocar nas linhas existentes. Numa aplicação real, esse valor padrão seria removido numa migration posterior; aqui, ele serve para que o SQL execute sem erros.

A cláusula REFERENCES users(id) declara o foreign key. O PostgreSQL garante que todo valor em organizer_id corresponde a um id existente na tabela users.

Executando as migrations

Para executar as migrations, vamos usar um script simples que lê os arquivos .sql do diretório migrations/ em ordem e executa cada um no banco. Crie o arquivo backend/migrate.ts:

Arquivo: backend/migrate.ts
import { Client } from "pg";
import fs from "fs";
import path from "path";

async function migrate() {
  const client = new Client({
    connectionString: process.env.DATABASE_URL,
  });
  await client.connect();

  const migrationsDir = path.join(__dirname, "migrations");
  const files = fs.readdirSync(migrationsDir).sort();

  for (const file of files) {
    if (!file.endsWith(".sql")) continue;
    const sql = fs.readFileSync(path.join(migrationsDir, file), "utf8");
    console.log(`Executando: ${file}`);
    await client.query(sql);
  }

  console.log("Migrations concluídas.");
  await client.end();
}

migrate().catch((err) => {
  console.error("Erro ao executar migrations:", err.message);
  process.exit(1);
});

Código 5-3: Script que aplica migrations em ordem.

Esse script é propositalmente simples: lê os arquivos em ordem alfabética (por isso o prefixo numérico 001_, 002_), executa cada SQL e pronto. Não rastreia quais migrations já foram aplicadas — se rodar duas vezes, a segunda execução falha porque as tabelas já existem. Para um projeto em sala de aula, isso é aceitável. Recrie o banco quando precisar rodar as migrations novamente1.

Para executar, usamos o tsx, um executor de TypeScript que compila e roda num passo só:

$ cd backend
$ DATABASE_URL=postgres://localhost:5432/tamojunto npx tsx migrate.ts
Executando: 001_create_events.sql
Executando: 002_create_users_add_fk.sql
Migrations concluídas.

As tabelas agora existem no banco. Verificando:

$ psql postgres://localhost:5432/tamojunto -c '\dt'
          List of relations
 Schema |  Name  | Type  |  Owner
--------+--------+-------+---------
 public | events | table | pedrov
 public | users  | table | pedrov
(2 rows)

  1. Em projetos de produção, ferramentas como Knex, Prisma ou Flyway rastreiam quais migrations já foram aplicadas e executam apenas as pendentes. O conceito é o mesmo, só a automação é maior. ↩