🛡️ CAPÍTULO 12: RESTRIÇÕES DE INTEGRIDADE (CONSTRAINTS)


🎯 Objetivos de Aprendizagem

Ao final deste capítulo (estimativa: 2 horas de estudo autoguiado), você será capaz de:

  • 🔹 Dominar as 6 restrições fundamentais de integridade declarativa: PRIMARY KEY, FOREIGN KEY, NOT NULL, UNIQUE, CHECK e DEFAULT.
  • 🔹 Implementar regras de negócio complexas no nível do banco de dados utilizando restrições CHECK (preco > 0 AND status IN (...)).
  • 🔹 Prevenir corrupção de dados e injeção de dados inválidos antes que eles alcancem o armazenamento em disco.
  • 🔹 Configurar constraints no SQLAlchemy 2.0 e tratar exceções IntegrityError no backend Python.

As restrições (Constraints) são as muralhas do seu banco de dados. Elas garantem que os dados inseridos no banco sigam as regras de negócio rigorosas e mantenham a consistência da informação, impedindo que a aplicação salve um preço negativo ou um cliente sem CPF. 🛡️🧩

🏢 O Cenário Prático (Seu Desafio)

A equipe de auditoria da TecProExpress encontrou anomalias gravíssimas na base de testes: existiam produtos cadastrados com peso negativo, motoristas cadastrados com a mesma placa de CNH e registros vazios.

"Seu desafio é reescrever a DDL do sistema de transportes adicionando Constraints (Restrições) diretas no motor SQL. Se um programador tentar inserir um pacote com peso de -5 kg, o SGBD deve cuspir um erro de volta na cara da aplicação antes mesmo do dado chegar no disco!"


🧠 Fundamentos: O Arsenal de Defesa (Constraints)

Você já usou a restrição máxima (PRIMARY KEY), mas o DDL nos fornece ferramentas mais granulares:

ConstraintFunção de DefesaExemplo Prático
NOT NULLO campo é obrigatório.Nenhum usuário sem Nome.
DEFAULTValor automático se ninguém enviar nada.Cliente novo entra com status Ativo por padrão.
UNIQUEImpede valor repetido (mas aceita NULL).O E-mail da conta de acesso.
CHECKValidações matemáticas e lógicas.O frete deve ser sempre >= 0.
FOREIGN KEYGarante integridade referencial.Pacote só existe se o cliente existir.

🛡️ Pipeline de Validação de Dados na Inserção (SGBD)

flowchart LR
    INSERT["📥 INSERT / UPDATE<br>(Novo Registro)"] --> C1{"1. NOT NULL / DEFAULT<br>Campos presentes?"}
    C1 -- Não --> E1["❌ ERRO: Null value in column"]
    C1 -- Sim --> C2{"2. CHECK<br>Regras lógicas válidas?"}
    C2 -- Não --> E2["❌ ERRO: Check constraint failed"]
    C2 -- Sim --> C3{"3. UNIQUE / PK<br>Chave duplicada?"}
    C3 -- Sim --> E3["❌ ERRO: Duplicate key value"]
    C3 -- Não --> C4{"4. FOREIGN KEY<br>Pai existe?"}
    C4 -- Não --> E4["❌ ERRO: Foreign key violation"]
    C4 -- Sim --> SUCESSO["✅ PERSISTÊNCIA OK<br>Gravado no Disco (WAL)"]

    style INSERT fill:#e3f2fd,stroke:#1976d2
    style SUCESSO fill:#e8f5e9,stroke:#2e7d32
    style E1 fill:#ffebee,stroke:#c62828
    style E2 fill:#ffebee,stroke:#c62828
    style E3 fill:#ffebee,stroke:#c62828
    style E4 fill:#ffebee,stroke:#c62828

📖 Exemplo Guiado: O DDL Blindado (Regras de Negócio)

Vamos criar a tabela de "Frete" da TecProExpress com uma armadura pesada de validações.

🛠️ Código do Exemplo

-- PASSO 1: DDL (Criando a Tabela Blindada)
CREATE TABLE frete_blindado (
    id INT PRIMARY KEY,
    codigo_rastreio CHAR(10) UNIQUE NOT NULL, -- Obrigatório e Exclusivo
    peso_kg DECIMAL(5,2) CHECK (peso_kg > 0), -- A regra de ouro da auditoria
    data_registro DATE DEFAULT CURRENT_DATE,  -- Se omitido, pega o dia de hoje
    status VARCHAR(20) DEFAULT 'PENDENTE'
);

