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.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

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 (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 ligação vira tabela própria com duas chaves estrangeiras. A tabela bilhetes guarda utilizador_id e sessao_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:

Esquema e compra de exemplo
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;
Resultado

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 estrangeiraConsequência
NO ACTIONRecusa uma violação quando a restrição é verificada; pode ser adiada se declarada deferrable.
RESTRICTImpede a operação de imediato, sem a possibilidade equivalente de adiamento.
CASCADEPropaga a eliminação ou alteração às linhas que referenciam.
SET NULLRemove a referência; exige que as colunas e outras restrições permitam NULL.
SET DEFAULTAplica 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?

A tua resposta

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.. NULL nã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.

Voltar à explicação
O limite de uma chave estrangeira
evento_idNOT NULL

REFERENCES eventos(id) garante que cada evento tem uma sessão?

A tua resposta

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 NULL implica 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.

Voltar à explicação
Saber que lado é apagado

bilhetes.sessao_id referencia sessoes.id com ON DELETE CASCADE. Apagar um bilhete apaga a sessão?

A tua resposta

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.

Voltar à explicação
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 NULL para 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.

Voltar à explicação
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.
Voltar à explicação
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.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.