Aprenda / SQL e banco de dados

IntermediárioAula 12 de 1616 min0% da trilha

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 informado

Enum 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.

opção 1 — enum de 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'
);
opção 2 — tabela de referência
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.

Quiz rápido

Por que vale a pena colocar uma CHECK constraint no banco (ex.: preco >= 0), mesmo já validando isso no backend?
Qual o principal trade-off de usar um enum de banco em vez de uma tabela de referência para um campo como status_pedido?