-- PASSO 2: DML (Carga de Sucesso)
-- Omitimos a data e o status para ver o DEFAULT agir!
INSERT INTO frete_blindado (id, codigo_rastreio, peso_kg) 
VALUES (1, 'AB12345678', 15.50);

-- PASSO 3: DML (A Carga que VAI FALHAR - Teste o CHECK)
-- Descomente e rode. O banco VAI recusar a inserção por causa do peso negativo.
-- INSERT INTO frete_blindado (id, codigo_rastreio, peso_kg) VALUES (2, 'XY99999999', -2.00);

🔍 Detalhamento do Código:

  • DEFAULT CURRENT_DATE: Uma função nativa. Se a aplicação de frontend esquecer de mandar a data, o próprio banco preenche.
  • CHECK (peso_kg > 0): A parede intransponível. A aplicação Node.js ou Python vai receber um erro "Constraint Violation" caso tente registrar peso negativo. O banco de dados nunca confia no Frontend!

🔗 Ações Referenciais em Chaves Estrangeiras (FK)

A Chave Estrangeira não serve apenas para "vincular". Ela define o que acontece quando alguém apaga o dado PAI.

  • RESTRICT (Padrão): O SGBD proíbe a deleção do Cliente se ele tiver Pacotes no sistema.
  • CASCADE: Se você deletar o Cliente, o SGBD deleta todos os Pacotes dele junto. (Use com extrema sabedoria).
  • SET NULL: Deleta o Cliente e deixa os Pacotes órfãos (com ID NULL). Útil se você não quer apagar o histórico de transporte, mas o cliente cancelou a conta.

📊 Fluxo da Chave Estrangeira

flowchart TD
    DEL["❌ Ação: DELETE FROM Cliente WHERE id=10"] --> SGBD{"⚙️ Motor de FK"}
    SGBD -- "RESTRICT" --> B1["⛔ ERRO: Ação Bloqueada"]
    SGBD -- "CASCADE" --> B2["🔥 Deleção em Massa (Cliente e Pacotes)"]
    SGBD -- "SET NULL" --> B3["🧹 Apaga o Cliente. Atualiza Pacotes para NULL"]

🛠️ Prática Obrigatória: Nomeando as Muralhas

Cenário: Facilitando o debug do desenvolvedor. Se você não der um nome para o seu CHECK, o banco cria um nome feio (ex: SYS_C0015). Veja como nomear sua restrição para o log de erro ficar legível.

  1. Crie a tabela de funcionario com um CHECK de salário nomeado como chk_salario_minimo.

🚀 Script de Seed (Gabarito de Nomenclatura)

-- DDL
CREATE TABLE funcionario (
    id INT PRIMARY KEY,
    nome VARCHAR(100),
    salario DECIMAL(10,2),
    
    -- O 'CONSTRAINT' antes da regra permite batizá-la!
    CONSTRAINT chk_salario_minimo CHECK (salario >= 1412.00)
);

-- DML (Essa linha falha e o erro vai explicitar o nome 'chk_salario_minimo')
-- INSERT INTO funcionario VALUES (1, 'João', 1000.00);


💻 Ponte Prática: Do SQL Manual ao SQLAlchemy 2.0 ORM

Como o SQLAlchemy 2.0 materializa as Constraints (Muralhas de Integridade) em código Python corporativo?

🔴 1. A Abordagem Manual (Erros de Validação Silenciosos)

Sem restrições no banco, erros do frontend (como um peso negativo digitado no formulário) são gravados diretamente no disco:

# ❌ ABORDAGEM SEM CONSTRAINTS: Dados corrompidos entram no banco sem barreira
cursor.execute("INSERT INTO fretes (codigo, peso) VALUES ('AB123', -50.0);") # ⚠️ Aceito silenciosamente!

🟢 2. A Abordagem com SQLAlchemy 2.0 (Defense in Depth com Constraints Nomeadas)

Com o SQLAlchemy 2.0, definimos CheckConstraint, UniqueConstraint e valores default com nomes explícitos para auditoria:

