Início › Desafios › Desafio 04
Uma task por venda, mesmo com o trigger rodando duas vezes
Este é o bug que ninguém vê nascer. O trigger funciona, o teste passa, e semanas depois o time de pós-venda pergunta por que todo cliente novo tem duas tarefas de onboarding iguais. Ninguém mexeu no código: alguém criou um Flow que atualiza um campo da oportunidade, e isso faz o seu trigger rodar de novo na mesma transação. Duplicação em silêncio é o sintoma clássico de trigger sem controle de reexecução.
O cenário
Quando uma oportunidade é ganha, a operação quer uma tarefa de onboarding criada automaticamente para o dono do negócio. A primeira versão entregue foi esta, e ela tem três problemas graves:
trigger OpportunityTrigger on Opportunity (after update) {
for (Opportunity opp : Trigger.new) {
if (opp.StageName == 'Closed Won') {
// NAO faca isso: DML dentro do laco
insert new Task(
WhatId = opp.Id,
Subject = 'Iniciar onboarding do cliente',
ActivityDate = Date.today().addDays(2)
);
}
}
}
Primeiro: DML dentro do laço estoura o limite com lote grande. Segundo: a condição olha só o estágio atual, então qualquer edição numa oportunidade já ganha cria outra tarefa. Terceiro, o mais sorrateiro: quando um Flow ou workflow atualiza um campo da mesma oportunidade, o Salesforce executa o trigger de novo na mesma transação, e essa reexecução enxerga os valores antigos originais, como se a oportunidade tivesse acabado de ser ganha outra vez.
O que você deve construir
Um trigger fino que delega para uma classe handler chamada
OnboardingHandler, com um método público e estático, exatamente com esta
assinatura:
public static void processarGanhas(List<Opportunity> novas, Map<Id, Opportunity> antigas)
O método recebe Trigger.new e Trigger.oldMap e cria a tarefa de onboarding
para cada oportunidade que entrou em Closed Won nesta transação.
Regras de aceite
- Exatamente uma tarefa por oportunidade ganha, mesmo que o trigger execute mais de uma vez na mesma transação (reexecução por Flow ou workflow field update).
- Só cria quando o estágio MUDOU para ganho. Editar uma oportunidade que já estava
em
Closed Wonnão cria nada. A comparação usa o valor anterior, não só o atual. - Um único DML por execução, não importa se o lote tem 1 ou 200 oportunidades. Nenhum DML dentro de laço.
- O controle de reexecução é por registro, não por transação. Um update de 201
oportunidades roda o trigger em dois blocos na mesma transação; o seu controle não pode fazer o
segundo bloco ser ignorado. Um simples
Booleanestático reprova aqui. - Lista sem nenhuma oportunidade recém-ganha: nenhum DML executado e nenhuma exceção.
O que este desafio avalia
- Se você entende por que um trigger roda mais de uma vez na mesma transação, e o detalhe de que a reexecução enxerga os valores antigos originais.
- Se você compara com
Trigger.oldMapem vez de olhar só o estado atual. - Se o seu controle de reexecução sobrevive a lotes com mais de 200 registros.
- Se você coleta primeiro e faz um DML só no fim.
Travei. Me dá uma direção (sem entregar a resposta)
Três perguntas que destravam:
- O que uma variável
staticguarda durante uma transação no Apex, e o que acontece com ela quando a transação termina? - Se um
Booleanestático viratrueno primeiro bloco de 200 registros, o que acontece com os registros do segundo bloco da mesma transação? - Que estrutura guarda "quais registros eu já tratei" em vez de "eu já rodei"?
Termos de busca que ajudam: apex trigger fires twice workflow field update, apex trigger recursion static variable, trigger oldmap detect field change.
Como testar
Cole no Execute Anonymous. O teste força um lote de 201 (dois blocos de trigger na mesma transação), depois simula a reexecução do jeito que a automação faz: chamando o handler de novo com os valores antigos originais.
// 1) Lote de 201: forca DOIS blocos de trigger na mesma transacao
List<Opportunity> opps = new List<Opportunity>();
for (Integer i = 0; i < 201; i++) {
opps.add(new Opportunity(
Name = 'Desafio04-' + i,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30)
));
}
insert opps;
for (Opportunity o : opps) { o.StageName = 'Closed Won'; }
update opps; // dispara o trigger real (201 = blocos de 200 + 1)
// 2) Simula a reexecucao da automacao: os MESMOS registros, com os valores
// antigos ORIGINAIS (e assim que o Salesforce reexecuta apos field update)
Map<Id, Opportunity> antigasOriginais = new Map<Id, Opportunity>();
for (Opportunity o : opps) {
antigasOriginais.put(o.Id, new Opportunity(Id = o.Id, StageName = 'Prospecting'));
}
List<Opportunity> atuais = [SELECT Id, StageName, OwnerId FROM Opportunity WHERE Id IN :opps];
OnboardingHandler.processarGanhas(atuais, antigasOriginais);
// 3) Provas
Integer tarefas = [SELECT count() FROM Task
WHERE WhatId IN :opps AND Subject = 'Iniciar onboarding do cliente'];
System.assertEquals(201, tarefas, 'esperava exatamente UMA task por oportunidade ganha');
System.debug('OK: 201 oportunidades, 201 tarefas, zero duplicada.');
Resolveu? O que fica não é o Set de Ids: é o hábito de perguntar, para todo trigger que você escrever, "o que acontece se isto rodar duas vezes na mesma transação?". Em org com automação de verdade, a pergunta não é se vai reexecutar, é quando.