Skip to main content

批量 Apex 触发器

学习目标

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

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

备注

用中文(简体)学习?在中文(简体)Trailhead Playground 中开始挑战,用括号中提供的译文完成挑战。仅复制并粘贴英文值,因为挑战验证基于英文数据。如果在中文(简体)组织中没有成功通过挑战,我们建议您 (1) 将区域设置切换为美国,(2) 按此处说明将语言切换为英文,(3) 再次单击“检查挑战”按钮。

查看 Trailhead 本地化语言徽章详细了解如何利用 Trailhead 译文。

批量触发器设计模式

Apex 触发器经过优化,可执行批量操作。我们建议使用批量设计模式来处理触发器中的记录。当您使用批量设计模式时,您的触发器具有更好的性能,消耗更少的服务器资源,并且大幅降低了超出平台限制的可能性。

批量化代码的优势体现在它可以有效地处理大量记录,并在 Lightning 平台的调控器限制内运行。调控器限制是为了确保失控代码不会垄断多租户平台上的资源。

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

在记录集上操作

我们先了解下触发器中最基本的批量设计概念。批量化触发器对触发器上下文中的所有 sObject 进行操作。通常,如果触发触发器的操作源自用户界面,则触发器对单个记录进行操作。但是,如果操作的来源是批量 DML 或 API,则触发器对记录集而不是单个记录进行操作。例如,当您通过 API 导入许多记录时,触发器会在整个记录集上运行。因此,一个良好的编程实践是始终假定触发器对一个记录集合进行操作,以便它在所有情况下都能工作。

下面的触发器 (MyTriggerNotBulk) 假定只有一条记录触发了触发器。当在同一事务中插入多条记录时,该触发器对完整记录集不起作用。下一个示例展示的是批量化触发器版本。

trigger MyTriggerNotBulk on Account(before insert) {
    Account a = Trigger.new[0];
    a.Description = 'New description';
}

此示例 (MyTriggerBulk) 是 MyTriggerNotBulk 的修改版。它使用 for 循环遍历所有可用的 sObject。如果 Trigger.new 包含一个 sObject 或多个 sObject,则此循环有效。

trigger MyTriggerBulk on Account(before insert) {
    for(Account a : Trigger.new) {
        a.Description = 'New description';
    }
}

执行批量 SOQL

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

下面的触发器 (SoqlTriggerNotBulk) 显示了不可取的 SOQL 查询模式。该示例在 for 循环内进行 SOQL 查询以获取每个客户关联的业务机会,该查询为 Trigger.new 中的每个客户 sObject 都运行一次。如果您有大量客户,for 循环内的 SOQL 查询可能会导致过多的 SOQL 查询。以下是推荐的 SOQL 查询方法示例。

trigger SoqlTriggerNotBulk on Account(after update) {
    for(Account a : Trigger.new) {
        // Get child records for each account
        // Inefficient SOQL query as it runs once for each account!
        Opportunity[] opps = [SELECT Id,Name,CloseDate
                             FROM Opportunity WHERE AccountId=:a.Id];
        // Do some other processing
    }
}

下面的示例 (SoqlTriggerBulk) 基于上一个示例进行修改,展示了运行 SOQL 查询的最佳实践。SOQL 查询完成了繁重的工作,并在主循环外调用了一次。

  • SOQL 查询使用内部查询 (SELECT Id FROM Opportunities) 以获取客户关联的业务机会。
  • SOQL 查询通过使用 IN 子句并绑定 WHERE 子句—WHERE Id IN :Trigger.new.中的 Trigger.new 变量,连接到触发器上下文记录。这一 WHERE 条件将客户筛选至只包含触发了此触发器的记录。

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

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

trigger SoqlTriggerBulk on Account(after update) {
    // Perform SOQL query once.
    // Get the accounts and their related opportunities.
    List<Account> acctsWithOpps =
        [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities)
         FROM Account WHERE Id IN :Trigger.new];
    // Iterate over the returned accounts
    for(Account a : acctsWithOpps) {
        Opportunity[] relatedOpps = a.Opportunities;
        // Do some other processing
    }
}

或者,如果您不需要客户父记录,您可以仅检索与此触发器上下文中的客户相关的业务机会。该列表在 WHERE 子句中通过将业务机会的 AccountId 字段与 Trigger.new 中的客户 ID 匹配来指定:WHERE AccountId IN :Trigger.new。返回的业务机会适用于此触发器上下文中的所有客户,而不是某一特定客户。下一个示例演示了用于获取所有相关业务机会的查询。

trigger SoqlTriggerBulk on Account(after update) {
    // Perform SOQL query once.
    // Get the related opportunities for the accounts in this trigger.
    List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.new];
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {
        // Do some other processing
    }
}

您可以通过将 SOQL 查询与 for 循环组合在一个语句的方式来减小上一个示例的大小:SOQL for 循环。以下是这一批量触发器使用 SOQL for 循环的另一个版本。

trigger SoqlTriggerBulk on Account(after update) {
    // Perform SOQL query once.
    // Get the related opportunities for the accounts in this trigger,
    // and iterate over those records.
    for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.new]) {
        // Do some other processing
    }
}





备注

