Módulo 6 · Engenharia de Software · ES06 · Aula 5 de 10
Transactions & Triggers
Concorrência no Banco
Por que o último ingresso vende duas vezes — ACID, isolamento, locks e gatilhos automáticos
⚠️ Race condition real
🔐 ACID
📊 4 níveis de isolamento
🔄 BEGIN / COMMIT / ROLLBACK
🪤 Triggers e suas armadilhas
DAILY · 15 MIN
15:00
⚠️ 1 caso público de race condition
❓ 1 dúvida sobre isolamento
🛡️ Como meu projeto trata concorrência hoje
🤝 Preciso de ajuda?
Cada aluno em 1 minuto · começamos em ordem alfabética
Autoestudos · Pré-aula 📚
3 tarefas individuais para chegar pronto + leituras sugeridas
📖
Tarefa 1 · Leitura
Material da Aula 5 + Kleppmann cap. 7
Leia o material e o capítulo 7 (Transactions) de Designing Data-Intensive Applications. Vocabulário do dia: ACID, isolation levels, locks, triggers.
📖 Material da aula
🔍
Tarefa 2 · Pesquisa
1 caso público de race condition
Pesquise um bug real causado por concorrência (overdraw bancário, double-spending, oversell de ingressos). Anote no caderno.
📓 Caderno de bordo
🧪
Tarefa 3 · Setup
Cliente SQL com 2 sessões
Garanta que seu DBeaver/Azure Data Studio abre 2 sessões simultâneas no banco do projeto — vai precisar para o teste de race.
Abrir docs MSFT ↗

📚 Leituras sugeridas

📌 Material de chegada: caderno de bordo com 1 caso público de race + 1 dúvida sobre isolamento + cliente SQL pronto para 2 sessões. Esses 3 itens alimentam o daily de abertura.

Agenda da Aula
2 horas · teoria + live coding + atividade prática em sala (sem ponderada)
⏱️ 40 min
TEORIA + LIVE CODING

⚠️ Race condition · ACID · Isolamento · Locks · Triggers

Bloco curto e denso de fundamentos: por que transações existem, ACID, 4 níveis de isolamento, BEGIN/COMMIT/ROLLBACK, locks pessimista vs otimista, anatomia de triggers.

  • Cenário concreto do último ingresso
  • Live coding da transação canônica
  • Q&A em cada bloco
⏱️ 65 min + dev
ATIVIDADE EM GRUPO · SEM PONDERADA

🛠️ Atividade prática em sala

Em grupo, no repositório do projeto: sp_comprar_ingresso + trg_avaliacao_audit + teste manual de race condition. Continua na janela de dev (16h–18h).

  • Stored procedure atômica de compra
  • Trigger de auditoria em avaliacao
  • 2 sessões SQL competindo pelo último ingresso
  • Merge Request no repo do grupo

🎯 Saída do dia: Merge Request feat(db): transactions + triggers de auditoria aberto no repositório do grupo, com README.md documentando o teste de race condition (com lock vs sem lock). Sem nota ponderada — entrega coletiva avaliada pelo professor.

A condição de corrida ⚠️
2 usuários, 1 ingresso disponível, sistema vende 2 — entenda o porquê
tempo
T1 · Alice
T2 · Bruno
t = 0ms
SELECT qtd FROM estoque
WHERE id = 42

→ retorna 1
t = 5ms
SELECT qtd FROM estoque
WHERE id = 42

→ retorna 1 🟡
t = 10ms
"Tem estoque, vou comprar"
UPDATE estoque
SET qtd = 0
t = 12ms
INSERT INTO ingresso ...
✅ Alice tem ingresso
t = 15ms
"Tem estoque, vou comprar"
UPDATE estoque
SET qtd = 0
(já era 0)
t = 17ms
INSERT INTO ingresso ...
💥 Bruno também tem!

🔍 Onde mora o bug?

Entre o SELECT e o UPDATE da T1, a T2 leu o mesmo valor desatualizado. O banco não sabia que essas duas operações eram um único negócio.

💡 A solução

Empacotar o par SELECT-then-UPDATE numa transação atômica com lock. Esse é o tema da aula. Vamos da teoria (ACID) ao código (TRY/CATCH).

