Salesforce Triggers(2)

学习目标

完成本单元后,您将能够:

  • 编写对sObject集合进行操作的触发器。
  • 编写执行高效SOQL和DML操作的触发器。

批量触发器设计模式

Apex触发器被优化以批量操作。我们建议使用批量设计模式处理触发器中的记录。当您使用批量设计模式时,您的触发器具有更好的性能,消耗更少的服务器资源,并且不太可能超出平台限制。
批量代码的好处是,批量代码可以高效地处理大量记录,并在Force.com平台的管理限制内运行。这些州长限制已经到位,以确保失控代码不会垄断多租户平台上的资源。

以下部分将演示在触发器中扩展Apex代码的主要方法:对触发器中的所有记录进行操作,并对sObject集合执行SOQL和DML,而不是同时对单个sObjects执行SOQL和DML。 SOQL和DML批量最佳实践适用于任何Apex代码,包括类中的SOQL和DML。给出的例子是基于触发器,并使用Trigger.New上下文变量。

在记录集上运行

我们先来看触发器中最基本的批量设计概念。批量触发器在触发器上下文中的所有sObjects上运行。通常情况下,如果触发触发器的操作来自用户界面,触发器将在一条记录上运行。但是,如果操作的起源是批量DML或API,则触发器将在记录集上运行,而不是一个记录。例如,当您通过API导入多条记录时,触发器将在整个记录集上进行操作。因此,一个良好的编程习惯就是始终假设触发器在一系列记录上运行,以便在任何情况下都能正常工作。

以下触发器假设只有一条记录导致触发器触发。在同一事务中插入多个记录时,此触发器不能在完整的记录集上工作。下一个示例中显示了一个批量版本。

trigger MyTriggerNotBulk on Account(before insert) {
    Account a = Trigger.New[0];
    a.Description = 'New description';
}
这个例子是MyTrigger的修改版本。它使用for循环遍历所有可用的sObjects。如果Trigger.New包含一个sObject或多个sObjects,则此循环有效。
trigger MyTriggerBulk on Account(before insert) {
    for(Account a : Trigger.New) {
        a.Description = 'New description';
    }
}
执行批量SOQL

SOQL查询可以是强大的。您可以检索相关记录,并在一个查询中检查多个条件的组合。通过使用SOQL功能,您可以编写更少的代码并减少对数据库的查询。减少数据库查询有助于避免遇到查询限制,即同步Apex的100个SOQL查询或异步Apex的200个查询限制。

以下触发器显示要避免的SOQL查询模式。这个例子在for循环中创建一个SOQL查询,以获取每个客户的相关机会,每个客户在Trigger.New中为每个Account sObject运行一次。如果您拥有大量客户,for循环中的SOQL查询可能导致太多的SOQL查询。下一个例子显示了推荐的方法。

trigger SoqlTriggerNotBulk on Account(after update) {   
    for(Account a : Trigger.New) {
        // 获取每个客户的子记录
        // 对于每个客户运行一次的SOQL查询效率低下!
        Opportunity[] opps = [SELECT Id,Name,CloseDate 
                             FROM Opportunity WHERE AccountId=:a.Id];
        
        // 做一些其他处理
    }
}
此示例是前一个示例的修改版本,并显示了运行SOQL查询的最佳实践。 SOQL查询完成繁重的工作,并在主循环之外调用一次。
  • SOQL查询使用内部查询(SELECT Id FROM Opportunities)来获取相关的客户机会。
  • SOQL查询通过使用IN子句并绑定WHERE子句中的Trigger.New变量(WHERE Id IN:Trigger.New)连接到触发器上下文记录。这个WHERE条件将客户过滤为仅触发此触发器的记录。

将查询中的两部分结合起来,就可以在一次调用中得到我们想要的记录:这个客户将触发每个客户的相关机会。

获取记录及其相关记录之后,for循环通过使用集合变量(本例中为acctsWithOpps)来迭代感兴趣的记录。 collection变量保存SOQL查询的结果。这样,for循环只能遍历我们想要操作的记录。由于相关的记录已经被获得,所以在循环内不需要进一步的查询来获得这些记录。

trigger SoqlTriggerBulk on Account(after update) {  
    // 执行一次SOQL查询。
    // 获取客户及其相关机会。
    List<Account> acctsWithOpps = 
        [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities) 
         FROM Account WHERE Id IN :Trigger.New];
  
    // 遍历返回的客户 
    for(Account a : acctsWithOpps) { 
        Opportunity[] relatedOpps = a.Opportunities;  
        // 做一些其他处理
    }
}
或者,如果您不需要客户父记录,则只能检索与此触发器上下文中的客户相关的商机。该列表在WHERE子句中通过将机会的AccountId字段与Trigger.New中的客户ID进行匹配来指定:WHERE AccountId IN:Trigger.New。返回的机会适用于此触发器上下文中的所有客户,而不是特定客户。下一个示例显示用于获取所有相关机会的查询。
trigger SoqlTriggerBulk on Account(after update) {  
    // 执行一次SOQL查询。
    // 获取这个触发器中客户的相关机会。
    List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.New];
  
    // 遍历相关的机会 
    for(Opportunity opp : relatedOpps) { 
        // 做一些其他处理
    }
}
您可以通过在一个语句中将SOQL查询与for循环组合来缩小前面的示例:SOQL for循环。这是使用SOQL for循环的这个批量触发器的另一个版本。
trigger SoqlTriggerBulk on Account(after update) {  
    // 执行一次SOQL查询。  
    // 在此触发器中获取客户的相关机会,
    // 并遍历这些记录。
    for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.New]) {
  
        // 做一些其他处理
    }
}
超越基础