# ✅ ABORDAGEM MODERNA COM SQLALCHEMY 2.0: Restrições de Integridade Nomeadas
from datetime import date
from sqlalchemy import create_engine, String, Float, Integer, Date, CheckConstraint, select, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.exc import IntegrityError

class Base(DeclarativeBase):
    pass

class FreteBlindadoModel(Base):
    """Tabela de fretes com restrições rigorosas de domínio."""
    __tablename__ = "fretes_blindados"
    __table_args__ = (
        CheckConstraint("peso_kg > 0.0", name="chk_peso_estritamente_positivo"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    codigo_rastreio: Mapped[str] = mapped_column(String(10), unique=True, nullable=False)
    peso_kg: Mapped[float] = mapped_column(Float, nullable=False)
    data_registro: Mapped[date] = mapped_column(Date, server_default=func.current_date())
    status: Mapped[str] = mapped_column(String(20), default="PENDENTE")

    def __repr__(self) -> str:
        return f"Frete(cod='{self.codigo_rastreio}', peso={self.peso_kg}kg, status='{self.status}')"

🛠️ Mini-Projeto 12 (BD): Repositório Auto-Defensivo e Constraints

Objetivo: Implementar um repositório auto-defensivo que captura violações de constraints (IntegrityError) e impede corrupção de dados.

📋 Pré-requisitos e Instalação

No terminal do seu ambiente virtual (PowerShell ou Bash), instale a biblioteca necessária:

pip install sqlalchemy

💻 Código Completo e Autocontido (miniprojeto_12_constraints.py)

Crie o arquivo miniprojeto_12_constraints.py e insira o código abaixo integralmente:

"""
Mini-Projeto 12: Repositório Auto-Defensivo e Constraints
Curso: GTI - Banco de Dados Relacionais e Engenharia de Software
Stack: Python 3.11+ | SQLAlchemy 2.0 | SQLite
"""
import os
from datetime import date
from sqlalchemy import create_engine, String, Float, Integer, Date, CheckConstraint, select, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlalchemy.exc import IntegrityError

# 1. Definição Declarativa com Defesas em Profundidade (Constraints)
class Base(DeclarativeBase):
    pass

class FreteBlindadoModel(Base):
    """Tabela de fretes com restrições rigorosas de domínio."""
    __tablename__ = "fretes_blindados"
    __table_args__ = (
        CheckConstraint("peso_kg > 0.0", name="chk_peso_estritamente_positivo"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    codigo_rastreio: Mapped[str] = mapped_column(String(10), unique=True, nullable=False)
    peso_kg: Mapped[float] = mapped_column(Float, nullable=False)
    data_registro: Mapped[date] = mapped_column(Date, server_default=func.current_date())
    status: Mapped[str] = mapped_column(String(20), default="PENDENTE")

    def __repr__(self) -> str:
        return f"Frete(cod='{self.codigo_rastreio}', peso={self.peso_kg}kg, status='{self.status}')"

# 2. Ponto de Entrada Executável
if __name__ == "__main__":
    DB_FILE = "tecpro_constraints.db"

    # Reset preventivo: indispensável para garantir idempotência frente ao UNIQUE de codigo_rastreio
    if os.path.exists(DB_FILE):
        os.remove(DB_FILE)

    engine = create_engine(f"sqlite:///{DB_FILE}", echo=False)
    Base.metadata.create_all(bind=engine)

    print("🔒 REPOSITÓRIO AUTO-DEFENSIVO COM CONSTRAINTS - TECPROEXPRESS")
    print("=" * 65)

    # 1. Inserção com Sucesso (Regra Válida)
    with Session(engine) as session:
        f1 = FreteBlindadoModel(codigo_rastreio="BR99887766", peso_kg=12.4)
        session.add(f1)
        session.commit()
        print(f"✅ Inserção Válida: {f1}")

    # 2. Teste da Muralha 1: Tentativa de Inserir Peso Negativo (CHECK Falha)
    with Session(engine) as session:
        try:
            print("\n⚠️ Tentando inserir frete com peso negativo (-10.0kg)...")
            f_invalido = FreteBlindadoModel(codigo_rastreio="BR00000001", peso_kg=-10.0)
            session.add(f_invalido)
            session.commit()
        except IntegrityError:
            session.rollback()
            print("🛡️ SGBD Bloqueou com Sucesso: Violação de 'chk_peso_estritamente_positivo'!")

    # 3. Teste da Muralha 2: Tentativa de Inserir Código Duplicado (UNIQUE Falha)
    with Session(engine) as session:
        try:
            print("\n⚠️ Tentando duplicar o código de rastreio 'BR99887766'...")
            f_duplicado = FreteBlindadoModel(codigo_rastreio="BR99887766", peso_kg=5.0)
            session.add(f_duplicado)
            session.commit()
        except IntegrityError:
            session.rollback()
            print("🛡️ SGBD Bloqueou com Sucesso: Violação de Chave Única (UNIQUE Constraint)!")

    print("=" * 65)

🚀 Como Executar

Execute o script no terminal:

python miniprojeto_12_constraints.py

🖥️ Saída Esperada no Console

🔒 REPOSITÓRIO AUTO-DEFENSIVO COM CONSTRAINTS - TECPROEXPRESS
=================================================================
✅ Inserção Válida: Frete(cod='BR99887766', peso=12.4kg, status='PENDENTE')

⚠️ Tentando inserir frete com peso negativo (-10.0kg)...
🛡️ SGBD Bloqueou com Sucesso: Violação de 'chk_peso_estritamente_positivo'!

⚠️ Tentando duplicar o código de rastreio 'BR99887766'...
🛡️ SGBD Bloqueou com Sucesso: Violação de Chave Única (UNIQUE Constraint)!
=================================================================

💡 Checkpoint de Lógica

Dica

Dica do Especialista: Uma arquitetura de dados sênior deve sempre possuir uma "Defesa em Profundidade" (Defense in Depth). Não é porque a linguagem de programação no frontend tem um "IF" bloqueando pesos negativos que o banco de dados deve aceitar qualquer coisa. A Constraint é o goleiro do seu time: se a defesa do código falhar, o goleiro do banco espalma a bola! 🚀🛡️



🧪 Quiz de Fixação e Autoavaliação — Capítulo 12

1. Qual restrição de integridade (Constraint) deve ser usada para garantir que a coluna preco_unitario nunca receba valores negativos ou zero no banco de dados?

  • A) NOT NULL
  • B) UNIQUE
  • C) CHECK (preco_unitario > 0)
  • D) DEFAULT '0'