触发器一次对 200 条记录执行批处理。因此,如果有 400 条记录触发了触发器,则触发器将触发两次,每 200 条记录触发一次。出于这个原因,您无法从触发器中获得 SOQL for 循环记录批处理的优势,因为触发器也会对记录进行批处理。在本例中,SOQL for 循环被调用两次,单独的 SOQL 查询也会被调用两次。但是,SOQL for 循环看起来仍然比遍历集合变量更简洁!

执行批量 DML

在触发器或类中执行 DML 调用时,尽可能在 sObject 集合上执行 DML 调用。在每个 sObject 上单独执行 DML 是低效使用资源的表现。Apex 运行时允许在一个事务中进行多达 150 次 DML 调用。

该触发器在遍历相关业务机会的 for 循环内 (DmlTriggerNotBulk) 执行更新调用。如果满足某些条件,触发器会更新业务机会描述信息。本示例中,每个业务机会的更新语句都被低效地调用一次。如果批量客户更新操作触发了触发器,则结果中会包含大量客户。如果每个客户都有一到两个业务机会,那么业务机会很快会超过 150 个。DML 语句限制为 150 次调用。

trigger DmlTriggerNotBulk on Account(after update) {
    // Get the related opportunities for the accounts in this trigger.
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.new];
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {
        // Update the description when probability is greater
        // than 50% but less than 100%
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            // Update once for each opportunity -- not efficient!
            update opp;
        }
    }
}

下一个示例 (DmlTriggerBulk) 演示了如何对一系列业务机会仅调用一次 DML,即可高效地批量执行 DML。该示例通过添加业务机会 sObject 来更新循环中的业务机会 (oppsToUpdate) 列表。接下来,将所有业务机会添加到列表之后,触发器在该列表的循环外部执行 DML 调用。无论更新的 sObject 数量大小,此模式都只需调用一次 DML。

trigger DmlTriggerBulk on Account(after update) {
    // Get the related opportunities for the accounts in this trigger.
    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) {
        // Update the description when probability is greater
        // than 50% but less than 100%
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            oppsToUpdate.add(opp);
        }
    }
    // Perform DML on a collection
    update oppsToUpdate;
}

让我们通过编写访问客户相关业务机会的触发器来应用您学到的设计模式。为 AddRelatedRecord 触发器修改上一单元中的触发器示例。AddRelatedRecord 触发器批量操作,但上一单元的触发器效率并不尽如人意,因为它遍历所有 Trigger.New sObject 记录。下一个示例修改触发器代码和 SOQL 查询,以便仅获取感兴趣的记录,然后遍历这些记录。如果您尚未创建触发器,请不要担心 — 您可以在本章节创建。

让我们先了解 AddRelatedRecord 触发器的相关要求。在插入或更新客户后会触发触发器。触发器为还没有业务机会的客户添加一个默认业务机会。 

首先,新插入的客户不会有默认的业务机会,所以我们肯定需要添加一个。但是对于更新的客户,我们需要确定他们是否有相关的业务机会。因此,让我们通过在 Trigger.operationType 上下文变量上使用 switch 语句来区分我们如何处理插入和更新。然后使用 toProcess 变量跟踪我们需要处理的客户。例如:

List<Account> toProcess = null;
switch on Trigger.operationType {
    when AFTER_INSERT {
        // do stuff
    }
    when AFTER_UPDATE {
        // do stuff
    }
}

对于所有客户插入,我们只需将新客户分配给 toProcess 列表:

when AFTER_INSERT {
     toProcess = Trigger.New;
}

对于更新,我们必须确定此触发器中的哪些现有客户还没有相关的业务机会。由于该触发器是 after 触发器,我们可以从数据库中查询受影响的记录。这是 SOQL 语句,我们将其结果分配给 toProcess 列表。

when AFTER_UPDATE {
     toProcess = [SELECT Id,Name FROM Account
                  WHERE Id IN :Trigger.New AND
                  Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)];
}

我们现在使用 for 循环遍历客户的 toProcess 列表,并将相关的默认业务机会添加到 oppList。完成后,我们使用 insert DML 语句批量添加业务机会列表。  以下是创建或更新完整触发器的方法。

  1. 如果您已经在上一单元中创建了 AddRelatedRecord 触发器,请将其内容替换为以下触发器,以达到修改触发器的目的。或者,使用 Developer Console 添加以下触发器,并输入 AddRelatedRecord 作为触发器名称。


    trigger AddRelatedRecord on Account(after insert, after update) {
        List<Opportunity> oppList = new List<Opportunity>();
        // Add an opportunity for each account if it doesn't already have one.
        // Iterate over accounts that are in this trigger but that don't have opportunities.
        List<Account> toProcess = null;
        switch on Trigger.operationType {
            when AFTER_INSERT {
            // All inserted Accounts will need the Opportunity, so there is no need to perform the query
                toProcess = Trigger.New;
            }
            when AFTER_UPDATE {
                toProcess = [SELECT Id,Name FROM Account
                             WHERE Id IN :Trigger.New AND
                             Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)];
            }
        }
        for (Account a : toProcess) {
            // Add a default opportunity for this account
            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. 在客户页面的业务机会相关列表中,找到名为 Lions & Cats 的新业务机会。触发器自动添加了该业务机会!

资源

在 Salesforce 帮助中分享 Trailhead 反馈

我们很想听听您使用 Trailhead 的经验——您现在可以随时从 Salesforce 帮助网站访问新的反馈表单。

了解更多 继续分享反馈