La guardia y el interruptor
Con la arquitectura ya ordenada, dos protecciones viven en un solo sitio: una guardia estática que impide hacer dos veces lo mismo en una transacción, y un interruptor con permiso personalizado para cargas de datos.
✓ En esta sub-lección
- 01Explicar por qué comparar old y new no basta cuando una regla de workflow vuelve a disparar el trigger.
- 02Poner la guardia static Set<Id> en el sitio donde se hace el trabajo: el servicio.
- 03Crear un interruptor con un permiso personalizado y FeatureManagement.checkPermission.
- 04Saber para quién es un bypass y para quién no.
La nutria: ¿Te acuerdas? · Repaso de la lección 3
El botón «Recalcular prioridad» aplica la regla del servicio a casos que ya existen. ¿Qué tiene que hacer después?
Soporte pidió una regla más: si un agente sube a mano la prioridad de un caso a 'High', su propietario también debe recibir la tarea de revisión. Se escribió con la defensa 2 del Módulo 6 (solo si la prioridad CAMBIÓ a 'High') y en la sandbox iba perfecto. En producción, los agentes empezaron a ver dos tareas por caso. Lo que avisó Operaciones al final de la tarea 3 era verdad.
Por qué la defensa 2 no basta aquí
La org heredada tiene una regla de workflow sobre Case que rellena un campo al guardar. En el Módulo 6 viste que, cuando una actualización de campo de workflow cambia el registro, los triggers de update se ejecutan una vez más. Lo que no viste es el detalle traicionero: en esa segunda pasada, Trigger.old NO trae la versión de justo antes del workflow, sino la de antes de la actualización ORIGINAL. Así que la comparación vuelve a ver 'Medium' → 'High', cree que la prioridad acaba de cambiar… y crea la tarea otra vez.
Un agente sube la prioridad de un caso de 'Medium' a 'High'. Nuestra regla crea una tarea de revisión. Pero la org heredada tiene una regla de workflow que, al guardar, rellena un campo del caso… y eso vuelve a disparar los triggers de update una vez más.
Elige los interruptores y pulsa «Guardar».
La nutria: ¿Todavía hay reglas de workflow?
La guardia, en su sitio
La defensa 3 del Módulo 6 es la que resuelve esto: un Set<Id> static que recuerda qué registros ya se procesaron en la transacción. Lo nuevo es DÓNDE vive. Con la arquitectura de este módulo hay un sitio obvio: el método del servicio que hace el trabajo. Así la protección se aplica venga de donde venga la llamada (el after insert, el after update o una segunda pasada por culpa del workflow) y nadie tiene que acordarse de añadirla en cada puerta.
public with sharing class CaseEscalationService {
// Casos que ya tienen su tarea en ESTA transacción
private static Set<Id> tasksCreatedFor = new Set<Id>();
public static void createReviewTasks(List<Case> cases) {
List<Task> tasks = new List<Task>();
for (Case c : cases) {
if (c.Priority == 'High' && !tasksCreatedFor.contains(c.Id)) {
tasksCreatedFor.add(c.Id);
tasks.add(new Task(WhatId = c.Id, OwnerId = c.OwnerId,
Subject = 'Revisar caso escalado'));
}
}
insert tasks;
}
}El interruptor: un bypass con permiso personalizado
La última petición de Soporte es la contraria: van a migrar 50.000 casos históricos de un sistema antiguo, y no quieren ni prioridades recalculadas ni miles de tareas en las colas. Hace falta un interruptor que apague las reglas… pero solo para quien hace la migración. La forma limpia es un (Custom Permission) que se asigna con un permission set solo al usuario de integración, y en el código una línea que lo comprueba: FeatureManagement.checkPermission devuelve true si el usuario que está ejecutando tiene ese permiso.
trigger CaseTrigger on Case (before insert, after insert, after update) {
// Interruptor: si quien guarda tiene el permiso, no se aplica ninguna regla
if (FeatureManagement.checkPermission('Bypass_Case_Triggers')) {
return;
}
CaseTriggerHandler handler = new CaseTriggerHandler();
switch on Trigger.operationType { /* … */ }
}La nutria: Es el mismo truco de tus reglas de validación
La nutria: ¿Y por qué no un Flow? Porque en Flow el interruptor va en cada flow