ACID · 4 promessas que toda transação faz 🔐
O contrato que diferencia "rodei um SQL" de "fiz uma transação"
A
Atomicity
Tudo ou nada. Se uma operação da transação falha, todas as outras revertem.
💥 Violação: estoque decrementou mas INSERT ingresso falhou e ficou assim.
C
Consistency
A transação leva o BD de um estado válido para outro estado válido (FKs, CHECKs, regras).
💥 Violação: qtd ficou negativo, ou ingresso aponta para usuário inexistente.
I
Isolation
Transações concorrentes não veem o estado intermediário uma da outra.
💥 Violação: T2 leu o estoque que T1 tinha mexido mas ainda não tinha confirmado.
D
Durability
Depois do COMMIT, o dado sobrevive a queda de energia, crash, restart.
💥 Violação: servidor reinicia e ingresso comprado some — usuário pagou e perdeu.

📌 Foco do dia: os 3 primeiros — A, C e I — são onde você como dev escreve código. Durability é responsabilidade do SGBD (WAL, fsync). Hoje, mire em A + C + I.

4 níveis de isolamento × 4 anomalias 📊
Nível mais alto = mais consistência, menos throughput · passe o mouse nas anomalias para ver detalhes

💀 Dirty Read

Leio dado que outra tx mexeu mas não comitou ainda.

🔁 Non-repeatable Read

Leio o mesmo registro 2x na mesma tx, valor mudou.

👻 Phantom Read

Mesmo WHERE, 2x, surgem novos registros.

🌀 Serialization Anomaly

Resultado é impossível em qualquer ordem serial real.

* PostgreSQL bloqueia phantom em REPEATABLE READ via MVCC; SQL Server não.

BEGIN / COMMIT / ROLLBACK na prática 🔄
Compra de ingresso atômica com TRY/CATCH e checagem de @@ROWCOUNT
Locks · Pessimista vs Otimista 🔒
Duas filosofias para tratar a mesma race condition — qual escolher?
PESSIMISTA
🔐

"Bloqueia primeiro, pergunta depois"

Trava o registro quando lê. Outras transações esperam.

  • ✅ Garantia forte de consistência
  • ⚠️ Reduz throughput — outras tx esperam
  • ⚠️ Risco de deadlock se locks forem em ordem inconsistente
  • 🎯 Use quando: estoque escasso, financeiro, conflitos frequentes
OTIMISTA
🎲

"Tenta primeiro, valida depois"

Não trava. Confere no UPDATE que ninguém mexeu antes via versão ou timestamp.

  • ✅ Throughput alto — sem espera
  • ⚠️ Precisa de retry no app quando perde a corrida
  • ⚠️ Falha em alta contenção (todo mundo perde)
  • 🎯 Use quando: leituras >> escritas, conflitos raros (perfis, configurações)

📌 Heurística rápida: ingresso, saldo, estoque crítico → pessimista. Comentário, like, perfil de usuário → otimista com versão. Nunca use otimista quando o custo de "perdeu a corrida" é dinheiro real.

Sandbox de Locks 🔒
Laboratório prático: SQLite rodando no navegador (Abra a mesma tela em duas abas)
Triggers · Anatomia de um gatilho 🪝
Código que o banco executa automaticamente em resposta a INSERT/UPDATE/DELETE

🎯 Para que serve

  • Auditoria — gravar quem mexeu em quê e quando
  • Validação cross-row — regra que CHECK não cobre
  • Manter agregados — soma, contagem, denormalizações
  • Replicar mudanças para tabela "shadow"

📐 Tipos · Quando dispara

  • BEFORE — antes da operação (PG)
  • AFTER — depois da operação (padrão)
  • INSTEAD OF — substitui a operação (em views)
  • Em: INSERT, UPDATE, DELETE (ou combinações)

inserted

Tabela virtual com as linhas novas de INSERT/UPDATE.

deleted

Tabela virtual com as linhas antigas de UPDATE/DELETE.

🪄 Em UPDATE: ambas

UPDATE = DELETE + INSERT logicamente — você tem o "antes" e o "depois".

