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