💡 Ver Resposta e Justificativa

Resposta Correta: C
Justificativa: A constraint CHECK avalia uma expressão lógica booleana a cada INSERT/UPDATE, bloqueando a transação se o resultado for falso.


2. Qual a diferença entre uma coluna definida como PRIMARY KEY e uma coluna definida como UNIQUE?

  • A) A PRIMARY KEY é única e NUNCA aceita valores NULL; a coluna UNIQUE garante valores não-repetidos, mas pode aceitar valores NULL (conforme o SGBD).
  • B) UNIQUE só aceita números e PRIMARY KEY só aceita letras.
  • C) PRIMARY KEY pode ter valores repetidos.
  • D) UNIQUE apaga os dados antigos automaticamente.
💡 Ver Resposta e Justificativa

Resposta Correta: A
Justificativa: Uma tabela só pode ter uma única Chave Primária (sempre NOT NULL). Mas pode ter múltiplas restrições UNIQUE (ex: CPF único, Email único, Matrícula única).


3. O que acontece quando o backend tenta inserir uma linha que viola uma constraint de integridade no banco de dados?

  • A) O banco desliga o servidor.
  • B) O SGBD aborta a transação imediatamente e retorna um erro de violação de integridade (gerando um IntegrityError no SQLAlchemy), impedindo a gravação do dado corrompido.
  • C) O banco aceita o dado e avisa no dia seguinte.
  • D) O dado é salvo em formato de texto.
💡 Ver Resposta e Justificativa

Resposta Correta: B
Justificativa: Constraints são a última e mais forte linha de defesa da arquitetura: se a validação da aplicação falhar, o SGBD rejeita o dado inválido no ato.


🎯 Laboratório Prático

Coloque este conhecimento em prática agora mesmo executando o roteiro autoguiado:
👉 ATIVIDADE 05: SQL DDL (ESTRUTURA)