Recursión y cómo evitarla
Un trigger que guarda registros de su propio objeto se vuelve a disparar a sí mismo. Por qué pasa, cómo reconocerlo y las tres defensas, de mejor a peor.
✓ En esta sub-lección
- 01Explicar qué provoca la recursión en un trigger y qué error produce.
- 02Evitarla de raíz eligiendo before y comprobando si el campo cambió.
- 03Escribir una guarda con un Set<Id> estático, y saber por qué un Boolean estático no basta.
La nutria: ¿Te acuerdas? · Repaso de la lección 4
¿Qué corre antes: tu trigger before o la regla de validación?
En la lección 3 viste un aviso: hacer update de los propios registros desde un trigger after es mala idea. Aquí ves por qué. Ese update es un guardado como cualquier otro, recorre el orden de ejecución entero y vuelve a disparar el mismo trigger. Que vuelve a hacer update. Que lo vuelve a disparar. Eso es la .
La nutria: El paralelo de Admin
Cómo se ve
trigger OpportunityReview on Opportunity (after update) {
List<Opportunity> toUpdate = new List<Opportunity>();
for (Opportunity o : Trigger.new) {
toUpdate.add(new Opportunity(Id = o.Id, Description = 'Revisada'));
}
update toUpdate; // ← guarda oportunidades… y dispara este trigger otra vez
}TRIGGERS ANIDADOS
nivel 1 / 16
- Nivel 1 · Ventas pasa Acme a 'Negotiation' → after update → update toUpdate
Salesforce no deja que el bucle siga para siempre: al llegar a 16 niveles de triggers anidados lanza «Maximum trigger depth exceeded» y la transacción entera se deshace. Antes de llegar ahí, cada vuelta ha gastado CPU, consultas y DML del mismo presupuesto del Módulo 4, así que a menudo el error que ves es otro governor limit. En los dos casos el usuario solo ve que no puede guardar.
Defensa 1: no provocarla
La mayoría de las recursiones nacen de cambiar el propio registro en after. Si lo que quieres es cambiar un campo del mismo registro, hazlo en before: se guarda solo, sin DML, y sin DML no hay nuevo guardado que dispare nada. El ejemplo de arriba, reescrito, desaparece como problema.
trigger OpportunityReview on Opportunity (before update) {
for (Opportunity o : Trigger.new) {
o.Description = 'Revisada'; // sin DML: no hay segunda vuelta
}
}Defensa 2: actuar solo si algo cambió
Hay casos en los que after es inevitable: por ejemplo, cuando una oportunidad cambia de etapa y tienes que actualizar la cuenta, y la cuenta a su vez tiene un trigger que toca sus oportunidades. Ahí la segunda defensa es la comparación de la lección 2: actúa solo si el campo que te importa cambió respecto a Trigger.oldMap. En la segunda vuelta el campo ya no cambia, y la lógica no se vuelve a ejecutar. Es lo que en Flow hace «Only when a record is updated to meet the condition requirements».
Defensa 3: la guarda estática
Cuando no puedes evitar el DML ni basta con comparar, apuntas qué registros ya procesaste en esta transacción. En el Módulo 5 viste que una variable static vive exactamente una transacción: nace con el primer uso y desaparece al terminar. Es el sitio perfecto para esa lista, en una pequeña clase auxiliar.
public class OpportunityTriggerGuard {
public static Set<Id> processed = new Set<Id>();
}
trigger OpportunityReview on Opportunity (after update) {
List<Opportunity> toUpdate = new List<Opportunity>();
for (Opportunity o : Trigger.new) {
if (OpportunityTriggerGuard.processed.contains(o.Id)) {
continue; // ya lo hice en esta transacción
}
OpportunityTriggerGuard.processed.add(o.Id);
toUpdate.add(new Opportunity(Id = o.Id, Description = 'Revisada'));
}
if (!toUpdate.isEmpty()) {
update toUpdate; // la segunda vuelta encuentra todos los Ids y no hace nada
}
}La nutria: Cómo se nota una recursión desde fuera
La nutria: ¿Y por qué no un Flow? Porque Flow no tiene memoria entre vueltas