触发器一次执行批次的200条记录。所以如果有400条记录触发一个触发器,那么触发器会触发两次,每个记录有一次。因为这个原因,在触发器中你没有得到SOQL的循环记录批处理的好处,因为触发器也记录了批处理记录。在这个例子中SOQL for循环被调用了两次,但是一个独立的SOQL查询也被调用了两次。但是,SOQL for循环看起来比迭代集合变量更优雅!

执行批量DML

在触发器或类中执行DML调用时,尽可能在一组sObject上执行DML调用。在每个sObject上执行DML单独使用资源效率低下。 Apex运行时允许在一个事务中多达150个DML调用。

该触发器在for循环中执行更新调用,该循环遍历相关的机会。如果满足某些条件,则触发器更新机会描述。在这个例子中,更新语句对于每个机会被低效地调用一次。如果批量客户更新操作触发了该触发器,则可能有多个客户。如果每个客户有一两个机会,我们可以很容易地结束150个机会。 DML语句限制是150个调用。

trigger DmlTriggerNotBulk on Account(after update) {   
    // 获取这个触发器中客户的相关机会。
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.New];          

    // 遍历相关的机会
    for(Opportunity opp : relatedOpps) {      
        // 当概率大于50%但小于100%时更新描述
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            // 为每个机会更新一次 - 效率不高!
            update opp;
        }
    }    
}
下面的例子展示了如何在一个机会列表上只用一个DML调用来高效地执行DML。该示例将添加Opportunity sObject以更新为循环中的机会列表(oppsToUpdate)。接下来,触发器在所有机会添加到列表之后,在此列表的循环外执行DML调用。无论正在更新的sObject数量如何,此模式只使用一个DML调用。
trigger DmlTriggerBulk on Account(after update) {   
    // 获取这个触发器中客户的相关机会。 
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.New];
          
    List<Opportunity> oppsToUpdate = new List<Opportunity>();

    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {      
        // 当概率大于50%但小于100%时更新描述
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            oppsToUpdate.add(opp);
        }
    }
    
    // 在集合上执行DML
    update oppsToUpdate;
}

批量设计模式的实际操作:获取相关记录的触发器示例

让我们通过编写一个访问客户相关机会的触发器来应用您所学习的设计模式。修改AddRelatedRecord触发器的前一单元的触发器示例。 AddRelatedRecord触发器批量操作,但效率不如它可能是因为它遍历所有Trigger.New sObject记录。下一个示例修改SOQL查询以仅获取感兴趣的记录,然后遍历这些记录。如果你还没有创建这个触发器,不要担心,你可以在本节中创建它。

让我们从AddRelatedRecord触发器的要求开始。客户被插入或更新后触发器触发。触发器为每个没有机会的客户添加一个默认机会。要解决的第一个问题是弄清楚如何获得孩子的机会记录。因为这个触发器是一个after触发器,我们可以从数据库中查询受影响的记录。他们已经被触发后触发的时间。让我们编写一个SOQL查询,返回这个触发器中没有相关机会的所有账号。

[SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND
                                             Id NOT IN (SELECT AccountId FROM Opportunity)]
现在我们已经获得了我们感兴趣的记录子集,让我们通过使用SOQL for循环遍历这些记录,如下所示。
for(Account a : [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND
                                         Id NOT IN (SELECT AccountId FROM Opportunity)]){
}
你现在已经看到了我们触发器的基础知识。唯一缺失的部分是创建默认的机会,我们将要进行批量处理。这是完整的触发器。
  1. 如果您已经在上一个单元中创建了AddRelatedRecord触发器,请通过用以下触发器替换其内容来修改触发器。否则,请使用开发者控制台添加以下触发器,并为触发器名称输入AddRelatedRecord。
    trigger AddRelatedRecord on Account(after insert, after update) {
        List<Opportunity> oppList = new List<Opportunity>();
        
        //为每个客户添加一个机会,如果它还没有。
        // 遍历此触发器中没有机会的客户。
        for (Account a : [SELECT Id,Name FROM Account
                         WHERE Id IN :Trigger.New AND
                         Id NOT IN (SELECT AccountId FROM Opportunity)]) {
            // 为此客户添加一个默认机会
            oppList.add(new Opportunity(Name=a.Name + ' Opportunity',
                                       StageName='Prospecting',
                                       CloseDate=System.today().addMonths(1),
                                       AccountId=a.Id)); 
        }
        
        if (oppList.size() > 0) {
            insert oppList;
        }
    }
  2. 要测试触发器,请在Salesforce用户界面中创建一个客户,并将其命名为“Lions&Cats”。
  3. 在客户页面的机会相关列表中,找到新的机会狮子会&猫。触发器自动添加了机会!