Constraints avançadas: enum vs. tabela de referência
"Já validei isso no backend" não é o mesmo que o banco garantir que um dado inválido nunca entra.
É tentador achar que, se o backend já valida "preço não pode ser negativo", o banco não precisa repetir essa regra. Mas o backend não é o único jeito de escrever numa tabela: um bug, uma migration rodada à mão, um script de importação, ou uma segunda aplicação acessando o mesmo banco podem inserir dado inválido sem passar pela validação nenhuma. O banco é a última linha de defesa — e é pra isso que servem constraints além de NOT NULL e FOREIGN KEY.
CHECK — regra de valor dentro da própria tabela
CREATE TABLE produtos (
id SERIAL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
preco DECIMAL NOT NULL CHECK (preco >= 0),
estoque INTEGER NOT NULL DEFAULT 0 CHECK (estoque >= 0)
);
INSERT INTO produtos (nome, preco) VALUES ('Teclado', -50.00);
-- Error: new row for relation "produtos" violates check constraint
-- (o banco recusa, mesmo que o backend tivesse deixado passar)UNIQUE composto — unicidade que só faz sentido combinando colunas
CREATE TABLE favoritos (
usuario_id INTEGER REFERENCES usuarios(id),
produto_id INTEGER REFERENCES produtos(id),
UNIQUE (usuario_id, produto_id)
-- o mesmo usuário não pode favoritar o mesmo produto duas vezes,
-- mas usuários diferentes podem favoritar o mesmo produto à vontade
);Um UNIQUE em usuario_id sozinho impediria um usuário de ter mais de um favorito, o que não é a regra desejada. A combinação das duas colunas é que precisa ser única — cada par (usuario_id, produto_id) só pode aparecer uma vez.
NOT NULL com DEFAULT — obrigatório, mas com um valor sensato se ninguém informar
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
usuario_id INTEGER REFERENCES usuarios(id),
status VARCHAR(20) NOT NULL DEFAULT 'pendente'
);
INSERT INTO pedidos (usuario_id) VALUES (1);
-- status vira 'pendente' automaticamente, sem precisar ser informadoEnum vs. tabela de referência — o mesmo problema, duas soluções
Quando um valor só pode ser um de um conjunto fixo (status de pedido, por exemplo), existem duas formas comuns de garantir isso no banco.
CREATE TYPE status_pedido AS ENUM ('pendente', 'pago', 'enviado', 'cancelado');
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
status status_pedido NOT NULL DEFAULT 'pendente'
);CREATE TABLE status_pedidos (
id SERIAL PRIMARY KEY,
nome VARCHAR(20) NOT NULL UNIQUE,
descricao TEXT
);
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
status_id INTEGER NOT NULL REFERENCES status_pedidos(id)
);Enum é mais compacto e simples de declarar, mas é rígido: adicionar um valor novo exige alterar o tipo (uma operação estrutural, não um INSERT), e não dá pra guardar metadata extra sobre cada valor — descrição, ordem de exibição, tradução. A tabela de referência resolve isso: adicionar um valor novo é um INSERT comum, e ela pode carregar qualquer coluna extra que fizer sentido. O custo é ter mais uma tabela pra manter e precisar de JOIN pra mostrar o nome do status.
Regra prática: valores que mudam raramente e não precisam de informação extra tendem bem com enum; valores que crescem com o negócio, ou que precisam de descrição/ordem/configuração, pedem tabela de referência.
Exercício
Crie uma tabela cupons (codigo, desconto_percentual) garantindo que o código nunca se repita e que o desconto esteja sempre entre 1 e 100.