Triggers · Boas práticas e armadilhas 🪤
Por que times maduros desconfiam deles — e quando NÃO usar

✅ Boas práticas

  • Curto e idempotente — não chame I/O externo nem loops pesados
  • Use SET NOCOUNT ON — evita confundir o app com counts extras
  • Pense em batchinserted/deleted podem ter N linhas, nunca assuma 1
  • Documente dentro do trigger — quem lê depois agradece
  • Versione no repositório — trigger é código, não configuração mágica
  • Logue próprias falhas em tabela de erro — debug fica viável

⚠️ Armadilhas clássicas

  • Cascata invisível — trigger A dispara B dispara C (recursão indireta)
  • Performance — trigger lento bloqueia o INSERT do app, não roda em background
  • Debugging difícil — bug aparece como "INSERT estranho" sem stack trace
  • Lógica de negócio escondida — devs novos não sabem que ele existe
  • Testes complicados — não dá para mockar o BD trivialmente
  • Acoplamento — migrar de SGBD vira projeto inteiro

🚫 Quando NÃO usar trigger · alternativas modernas

  • Lógica de negócio complexa → coloque na application layer, não no banco
  • Notificação assíncrona → use change data capture (Debezium) ou change feed (Cosmos, MongoDB)
  • Histórico completo de eventos → considere event sourcing (a aplicação grava eventos, estado é projeção)
  • Integração com outros sistemas → publique mensagem em fila no app, não em trigger

📌 Regra prática: trigger é ótimo para auditoria simples e invariantes do banco. Para lógica de negócio e integrações, é dívida técnica esperando para acontecer.

Do banco até a tela · checkout 💳
Como uma transação SQL propaga camada a camada até virar estado visual no app mobile
Pipeline · de baixo para cima
🗄️
Banco · SQL Server
sp_comprar_ingresso
BEGIN · lock · INSERT · COMMIT / ROLLBACK
🔌
API · ASP.NET
POST /comprar
200 OK · 409 sold_out · 500 erro
🗂️
Model · Repository
CheckoutRepo.comprar()
HTTP cru → union type TS tipado
🧠
ViewModel · custom hook
useCheckout()
5 estados: idle · loading · success · sold_out · error
📱
View · React Native
CheckoutScreen.tsx
spinner · modal · toast · alert
3 cenários · mesmo pipeline
⛔ Sem estoque · 409 Conflict
Procedure: RAISE EXCEPTION 'sold_out' → API: 409 → Repo: { ok:false, reason:'sold_out' } → ViewModel vira sold_out → View: modal "ingresso esgotado".
⚠️ Deadlock · 500 Internal Error
Lock pessimista detectou conflito → ROLLBACK automático → API: 500 → Repo: 'deadlock' → ViewModel: error → View: "tente novamente".
✅ Compra confirmada · 200 OK
Procedure devolve ingresso_id → API: 200 + JSON → Repo: { ok:true, ingressoId } → ViewModel: success → View: toast verde + navegação.

📌 A grande sacada: a procedure não decide nada visual — ela só fala o que aconteceu via códigos. Cada camada acima traduz esse "o quê" para o vocabulário dela. View nunca toca SQL. Procedure nunca toca pixel.

Repository · erros SQL → tipos do app 🔌
A tradução começa aqui: HTTP cru entra, union type tipado sai · a View vai consumir esse tipo

📌 Olhe a divisão de trabalho: Repository só fala HTTP. Sabe que 409 = "sold_out". Não sabe quem é o usuário, não sabe que tela está aberta. Domínio puro. É o que torna esse código testável sem precisar montar tela nem banco.

ViewModel · useCheckout hook 🧠
useReducer com 5 estados · expõe comprar() para a View · mapeia cada reason num estado nominado

📌 Por que reducer e não useState: o estado tem 5 formas mutuamente exclusivas. Booleanos soltos (isLoading, hasError, isSoldOut) permitem combinações impossíveis ("loading + sold_out"). Reducer + union type tornam estados ilegais impossíveis de existir.

View · CheckoutScreen.tsx 📱
JSX puro · pattern matching no state.kind · cada estado vira um elemento visual

idle

Botão "Comprar" habilitado. Nada mais visível. Estado inicial e pós-reset.

