🛡️ 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,CHECKeDEFAULT. - 🔹 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
IntegrityErrorno 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:
| Constraint | Função de Defesa | Exemplo Prático |
|---|---|---|
| NOT NULL | O campo é obrigatório. | Nenhum usuário sem Nome. |
| DEFAULT | Valor automático se ninguém enviar nada. | Cliente novo entra com status Ativo por padrão. |
| UNIQUE | Impede valor repetido (mas aceita NULL). | O E-mail da conta de acesso. |
| CHECK | Validações matemáticas e lógicas. | O frete deve ser sempre >= 0. |
| FOREIGN KEY | Garante 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.
- Crie a tabela de
funcionariocom um CHECK de salário nomeado comochk_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 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
🧪 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 valoresNULL(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
IntegrityErrorno 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.
Coloque este conhecimento em prática agora mesmo executando o roteiro autoguiado:
👉 ATIVIDADE 05: SQL DDL (ESTRUTURA)