Um gatilho de banco de dados é um hook de código que roda automaticamente em eventos de dados — antes do save para validar, depois dele para reagir. A ideia tem duas linhagens que compartilham um princípio: o trigger SQL clássico, código procedural vivendo dentro do motor do banco, e o hook em nível de aplicação moderno — beforeSave, afterSave, beforeDelete — JavaScript registrado por classe no backend. Ambos codificam a mesma promessa: a regra dispara para toda gravação, não importa qual cliente, SDK ou script a executou — que é exatamente o que a validação no cliente nunca pode prometer.
Principais pontos
| Pergunta | Resposta |
|---|---|
| O princípio | Código preso a eventos de dados — automático, por tabela/classe, todo caminho de gravação |
| Before = | Validar · normalizar · aplicar defaults — ainda pode alterar ou abortar a gravação |
| After = | Reagir — contar, notificar, sincronizar; a gravação já aconteceu, então seja idempotente |
| As duas formas | Triggers SQL dentro do motor · hooks JS no processo do backend |
| Os bugs clássicos | Lógica escondida · loops infinitos de autodisparo · triggers lentos travando todo save |
O modelo de hooks em ação
Uma regra de validação e sua aplicação, vistas das duas pontas — o servidor a define uma vez, e todo cliente em todo lugar a herda:
// JavaScript — Cloud Code (cloud/main.js)
// beforeSave: validate + normalize — can still change or ABORT the write
Parse.Cloud.beforeSave('Review', (req) => {
const stars = req.object.get('stars');
if (stars < 1 || stars > 5) throw 'Stars must be between 1 and 5';
const comment = req.object.get('comment');
if (comment && comment.length > 500) {
req.object.set('comment', comment.slice(0, 497) + '…');
}
});
// afterSave: side effects — the write already happened; be idempotent
Parse.Cloud.afterSave('Review', async (req) => {
if (req.object.existed()) return; // count only NEW reviews, once
await updateAverageStars(req.object.get('movie'));
}); // Flutter / Dart — Back4app Flutter SDK
// The hook fires for EVERY write path — this client included
final review = ParseObject('Review')
..set('movie', 'Arrival')
..set('stars', 9); // invalid — no client-side check needed
final response = await review.save();
print(response.error?.message); // "Stars must be between 1 and 5"
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection. // iOS / Swift — Back4app Swift SDK
// The hook fires for EVERY write path — this client included
var review = Review()
review.movie = "Arrival"
review.stars = 9 // invalid — no client-side check needed
do {
_ = try await review.save()
} catch {
print(error.localizedDescription) // "Stars must be between 1 and 5"
}
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection. // Android / Kotlin — Back4app Android SDK
// The hook fires for EVERY write path — this client included
val review = ParseObject("Review")
review.put("movie", "Arrival")
review.put("stars", 9) // invalid — no client-side check needed
try {
review.save()
} catch (e: ParseException) {
println(e.message) // "Stars must be between 1 and 5"
}
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection. A família completa de hooks estende a mesma forma por todo o ciclo de vida dos dados: beforeSave/afterSave, beforeDelete (bloquear a exclusão de um Album que ainda tem Photos) e afterDelete (limpar os filhos dele), mais beforeFind/afterFind para reescrever queries e remover campos dos resultados — o CRUD, embrulhado.
A forma clássica: triggers SQL
CREATE TRIGGER audit_price_change
AFTER UPDATE ON products
FOR EACH ROW -- nível de linha: dispara por linha afetada
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION log_price_change(); -- OLD e NEW guardam as duas versões
A taxonomia que todo banco compartilha, segundo as referências do PostgreSQL e do MySQL: timing — BEFORE (pode modificar NEW ou abortar), AFTER (reage à linha confirmada), INSTEAD OF (substitui a operação, principalmente em views); evento — insert, update, delete; granularidade — nível de linha versus nível de instrução (um update de 10 linhas dispara um trigger de linha 10 vezes, um de instrução uma vez). Duas semânticas que valem gravar: triggers SQL rodam dentro da mesma transação da gravação — uma falha no trigger desfaz a operação inteira — e triggers AFTER só disparam para gravações que de fato tiveram sucesso.
Before vs. after: escolhidos pelo trabalho
| Before hooks | After hooks | |
|---|---|---|
| Pode alterar os dados | Sim — mutar, aplicar default, truncar | Não — já foi gravado |
| Pode abortar a gravação | Sim — lance um erro, e o save falha | Não |
| Certo para | Validação, normalização, campos computados | Contadores, notificações, sincronização, auditoria |
| Errado para | Efeitos colaterais — se o save falhar depois, o efeito já disparou | Qualquer coisa que deveria ter bloqueado a gravação |
| Comportamento na falha | Rejeita a operação, erro para o cliente | Muitas vezes fire-and-forget — erros caem nos logs |
| Disciplina | Rápido — ele bloqueia todo save | Idempotente — pode rodar de novo |
Os corolários que nenhum explicador enuncia: um efeito colateral em um before-hook é um bug por construção (o e-mail sai, e então o save falha), e um after-hook que não é idempotente é um contador duplicado esperando um retry. Em sistemas de hooks como o do Back4app, o afterSave completa depois que o cliente já recebeu a resposta — reações são assíncronas por design, então suas falhas precisam ser toleráveis e logadas.
Hooks vs. triggers SQL
| Hooks de aplicação (beforeSave/afterSave) | Triggers SQL | |
|---|---|---|
| Linguagem | JavaScript + o SDK e o ecossistema inteiros | SQL / dialetos procedurais de SQL |
| Chamadas externas | Sim — APIs, push, webhooks | Essencialmente não — e nem deveriam |
| Vive em | Seu codebase: versionado, testável, deployado | O schema: dentro do banco de dados |
| Transação | Before-hooks são o portão da gravação; after-hooks rodam pós-resposta | Mesma transação — a falha desfaz tudo |
| Vale para | Toda requisição que passa pela API do backend | Toda gravação na tabela, venha de onde vier |
| Ponto cego | Gravações diretas no banco passam por fora | Lógica invisível aos debuggers da aplicação |
A última linha é a simetria honesta: a garantia de cada forma está limitada à sua camada. Um trigger SQL pega até uma sessão psql renegada, mas esconde lógica do ferramental da aplicação; um hook em nível de API cobre todo caminho de cliente através do backend, mas não o acesso cru ao banco — e é por isso que plataformas donas do gateway de API (todo o tráfego flui por ele) ficam, na prática, com o melhor dos dois. A nota de rodapé sobre ORMs entra aqui também: hooks de ORM só disparam através do ORM — operações em massa e SQL cru passam reto por eles.
As armadilhas, com honestidade
Lógica escondida é o clássico: um desenvolvedor depura o próprio código por horas enquanto um trigger reescreve valores em silêncio — triggers são invisíveis no ponto de chamada, então documente-os e mantenha-os poucos. Loops infinitos: um trigger gravando na própria tabela dispara a si mesmo de novo (e um afterSave salvando o próprio objeto também); proteja-se checando o que mudou, e dê à recursão uma condição de parada antes que ela encontre uma por você. Custo síncrono: before-hooks moram dentro da latência de toda gravação — um hook de 200 ms deixa todo save 200 ms mais lento, e gravações em massa multiplicam os disparos por linha pela contagem de linhas. Cadeias em cascata: triggers que disparam triggers que disparam triggers transformam um insert em um projeto de arqueologia. A regra guarda-chuva de décadas de prática: triggers aplicam regras; no momento em que um começa a orquestrar um workflow, mova o workflow para funções ou jobs e deixe o trigger apenas enfileirar.
Casos de uso comuns
- Validação que todo cliente obedece — a regra de estrelas-entre-1-e-5, aplicada contra requisições forjadas e SDKs futuros igualmente.
- Trilhas de auditoria — quem mudou o quê, quando, escritas pelo próprio evento em vez de por clientes cooperativos.
- Contadores desnormalizados e campos computados — médias, contagens e duplicatas amigáveis à busca mantidas verdadeiras no momento da gravação.
- Integridade em cascata — recusar exclusões com filhos, ou limpar os filhos depois.
- Efeitos colaterais reativos — um
afterSavedisparando uma notificação push ou um webhook de saída: o evento de banco de dados virando evento de integração.
Qual hook onde? Matriz de decisão
| O trabalho | O hook |
|---|---|
| Rejeitar dados ruins | beforeSave — lance um erro |
| Trim, default, canonicalizar | beforeSave — mute o objeto |
| Atualizar um contador ou agregado | afterSave — com idempotência |
| Notificar uma pessoa ou sistema | afterSave → push / webhook / fila |
| Bloquear exclusões perigosas | beforeDelete |
| Limpar depois de exclusões | afterDelete |
| Aplicar escopo de query por usuário | beforeFind |
| Reação longa ou lenta | afterSave enfileira um job — nunca faz o trabalho inline |
Limitações e trade-offs
- Invisibilidade é o preço da automaticidade. Lógica que ninguém chama é lógica que ninguém lembra; nomes, docs e code review mantêm a mágica auditável.
- Hooks têm o escopo da sua camada. Hooks de API não veem gravações diretas no banco; triggers SQL não perdem nada, mas se escondem do seu ferramental — saiba qual ponto cego você escolheu.
- A latência de gravação é o orçamento. Todo before-hook gasta dele; meça hooks como você mede queries.
- After-hooks são eventualmente consistentes. Contadores atrasam milissegundos e podem disparar em dobro — projete as leituras (e os retries) de acordo.
- Triggers não substituem constraints. Índices únicos, chaves estrangeiras e ACLs aplicam mais barato e mais cedo; triggers começam onde as regras declarativas param.
Gatilhos de Banco de Dados no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Gatilhos aqui são a família de eventos de dados do Cloud Code: registre Parse.Cloud.beforeSave('Review', …) uma vez — as abas de código mostram o padrão inteiro — e a regra vale para toda gravação que chega por REST, GraphQL, qualquer SDK ou o dashboard, porque o gateway de API é o único caminho até os dados. Os hooks recebem contexto rico (request.object, request.original, request.user, status de master key), então a validação pode diferir para admins, o beforeFind pode aplicar escopo por usuário em cima das ACLs, e os after-hooks têm o ecossistema JavaScript inteiro para as reações que triggers SQL não alcançam — notificações push, webhooks de saída, enfileiramento de jobs. Deployado com o seu código, versionado no git, testável como qualquer função: a garantia do trigger, sem a arqueologia do trigger.
Perguntas frequentes
O que é um trigger de banco de dados?
É código procedural que executa automaticamente quando um evento de dados — insert, update, delete — ocorre em uma tabela ou classe específica. A forma clássica vive dentro do banco SQL; a forma moderna, em nível de aplicação, é uma função hook como beforeSave ou afterSave que o backend roda em volta de toda gravação.
Qual a diferença entre triggers BEFORE e AFTER?
Capacidade e timing. O BEFORE roda antes da gravação e pode validar, modificar os dados que chegam ou abortar a operação por completo. O AFTER roda depois que a gravação teve sucesso — não pode mudar o que aconteceu, só reagir: registrar, contar, notificar. O AFTER nunca dispara para gravações que falharam.
Qual a diferença entre um trigger e uma stored procedure?
A invocação. Uma stored procedure é chamada explicitamente, recebe parâmetros e retorna resultados. Um trigger nunca é chamado — ele dispara automaticamente quando seu evento ocorre, sem parâmetros, preso a uma tabela. A mesma maquinaria procedural, com o modelo de ativação oposto.
A lógica deve ficar em triggers ou no código da aplicação?
O consenso é um híbrido: triggers (ou hooks) para regras que precisam valer em todo caminho de gravação — validação, integridade, auditoria — e serviços da aplicação para workflows complexos. Hooks em nível de aplicação são o meio-termo moderno: as garantias do trigger, uma linguagem de verdade e controle de versão.
Triggers prejudicam a performance?
Eles rodam de forma síncrona dentro do caminho de gravação, então um trigger lento deixa todo save lento, e triggers em nível de linha se multiplicam em operações em massa — um update de cem mil linhas dispara cem mil vezes. Mantenha os before-hooks enxutos e empurre reações lentas para after-hooks ou jobs em segundo plano.
Um trigger pode causar um loop infinito?
Famosamente — um trigger que grava na própria tabela dispara a si mesmo de novo, e um afterSave que salva o objeto que acabou de tratar entra em recursão até algo quebrar. Proteja-se checando o que de fato mudou antes de gravar, e nunca salve de novo o objeto disparador dentro do próprio after-hook sem uma condição de parada.
Triggers podem chamar serviços externos?
Triggers SQL essencialmente não podem — nem deveriam — alcançar nada fora do banco. Essa é a vantagem de cartaz dos hooks em nível de aplicação: um afterSave em JavaScript pode enviar uma notificação push, chamar qualquer API ou disparar um webhook de saída, porque roda no processo do backend com o ecossistema inteiro disponível.
Para que serve o beforeSave?
Para os três trabalhos que exigem rodar antes da gravação: validação (lance um erro e o save falha, para qualquer cliente), normalização (trim, truncamento, canonicalização de campos) e defaults ou valores computados. Efeitos colaterais não pertencem ali — se o save falhar depois, o efeito já aconteceu.