loading

Botão "Comprando..." + ActivityIndicator. Desabilita o tap pra evitar double-click (compra duplicada no banco).

success

Toast verde com ingressoId. Você pode também navegar para a tela "Meus ingressos" aqui.

sold_out

Modal bloqueante. UX honesta: o último ingresso era exatamente esse. reset volta a idle.

⚠️ error

Alert com mensagem técnica. Deadlock entra aqui — sugere "tente novamente" (o lock pessimista resolve no retry).

📌 O ciclo: toca → loading → resposta → reducer escolhe 1 dos 4 estados terminais → JSX re-renderiza. Tudo isso nasceu do contrato da sua sp_comprar_ingresso.

Atividade Prática · Em sala 🛠️
Tela mobile (React Native + Expo) + Supabase + trigger de log · em grupo · 1h05 + dev (16h–18h)
01
📱

Tela mobile simples

App em React Native + Expo com UMA tela: input para escrever uma avaliação, botão "Enviar" e lista das últimas 5 avaliações cadastradas.

Sem login. Estilo livre. Foco no fluxo: digitar → salvar → ver na lista.

02
🗄️

Banco no Supabase

Criar projeto no Supabase, tabela avaliacao(id, texto, criado_em) e tabela log_avaliacao(id, op, payload, criado_em).

App usa @supabase/supabase-js com URL + anon key.

03
🪝

Trigger de log

trg_avaliacao_log em AFTER INSERT na tabela avaliacao que grava automaticamente uma linha em log_avaliacao.

App nunca escreve no log; quem escreve é o trigger.

04
▶️

Emular com Expo

Rodar npx expo start, abrir no celular via Expo Go (QR code) ou no emulador Android/iOS — demonstrar tela funcionando.

⭐ Excelência: exibir também a lista de logs lida diretamente da tabela log_avaliacao.

📌 Fluxo de entrega (em grupo):

  1. Branch feat/mobile-supabase-trigger no repositório do grupo
  2. Pasta app/ com o projeto Expo (npx create-expo-app) e db/ com schema.sql + trg_avaliacao_log.sql
  3. README.md com print da tela rodando no Expo Go e print da tabela log_avaliacao populada pelo trigger
  4. Abrir Merge Request feat(mobile): tela de avaliacao + supabase + trigger de log

⚠️ Importante: sem nota ponderada. A entrega coletiva é avaliada qualitativamente — vale como débito técnico zero para a próxima aula (que aprofunda mobile, MVVM e integração com a API).

🛠️
Mão na massa · Atividade em grupo
65 minutos em sala + janela de dev (16h–18h) para entregar os 4 itens. Cada equipe trabalha no próprio repositório.
1️⃣ Criar branch feat/db-transactions-triggers
2️⃣ Modelar tabelas log_auditoria e log_avaliacao_audit
3️⃣ Escrever sp_comprar_ingresso com lock pessimista
4️⃣ Escrever trg_avaliacao_audit com inserted/deleted
5️⃣ Abrir 2 sessões e testar race condition
6️⃣ Documentar no README · sem lock vs com lock
7️⃣ ⭐ Excelência: SERIALIZABLE + análise throughput
8️⃣ Abrir Merge Request no repo do grupo
💡 Estratégia: 3 alunos do grupo em paralelo (1 procedure, 1 trigger, 1 testes). Quem terminar primeiro ataca a Excelência ⭐.
🎯
A aula 5 me ensinou que...
"Funciona localmente" não é prova de correção. Concorrência é um teste por si só. Na aula 6 subimos para o frontend mobile com React Native + MVVM — o consumidor das procedures e triggers que você acabou de blindar.
✅ Race condition reconhecível
✅ ACID com exemplos de violação
✅ 4 níveis de isolamento × 4 anomalias
BEGIN/COMMIT/ROLLBACK em TRY/CATCH
✅ Lock pessimista vs otimista
✅ Trigger com tabelas mágicas
✅ Quando NÃO usar trigger
✅ MR aberto no repo do grupo
✅ Ponte para mobile · aula 6
Módulo 6 · Engenharia de Software · Aula 5 de 10 · 22/05/2026