Apex Trigger 中的递归调用现象

Posted by Peter Dong on December 15, 2022

递归 Trigger 对 Salesforce 开发人员来说是个大问题. 我们总是希望在 Salesforce 中避免递归调用 Trigger. 但是, 如果我们有一个庞大的功能复杂的Org, 其中有多个带有父子关联的记录更新的触发器, 包括 Process builder 和 Workflow field updates等. 那么我们就会很容易面临递归调用 Trigger 的问题.

因此, 今天我将分享一些方法, 使用这些方法我们可以轻松地避免递归触发器.

如何避免在 Trigger 中发生递归调用呢?

那么有几个不同的方法来解决触发器中的递归问题:

  • 使用静态的 Boolean 变量控制
  • 使用静态的 Set 集合去存储 Record Id.
  • 使用静态 Map
  • 使用 Old Map
  • 遵循 Apex Trigger 最佳实践

让我们详细讨论以上解决方案和它们的局限性.

使用静态的 Boolean 变量

你可以创建一个带有静态布尔变量的参数,其默认值为true. 在触发器中, 在执行你的代码之前 ,要检查该变量是否为真. 代码执行完成, 就把这个变量设置为false, 但是这个方案有个问题: 记录数量较少(<200条记录)的话方案没问题. 但是如果我们有大量的记录, 它其实就无法工作. 我们看如下代码:

Apex Class:

1
2
3
public class ContactTriggerHandler{
     public static Boolean firstRun = true;
}

Trigger 代码:

1
2
3
4
5
6
7
8
Trigger ContactTriggers on Contact (after update){
    Set<String> accIdSet = new Set<String>(); 
    if(ContactTriggerHandler.firstRun){
        ContactTriggerHandler.isFirstTime = false;
        System.debug('---- Trigger run ----> ' + Trigger.New.size() );
       // any code here
    }
}

在我的 ORG 中, 有超过200条联系人, 让我们查询所有联系人记录并尝试更新所有数据. 在这种情况下, 触发器应该执行2次. 但是由于静态变量的原因, 触发器只执行一次, 并且会跳过剩余的记录.

1
2
3
4
List<Contact> contLst =[SELECT ID, FirstName FROM Contact LIMIT 400];
update contLst;

System.debug('contLst size: ' + contLst.size());

结果打印:

|DEBUG|---- Trigger run ----> 200
|DEBUG|contLst size: 360

查看 Salesforce 文档, 了解 Apex Trigger 的最佳实践, 以避免在 Apex 类中使用静态布尔变量的递归. 静态布尔变量是一种 anti-pattern, 所以请尽量不要使用.

使用静态 Set

因此, 最好使用一个静态 Set 或 Map 来存储所有已执行的记录ID. 当下次再执行时, 我们可以检查记录是否已经执行.

1
2
3
public class ContactTriggerHandler{
     public static Set<Id> setExecutedRecord = new Set<Id>();
}
1
2
3
4
5
6
7
8
9
10
trigger ContactTriggers on Contact (after update){
    System.debug('---- Trigger run ----> ' + Trigger.New.size() );
    for(Contact conObj : Trigger.New){
        if(!ContactTriggerHandler.setExecutedRecord.contains(conObj.id)){  
            // logic here
            // ...           
            ContactTriggerHandler.setExecutedRecord.add(conObj.Id);
        }    
    }
}

结果打印:

|DEBUG|---- Trigger run ----> 200
|DEBUG|---- Trigger run ----> 160
|DEBUG|contLst size: 360

其实使用静态 Set 看起来也不是很理想, 因为有很多时候, 我们可能会在同一个事务中同时触发 insert 和 update(特别是混合使用 Apex 和 Flow 逻辑的情况下).

使用静态 Map

我们可以使用静态 Map 来存储 Trigger 事件名称和 一组 Ids, 如下所示.

1
public static Map<String,Set<Id>> mapExecutedRecord = new Map<String,Set<Id>>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ContactTriggerHandler {
    //on before update
    public static void beforeUpdate(map<ID, Contact> newMap, map<ID, Contact> oldMap) {
        List<Contact> conList = new List<Contact>();
        if(!ContactTriggerHandler.mapExecutedRecord.containsKey('beforeUpdate'))
            ContactTriggerHandler.mapExecutedRecord.put('beforeUpdate', new Set<ID>());
        for (Contact con: newMap.values()) {
            if(!ContactTriggerHandler.mapExecutedRecord.get('beforeUpdate').contains(con.Id)){
                conList.add(con);
                ContactTriggerHandler.mapExecutedRecord.get('beforeUpdate').add(con.Id);
            }
        }
    }
}

使用 Old Map

在执行 Trigger 逻辑之前, 总是使用 Old map 来比较值的变化. 下面是我们如何比较旧值以解决触发器中的递归问题的例子.

1
2
3
4
5
for(Opportunity opp: Trigger.New){
	if(opp.Stage != Trigger.oldMap.get(opp.Id).Stage){
		// logic here
	}
}

你的代码只会在值改变时执行.

总结

最后我们在使用这些方法的同时, 还应该遵循以下最佳实践.

  • 一个对象对应唯一一个Trigger: 这样的话, 我们不必考虑 Trigger 的执行顺序. 由于无法控制哪个 Trigger 会先被执行.
  • Trigger 里的逻辑尽量少: 使用 Trigger 框架, 然后在 Handler 里处理 具体的业务逻辑
  • 处理递归 Trigger: 为了避免触发器的递归, 确保你的触发器只被执行一次. 如果递归处理得不好, 你可能会遇到错误: “Maximum trigger depth exceeded’”.

Buy Me a Coffee