# Do modelo ao esquema relacional

Conversão de UML em tabelas, chaves primárias e estrangeiras, e validação do esquema da Loja de Bilhetes.

Página: https://resumos.rgo.pt/cadeiras/lbaw/esquema-relacional/

O [modelo conceptual](https://resumos.rgo.pt/cadeiras/lbaw/esquema-relacional/modelo-conceptual/) desenha o problema. O esquema relacional traduz esse desenho para tabelas que o PostgreSQL consegue guardar. A conversão segue regras fixas: cada regra do UML tem um destino certo nas tabelas, e aplica-las bem evita os erros que depois custam migrações.

## As regras de conversão

Cada classe vira uma tabela, cada atributo vira uma coluna e cada objeto ganha um identificador único, a **chave primária**. As associações viram **chaves estrangeiras**: colunas que referem a chave primária de outra tabela.

*   Associação de um para muitos (`1` a `1..*`): a chave estrangeira vai para o lado dos muitos. Cada sessão guarda `evento_id` a referir o evento. O evento não guarda nada sobre as sessões.
*   Associação de muitos para muitos resolvida por classe: a classe de associação vira tabela própria com duas chaves estrangeiras. A tabela `bilhetes` guarda `utilizador_id` e `sessao_id`.
*   Associação de um para um: a chave estrangeira vai para qualquer dos lados, com restrição de unicidade.

A direção da chave estrangeira é a decisão que mais erros causa. Pergunta sempre “cada objeto deste lado refere quantos do outro”. Cada sessão refere um evento, por isso a coluna vive na sessão. Se a pusesses no evento, um evento só conseguiria guardar uma sessão.

Evento `1` a Sessão `1..*`: a estrangeira `evento_id` vive na tabela `sessoes`. O evento não guarda nada sobre as sessões.

```
-- lado dos muitos guarda a referência
sessoes (..., evento_id INTEGER NOT NULL REFERENCES eventos (id))
```

Utilizador N a Sessão N, resolvida pela classe Bilhete: a tabela `bilhetes` guarda as duas estrangeiras, `utilizador_id` e `sessao_id`. A classe de associação vira sempre tabela própria.

```
bilhetes (..., utilizador_id INTEGER NOT NULL REFERENCES utilizadores (id),
          sessao_id INTEGER NOT NULL REFERENCES sessoes (id))
```

Um para um: a estrangeira vai para qualquer dos lados, com restrição de unicidade para impedir que dois objetos refiram o mesmo. Sem o `UNIQUE`, o um para um degrada-se em um para muitos silencioso.

```
-- exemplo: cada sessão tem no máximo um cartaz
cartazes (..., sessao_id INTEGER UNIQUE REFERENCES sessoes (id))
```

## Conversão completa do exemplo

Aplicadas as regras às quatro classes da Loja de Bilhetes:

```
CREATE TABLE utilizadores (
    id SERIAL PRIMARY KEY,
    nome TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    palavra_passe TEXT NOT NULL
);

CREATE TABLE eventos (
    id SERIAL PRIMARY KEY,
    titulo TEXT NOT NULL,
    descricao TEXT,
    categoria TEXT
);

CREATE TABLE sessoes (
    id SERIAL PRIMARY KEY,
    evento_id INTEGER NOT NULL REFERENCES eventos (id),
    data_hora TIMESTAMP NOT NULL,
    preco NUMERIC(8, 2) NOT NULL,
    lotacao INTEGER NOT NULL CHECK (lotacao > 0)
);

CREATE TABLE bilhetes (
    id SERIAL PRIMARY KEY,
    utilizador_id INTEGER NOT NULL REFERENCES utilizadores (id),
    sessao_id INTEGER NOT NULL REFERENCES sessoes (id),
    codigo TEXT NOT NULL UNIQUE,
    estado TEXT NOT NULL CHECK (estado IN ('reservado', 'pago', 'cancelado'))
);
```

Cada restrição do modelo aparece algures: o email único vira `UNIQUE`, a lotação positiva vira `CHECK`, o estado limitado aos três valores vira `CHECK` com a lista. O `SERIAL` cria o identificador automático que serve de chave primária. Repara que `descricao` aceita nulos e `titulo` não: um evento sem descrição ainda se vende, um evento sem título não se apresenta.

## Uma violação de chave estrangeira explicada

Tenta vender um bilhete para uma sessão que não existe:

```
INSERT INTO bilhetes (utilizador_id, sessao_id, codigo, estado)
VALUES (1, 999, 'B-0001', 'reservado');
```

O PostgreSQL recusa com erro de violação de chave estrangeira: a sessão 999 não está na tabela `sessoes`, por isso o bilhete ficaria a apontar para o vazio. Esta recusa é a integridade referencial a funcionar. Sem a chave estrangeira, a inserção passava e a aplicação mostrava um bilhete para uma sessão fantasma, com erro só na página de confirmação, longe da causa.

A mesma proteção vale ao apagar: não consegues apagar um evento que ainda tem sessões, porque as sessões ficariam órfãs. Decide por tabela o comportamento, apagar em cascata ou bloquear, em vez de descobrires a meio do projeto.

![Esquema das quatro tabelas. Sessoes aponta para eventos por evento_id. Bilhetes aponta para utilizadores por utilizador_id e para sessoes por sessao_id.](https://resumos.rgo.pt/cadeiras/lbaw/esquema-relacional/figura-1.svg)

As setas apontam da estrangeira para a primária: lê “sessões refere eventos”. Se desenhares o teu esquema assim antes de escreveres o SQL, as referências em falta saltam à vista.

Como validar o esquema

Percorre cada caso de uso e escreve as inserções que ele exige, como fizemos acima. Se algum passo precisar de uma coluna que não existe, falta um atributo. Se alguma inserção válida for recusada, há uma restrição a mais. O esquema está pronto quando todos os casos de uso passam neste teste de papel.

## Corre o esquema na prática

O esquema completo, os dados de exemplo e a pergunta final cabem num programa só. Corre-o e confirma que a última linha devolve a compra ligada ao evento:

```sql
PRAGMA foreign_keys = ON;
CREATE TABLE utilizadores (
    id INTEGER PRIMARY KEY,
    nome TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    palavra_passe TEXT NOT NULL
);
CREATE TABLE eventos (
    id INTEGER PRIMARY KEY,
    titulo TEXT NOT NULL,
    descricao TEXT,
    categoria TEXT
);
CREATE TABLE sessoes (
    id INTEGER PRIMARY KEY,
    evento_id INTEGER NOT NULL REFERENCES eventos (id),
    data_hora TEXT NOT NULL,
    preco NUMERIC(8, 2) NOT NULL,
    lotacao INTEGER NOT NULL CHECK (lotacao > 0)
);
CREATE TABLE bilhetes (
    id INTEGER PRIMARY KEY,
    utilizador_id INTEGER NOT NULL REFERENCES utilizadores (id),
    sessao_id INTEGER NOT NULL REFERENCES sessoes (id),
    codigo TEXT NOT NULL UNIQUE,
    estado TEXT NOT NULL CHECK (estado IN ('reservado', 'pago', 'cancelado'))
);
INSERT INTO eventos (titulo, descricao, categoria)
VALUES ('Noite de Fado', 'Fado na sala principal', 'musica');
INSERT INTO sessoes (evento_id, data_hora, preco, lotacao)
VALUES (1, '2026-12-14 21:30', 12.50, 100);
INSERT INTO utilizadores (nome, email, palavra_passe)
VALUES ('Ana', 'ana@example.com', 'segredo');
INSERT INTO bilhetes (utilizador_id, sessao_id, codigo, estado)
VALUES (1, 1, 'B-0001', 'reservado');
SELECT e.titulo, s.data_hora, b.codigo, b.estado
FROM bilhetes AS b
JOIN sessoes AS s ON s.id = b.sessao_id
JOIN eventos AS e ON e.id = s.evento_id;
```

A última linha imprime `Noite de Fado|2026-12-14 21:30|B-0001|reservado`. Repara nas adaptações ao motor desta página: `INTEGER PRIMARY KEY` em vez de `SERIAL` e datas como texto, porque aqui corre SQLite e não PostgreSQL. As chaves e as restrições são as mesmas do esquema.
