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.
A sessão S12 pertence ao evento E3. No esquema relacional, a linha de sessoes guarda evento_id = 3, que referencia a linha do evento. O mapeamento traduz classes e associações em tabelas e restrições. Algumas regras, como criar cada evento com pelo menos uma sessão, exigem coordenação além da chave estrangeira.
As regras de conversão
No nosso modelo sem generalizações, cada classe vira uma tabela e cada atributo vira uma coluna. Cada objeto ganha um identificador único, a chave primária. As associações viram chaves estrangeiras: colunas que referem uma chave candidata elegível da tabela referida, normalmente a primária. A tabela referida pode ser a própria tabela, como em hierarquias com parent_id.
- Associação de um para muitos (
1a1..*): a chave estrangeira vai para o lado dos muitos. Cada sessão guardaevento_ida referir o evento. O evento não guarda nada sobre as sessões. - Associação de muitos para muitos resolvida por classe: a ligação vira tabela própria com duas chaves estrangeiras. A tabela
bilhetesguardautilizador_idesessao_id. - Associação de um para um: escolhe o lado da referência segundo a obrigatoriedade e as operações, e acrescenta unicidade. Se um cartaz exige uma sessão mas uma sessão pode não ter cartaz, guardar a FK no cartaz evita referências vazias.
A direção da chave estrangeira é a decisão que mais erros causa. Pergunta sempre “cada objeto deste lado refere quantos do outro”, porque cada sessão refere um evento e 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 entidade Bilhete mantém identidade própria.
bilhetes (..., utilizador_id INTEGER NOT NULL REFERENCES utilizadores (id),
sessao_id INTEGER NOT NULL REFERENCES sessoes (id))Um para um: a estrangeira com UNIQUE impede que dois cartazes refiram a mesma sessão. NOT NULL exige uma sessão para cada cartaz. 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 NOT NULL UNIQUE REFERENCES sessoes (id))Conversão completa do exemplo
Aplicadas as regras às quatro classes da Loja de Bilhetes:
CREATE TABLE utilizadores (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nome TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
palavra_passe_hash TEXT NOT NULL
);
CREATE TABLE eventos (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
titulo TEXT NOT NULL,
descricao TEXT,
categoria TEXT
);
CREATE TABLE sessoes (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
evento_id INTEGER NOT NULL REFERENCES eventos (id),
data_hora TIMESTAMP NOT NULL,
preco NUMERIC(8, 2) NOT NULL CHECK (preco >= 0),
lotacao INTEGER NOT NULL CHECK (lotacao > 0)
);
CREATE TABLE bilhetes (
id INTEGER GENERATED ALWAYS AS IDENTITY 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'))
);
Algumas restrições são locais: o email único vira UNIQUE, a lotação positiva vira CHECK e o estado limitado aos três valores vira CHECK com a lista; GENERATED ALWAYS AS IDENTITY gera os identificadores, enquanto PRIMARY KEY acrescenta unicidade e obrigatoriedade. A FK continua a ser um inteiro normal, sem gerar novos identificadores, e SERIAL é a alternativa antiga, ainda suportada. A exigência de pelo menos uma sessão por evento e o limite total de bilhetes ainda não são garantidos por este SQL; descricao aceita nulos e titulo não, pois um evento sem descrição ainda se vende, mas 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, pois a sessão 999 não está na tabela sessoes e 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, por isso decide por tabela o comportamento, apagar em cascata ou bloquear, em vez de descobrires a meio do projeto.
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.
Seguir uma compra entre tabelas
A consulta final parte de um bilhete, segue a sessão e encontra o evento. As condições ON ligam a FK de cada linha à PK da linha que ela refere:
CREATE TABLE utilizadores (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nome TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
palavra_passe_hash TEXT NOT NULL
);
CREATE TABLE eventos (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
titulo TEXT NOT NULL,
descricao TEXT,
categoria TEXT
);
CREATE TABLE sessoes (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
evento_id INTEGER NOT NULL REFERENCES eventos (id),
data_hora TIMESTAMP NOT NULL,
preco NUMERIC(8, 2) NOT NULL CHECK (preco >= 0),
lotacao INTEGER NOT NULL CHECK (lotacao > 0)
);
CREATE TABLE bilhetes (
id INTEGER GENERATED ALWAYS AS IDENTITY 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_hash)
VALUES ('Ana', 'ana@example.com', 'hash_apenas_para_demonstracao');
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;Executa a consulta para ver as tabelas.
O bloco usa os mesmos tipos e restrições PostgreSQL do esquema anterior. Cada execução cria uma base em memória e gera de novo os identificadores. O hash mostrado é apenas um marcador de dados de teste, não serve para autenticar.
Chaves, domínios e NULL
Uma chave primária é única e não nula. Uma chave candidata resulta das regras do domínio e é mínima; só uma é escolhida como primária, mas as outras continuam a exigir a unicidade apropriada. Uma chave artificial não substitui UNIQUE(email) quando o email deve ser único.
Em PostgreSQL, CHECK(preco >= 0) aceita a linha se a expressão der verdadeiro ou desconhecido. Para impedir um preço nulo, acrescenta NOT NULL. O DEFAULT fornece um valor quando ele é omitido; não impede a pessoa de enviar outro nem substitui uma restrição.
Por defeito, UNIQUE permite vários NULL em PostgreSQL, porque não os trata como valores iguais. Quando a regra precisa de outra semântica, considera NULLS NOT DISTINCT nas versões que o suportam, ou um desenho alternativo. Uma restrição de email único pode ainda exigir uma política para maiúsculas.
Apagar e referenciar
| Ação da chave estrangeira | Consequência |
|---|---|
| NO ACTION | Recusa uma violação quando a restrição é verificada; pode ser adiada se declarada deferrable. |
| RESTRICT | Impede a operação de imediato, sem a possibilidade equivalente de adiamento. |
| CASCADE | Propaga a eliminação ou alteração às linhas que referenciam. |
| SET NULL | Remove a referência; exige que as colunas e outras restrições permitam NULL. |
| SET DEFAULT | Aplica o default, que também tem de respeitar a chave estrangeira. |
Não escolhas CASCADE para tudo. Eliminar uma categoria pode justificar outra ação que eliminar um documento fiscal. O sentido da FK também importa: eliminar um bilhete não apaga a sessão que ele refere.
Mapear subtipos e relações opcionais
Para uma especialização há três estratégias, cada uma com um custo diferente na verificação das regras:
- Tabela de supertipo e uma por subtipo, com a chave de cada subtipo também a ser FK para o supertipo. Partilha atributos sem os duplicar, mas a regra «todo o supertipo tem exatamente um destes subtipos» não aparece automaticamente só com essas FKs.
- Tabela única com discriminador e colunas específicas. Precisa de CHECKs que liguem o discriminador aos atributos exigidos; pode produzir muitos NULL.
- Tabela por classe concreta, repetindo nela os atributos do supertipo. Evita certas junções, mas uma consulta a todos os utilizadores pode exigir
UNION. Se os subtipos se sobrepõem, a mesma pessoa pode surgir em várias tabelas, exigindo coordenação da identidade e dos atributos comuns.
A escolha depende de consultas, restrições e da forma de especialização. Explica como garantes totalidade e disjunção quando são obrigatórias, e não escolhas a terceira estratégia sem verificar as restrições da especialização.
Numa relação N:N simples, uma tabela com PK composta (utilizador_id,sessao_id) permite no máximo uma ligação por par. Bilhete tem identidade própria: a mesma pessoa pode comprar vários para a mesma sessão, logo o par não é único neste exemplo. Não apliques automaticamente a PK composta de uma associação simples a uma entidade associativa com várias ocorrências por par.
A validação do esquema continua com dependências funcionais e normalização.
Notação compacta, schema e passagem a SQL
A5 documenta cada relação em notação compacta antes do DDL. Lê bilhetes(id, utilizador_id -> utilizadores NN, sessao_id -> sessoes NN, codigo UK NN, estado NN CK estado IN Estados): cada -> é uma chave estrangeira, UK é unicidade, NN é obrigatório, CK é verificação e Estados é o domínio dos três valores. Traduz cada marca para SQL: NN vira NOT NULL, UK vira UNIQUE, CK vira CHECK, -> vira REFERENCES.
Em PostgreSQL, uma base de dados contém schemas e cada schema contém tabelas. O search_path diz em que schema o servidor procura tabelas sem prefixo. No servidor da faculdade, o schema public é partilhado: cria o schema do grupo com CREATE SCHEMA lbawXXg e começa os scripts com SET search_path TO lbawXXg, para não criar tabelas no schema partilhado.
Uma referência a um par
Numa extensão da loja com lugares numerados, o lugar 90 pertence a S12 e o lugar 91 a S13. Um bilhete que guarda lugar_id=90 e sessao_id=13 mistura referências existentes, mas descreve uma combinação impossível. Duas FKs independentes só verificariam que 90 e 13 existem.
Uma chave estrangeira composta verifica que o par existe na mesma linha da relação referida:
CREATE TABLE lugares (
id INTEGER PRIMARY KEY,
sessao_id INTEGER NOT NULL REFERENCES sessoes(id),
numero TEXT NOT NULL,
UNIQUE (sessao_id, numero),
UNIQUE (id, sessao_id)
);
CREATE TABLE bilhetes_com_lugar (
id INTEGER PRIMARY KEY,
lugar_id INTEGER NOT NULL,
sessao_id INTEGER NOT NULL,
FOREIGN KEY (lugar_id, sessao_id) REFERENCES lugares(id, sessao_id)
);
UNIQUE(sessao_id, numero) impede dois lugares A1 na mesma sessão, mas permite A1 em sessões diferentes. UNIQUE(id, sessao_id) é redundante quanto à unicidade, pois id já é PK, mas fornece a chave declarada que esta FK composta referencia. O par (90,12) passa; (90,13) é recusado.
Os dois NOT NULL também fazem parte da regra. Com o comportamento predefinido MATCH SIMPLE, uma FK composta não exige correspondência quando algum componente é NULL. Para um bilhete que exige lugar e sessão, não deixes essa saída aberta. Se não precisas de guardar a sessão no bilhete, referir só o lugar e obter a sessão por junção evita duplicar essa informação. O exemplo mostra como garantir coerência quando ambos os campos têm de ficar guardados.
Esta regra cabe numa FK, por isso não precisa de um trigger que consulte os lugares. Usa primeiro as restrições declarativas que exprimem a invariante. A atribuição exclusiva de um lugar a bilhetes ativos é outra regra e exigiria uma restrição própria; este esquema só verifica a pertença do lugar à sessão.
Exercícios
Uma restrição que aceita desconhecido
Em PostgreSQL, CHECK(preco>=0) sem
NOT NULL impede preco=NULL?
NOT NULL é uma restrição independente.NULL não é um número.O CHECK não implica NOT NULL.Pista
Calcula o valor lógico de NULL>=0.
Mais uma pista
Desconhecido não é falso.
Solução
NULL>=0 é desconhecido. A linha passa no
CHECK; NOT NULL é necessário para
impedir a ausência.
- Não. O CHECK aceita desconhecido..
NOT NULLé uma restrição independente. - Sim,
NULLé negativo..NULLnão é um número. - Sim, todo o CHECK torna a coluna obrigatória.. O CHECK não implica
NOT NULL.
Erros frequentes
Não uses o comportamento do WHERE como regra do
CHECK.
O limite de uma chave estrangeira
evento_idNOT NULL REFERENCES eventos(id) garante que cada
evento tem uma sessão?
Pista
Tenta inserir só um evento.
Mais uma pista
A FK verifica as linhas da tabela que referencia.
Solução
O evento sem sessões não viola esta FK. O mínimo na direção inversa exige outra verificação ou um protocolo de criação.
- Não. Garante a referência de cada sessão.. Um evento pode existir sem nenhuma linha em sessoes.
- Sim, porque
NOT NULLimplica 1..*.. Aplica-se à coluna de cada sessão, não à existência de filhos. - Só se evento_id tiver índice.. Um índice não cria sessões.
Erros frequentes
Não traduzas uma associação só num sentido.
Saber que lado é apagado
bilhetes.sessao_id referencia sessoes.id
com ON DELETE CASCADE. Apagar um bilhete apaga a sessão?
Pista
Identifica a tabela referenciada.
Mais uma pista
A sessão é o pai nessa restrição.
Solução
Apagar um bilhete não afeta a sessão por esta FK. Apagar a sessão propaga para as linhas de bilhetes que a referem.
- Não. Apagar a sessão pode apagar os seus bilhetes.. A ação propaga do objeto referenciado para os que o referem.
- Sim, CASCADE apaga nos dois sentidos.. Isso não é a semântica da FK.
- Sim, se o bilhete for pago.. O estado não muda a direção da ação.
Erros frequentes
Não confundas eliminar uma referência com eliminar o alvo.
Traduzir uma regra para SQL
Cada cartaz pertence a uma sessão e cada sessão pode ter zero ou um cartaz. A tabela sessoes já tem a PK id. Escreve a declaração de cartazes com uma PK própria e as restrições necessárias. A verificação do SQL é manual.
Pista
O lado obrigatório deve guardar a referência.
Mais uma pista
Uma FK sem UNIQUE também permitiria vários cartazes por sessão.
Solução
CREATE TABLE cartazes (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sessao_id INTEGER NOT NULL UNIQUE REFERENCES sessoes(id)
);NOT NULL exige uma sessão para cada cartaz. A FK exige que essa sessão exista. UNIQUE limita a um cartaz por sessão. Nenhuma destas regras obriga a criar um cartaz para cada sessão.
Confere a tua resposta:
- Coloco sessao_id no cartaz e faço referência a sessoes(id).
- Uso
NOT NULLpara impedir um cartaz sem sessão. - Uso UNIQUE para impedir dois cartazes na mesma sessão.
- Permito criar uma sessão sem cartaz.
Erros frequentes
Uma FK opcional em sessoes pode representar a associação, mas por si só não impede cartazes sem sessão. Não acrescentes IDENTITY à FK: o seu valor refere uma chave já existente.
Traduzir notação compacta para DDL
Assume sessoes(id INTEGER PRIMARY KEY) no schema do grupo. Na notação compacta, id é a PK de bilhetes(id, sessao_id -> sessoes NN, codigo UK NN, estado NN CK estado IN Estados), com Estados = {reservado, pago, cancelado}. Escreve o DDL PostgreSQL, incluindo a seleção do schema. Usa lbawXXg apenas como nome ilustrativo.
Pista
Traduz cada marca separadamente e depois combina as restrições da coluna.
Solução
CREATE SCHEMA lbawXXg;
SET search_path TO lbawXXg;
-- A tabela sessoes é criada neste schema antes de bilhetes.
CREATE TABLE bilhetes (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sessao_id INTEGER NOT NULL REFERENCES sessoes(id),
codigo TEXT NOT NULL UNIQUE,
estado TEXT NOT NULL CHECK (estado IN ('reservado', 'pago', 'cancelado'))
);NN impede NULL, UK impede códigos repetidos e CK restringe o domínio. A FK refere uma sessão existente, por isso não recebe IDENTITY. A PK gera uma identidade própria para cada bilhete, permitindo vários bilhetes da mesma sessão.
Confere a tua resposta:
- Traduzo referência, NN, UK e CK.
- Declaro PK e não gero a FK.
- Coloco as tabelas no schema criado.
Verificar o autor e o grupo juntos
Uma aplicação tem utilizadores 7 e 8, grupos 2 e 3 e pertenças (utilizador_id, grupo_id) iguais a (7,2) e (8,3). A regra exige que a pertença do autor ao grupo continue registada enquanto houver posts desse autor nesse grupo. posts guarda id, autor_id e grupo_id. Duas FKs independentes para utilizadores e grupos recusam um post de 7 no grupo 3? Escreve uma restrição que garanta a combinação e diz o que acontece ao apagar (7,2) depois de existir um post de 7 no grupo 2.
Pista
A existência de cada componente não prova a existência da combinação.
Solução
As duas FKs aceitam 7 e 3 separadamente, embora (7,3) não seja uma pertença. Uma solução usa a pertença como relação referida:
CREATE TABLE membros (
utilizador_id INTEGER NOT NULL REFERENCES utilizadores(id),
grupo_id INTEGER NOT NULL REFERENCES grupos(id),
PRIMARY KEY (utilizador_id, grupo_id)
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
autor_id INTEGER NOT NULL,
grupo_id INTEGER NOT NULL,
FOREIGN KEY (autor_id, grupo_id)
REFERENCES membros(utilizador_id, grupo_id)
);A PK composta fornece a unicidade do par; a FK exige esse mesmo par, na mesma ordem, e os dois NOT NULL impedem escapar por um componente ausente. (7,3) é recusado; (7,2) passa.
Com a ação predefinida, apagar a pertença (7,2) é recusado enquanto o post a referir. Isso cumpre a regra dada. Se a regra real só exigir pertença no momento de publicar, esta restrição é mais forte: impediria sair do grupo conservando os posts. É preciso decidir o ciclo de vida, por exemplo conservar uma pertença histórica inativa e controlar a autorização de novas publicações, em vez de escolher CASCADE e apagar os posts sem justificação.
Confere a tua resposta:
- Mostro que duas referências independentes aceitam o par errado.
- Declaro PK composta da pertença e FK composta obrigatória no post.
- Identifico o bloqueio à eliminação da pertença referida.