Salesforce Apex (基础2)sObjects

学习目标

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

  • 描述sObjects和Salesforce记录之间的关系。
  • 创建并使用特定的sObject变量。
  • 将一个通用的sObject转换为一个特定的sObject。

使用sObjects

由于Apex与数据库紧密集成,因此您可以直接从Apex访问Salesforce记录及其字段。 Salesforce中的每个记录都本地表示为Apex中的sObject。例如,Acme客户记录对应于Apex中的Account sObject。您可以在用户界面中查看和修改的Acme记录的字段也可以在sObject上直接读取和修改。
下表列出了Acme客户示例记录的一些填充字段。 Account sObject是客户记录的抽象,并将客户字段信息作为对象保存在内存中。
表1.检索到的记录的AccountSObject
客户字段
Id 001D000000JlfXe
公司名称 Acme
电话 (415)555-1212
员工人数 100

每个Salesforce记录在插入Salesforce之前都被表示为一个sObject。同样,从Salesforce检索持久性记录时,它们存储在sObject变量中。

Salesforce中的标准和自定义对象记录将映射到Apex中的sObject类型。以下是Apex中用于标准对象的一些常用的sObject类型名称。

  • 客户
  • 联系
  • 潜在客户
  • 机会

如果您在组织中添加了自定义对象,请使用Apex中自定义对象的API名称。例如,名为Merchandise的自定义对象对应于Apex中的Merchandise__c sObject。

创建sObject变量

要创建一个sObject,你需要声明一个变量并将其分配给一个sObject实例。变量的数据类型是sObject类型。

以下示例创建一个类型为Account的sObject变量,并将其分配给名称为Acme的新客户。

Account acct = new Account(Name='Acme');

sObject和字段名称

sObjects的名称对应于相应的标准或自定义对象的API名称。同样,sObject字段的名称对应于相应字段的API名称。

对象和字段的API名称可能与标签不同。例如,“雇员”字段具有“雇员”标签,并在“客户记录”页面上显示为“雇员”,但其API名称为“NumberOfEmployees”。要在Apex中访问此字段,您需要使用该字段的API名称:NumberOfEmployees。

以下是用于自定义对象和自定义字段的API名称的一些规则的亮点。

对于自定义对象和自定义字段,API名称始终以__c后缀结尾。对于自定义关系字段,API名称以__r后缀结尾。例如:

  • 具有Merchandise 标签的自定义对象具有Merchandise__c的API名称。
  • 描述标签的自定义字段的API名称为Description__c。
  • 具有Items标签的自定义关系字段具有Items__r的API名称。

另外,标签中的空格在API名称中用下划线代替。例如,Employee Seniority的自定义字段名称具有Employee_Seniority__c的API名称。

查找对象和字段名称

要查找在Apex中使用的标准对象及其字段的名称,请参阅Salesforce和Force.com的对象参考。

对于自定义对象,请在您的组织中查找对象和字段API名称。从“设置”中,在“快速查找”框中输入对象,然后选择“对象”,然后单击对象的名称。

创建sObjects和添加字段

在插入Salesforce记录之前,必须首先在内存中创建它作为sObject。与其他任何对象一样,sObject使用新的运算符创建:

Account acct = new Account();

API对象名称成为Apex中sObject变量的数据类型。在这个例子中,Account是acct变量的数据类型。

由acct变量引用的客户是空的,因为我们还没有填充它的任何字段。有两种方法可以添加字段:通过构造函数或使用点符号。

添加字段的最快方法是在构造函数中将它们指定为名称 – 值对。例如,这个语句创建一个新的客户sObject,并用字符串值填充它的Name字段。

Account acct = new Account(Name='Acme');
名称字段是客户唯一必填字段,这意味着必须在插入新记录之前填充该字段。但是,您也可以填充其他字段以及新记录。这个例子还增加了一个电话号码和雇员的数量。
Account acct = new Account(Name='Acme', Phone='(415)555-1212', NumberOfEmployees=100);
或者,您可以使用点符号将字段添加到sObject。以下示例与前面的示例相同,但需要多行代码。
Account acct = new Account();
acct.Name = 'Acme';
acct.Phone = '(415)555-1212';
acct.NumberOfEmployees = 100;

使用通用的sObject数据类型

通常,在使用sObjects时,使用特定的sObject数据类型(例如标准对象的Account或Book__c)作为名为Book的自定义对象。但是,当您不知道您的方法正在处理的sObject类型时,可以使用通用的sObject数据类型。
使用通用sObject数据类型声明的变量可以引用任何Salesforce记录,无论它是标准还是自定义对象记录。
A generic sObject variable can point to any Salesforce record

此示例显示如何将通用的sObject变量分配给任何Salesforce对象:客户和名为Book__c的自定义对象。

sObject sobj1 = new Account(Name='Trailhead');
sObject sobj2 = new Book__c(Name='Workbook 1');
相比之下,使用特定的sObject数据类型声明的变量只能引用相同类型的Salesforce记录。
A specific sObject variable can point to the Salesforce record of the same type only

将泛型对象转换为特定的对象类型

当你处理通用的sObjects时,你有时需要将你的sObject变量转换为特定的sObject类型。这样做的好处之一就是能够使用点符号访问字段,这在通用的sObject上是不可用的。由于sObject是所有特定sObject类型的父类型,因此可以将通用sObject转换为特定的sObject。这个例子展示了如何将一个通用的sObject转换为Account。

// 将一个通用的sObject转换为一个Account
Account acct = (Account)myGenericSObject;
// 现在,您可以使用点符号来访问客户上的字段Account
String name = acct.Name;
String phone = acct.Phone;

告诉我更多…

与特定的sObjects类型不同,通用sObjects只能通过newSObject()方法创建。而且,只能通过put()和get()方法访问通用sObject的字段。

在这个单元中,你已经学习了什么sObjects以及如何使用它们。但是,创建一个sObject并不会将其作为记录保存在数据库中。要将sObject保存为记录,并用它来做其他事情,请使用数据操作语言(DML)。要检索记录,请使用Salesforce对象查询语言(SOQL)。查看以后的单元,了解DML和SOQL。

Salesforce Apex (基础1)入门

学习目标

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

  • 描述Apex编程语言的主要特点。
  • 保存Apex类并使用Anonymous.Apex调用方法。
  • 使用开发者控制台检查调试日志。

Apex入门

Apex是一种使用类似Java的语法的编程语言,像数据库存储过程一样行事。 Apex使开发人员能够将业务逻辑添加到系统事件中,如按钮点击,相关记录更新和Visualforce页面。

作为一种语言,Apex是:

  • Hosted-Apex在服务器(Force.com平台)上保存,编译并执行。
  • 面向对象-Apex支持类,接口和继承。
  • 强类型-Apex在编译时验证对对象的引用。
  • 多租户感知 – 因为Apex运行在多租户平台上,所以它通过强制实施限制防止代码独占共享资源,从而防止代码走失。
  • 与数据库集成 – 访问和操作记录非常简单。 Apex提供对记录及其字段的直接访问,并提供语句和查询语言来操作这些记录。
  • 关注数据–Apex提供对数据库的事务访问,允许您回滚操作。
  • 易于使用-Apex基于熟悉的Java成语。
  • 易于测试 – Apex为单元测试创​​建,执行和代码覆盖提供了内置的支持。
  • Salesforce确保所有定制的Apex代码在任何平台升级之前执行所有单元测试,并按预期工作。
  • Versioned-Custom Apex代码可以针对不同版本的API进行保存。
Apex is a cloud-based programming language

Apex 语言亮点

像其他面向对象的编程语言一样,这些是Apex支持的一些语言结构:

  • 类,接口,属性和集合(包括数组)。
  • 对象和数组表示法。
  • 表达式,变量和常量。
  • 条件语句(if-then-else)和控制流语句(for循环和while循环)。

与其他面向对象的编程语言不同,Apex支持:

  • 作为Apex的云开发是在云中存储,编译和执行的。
  • 触发器,类似于数据库系统中的触发器。
  • 数据库语句,允许您直接进行数据库调用和查询语言来查询和搜索数据。
  • 事务和回滚。
  • 全局访问修饰符,它比public修饰符更宽松,并允许跨命名空间和应用程序访问。
  • 自定义代码的版本。

另外,Apex是一个不区分大小写的语言。

开发工具

您可以使用Salesforce用户界面直接在浏览器中编写Apex并访问调试信息。打开您的名字下的开发者控制台或快速访问菜单(设置齿轮图标)。

您也可以使用Eclipse的Force.com IDE插件在客户端上编写Apex。请参阅Salesforce Developer网站上的Force.com IDE。

数据类型概述

Apex支持各种数据类型,包括特定于Salesforce的数据类型(sObject数据类型)。
Apex支持以下数据类型。

  • 诸如Integer,Double,Long,Date,Datetime,String,ID,Boolean等原语。
  • 一个sObject,或者作为一个通用的sObject或者一个特定的sObject,比如一个Account,Contact或者MyCustomObject__c(你将在后面的单元中学到更多关于sObjects的知识)。
  • 收藏品包括:
    • 基元,sObjects,用户定义的对象,从Apex类创建的对象或集合的列表(或数组)
    • 一组原语
    • 从基元到基元,sObject或集合的映射
  • 一个键入的值列表,也被称为枚举
  • 用户定义的Apex类
  • 系统提供的Apex类

Apex收藏:列表

列表包含对象的有序集合。 Apex中的列表与数组同义,两者可以互换使用。

以下两个声明是等价的。 colors变量是使用List语法声明的。

List<String> colors = new List<String>();
另外,颜色变量可以声明为一个数组,但分配给一个列表而不是一个数组。
String[] colors = new List<String>();

通常,创建列表而不是数组会更容易,因为列表不需要提前确定需要分配多少个元素。

您可以在创建列表时将元素添加到列表中,或者通过调用add()方法创建列表后添加元素。这第一个例子显示了向元素添加元素的两种方法。

// 创建一个列表并向其中添加元素
List<String> colors = new List<String> { 'red', 'green', 'blue' };

// 创建元素后,将元素添加到列表中
List<String> moreColors = new List<String>();
moreColors.add('orange');
moreColors.add('purple');
列表元素可以通过指定方括号之间的索引来读取,就像数组元素一样。另外,您可以使用get()方法读取列表元素。这个例子是基于前面例子中创建的列表,并展示了如何使用任一方法读取列表元素。该示例还显示了如何迭代数组元素。
// 从列表中获取元素
String color1 = moreColors.get(0);
String color2 = moreColors[0];
System.assertEquals(color1, color2);

// 遍历列表来读取元素
for(Integer i=0;i<colors.size();i++) {
    // Write value to the debug log
    System.debug(colors[i]);
}

超越基础

Apex支持另外两种集合类型:Set和Map。您可以在“Apex开发人员指南”的“集合”部分了解更多。

Apex 类

Apex类的好处之一是代码重用。类方法可以被触发器和其他类调用。以下教程将引导您保存组织中的示例类,使用此类发送电子邮件以及检查调试日志。

保存一个Apex类

将EmailManager类保存在您的组织中:

  1. 打开您的名字下的开发者控制台或快速访问菜单(设置齿轮图标Setup gear icon)。
  2. 在开发者控制台中,点击 File | New | Apex Class ,然后输入EmailManager作为类名称,然后单击OK。
  3. 用EmailManager类的例子替换默认的类体。

    EmailManager类具有公共方法(sendMail()),用于发送电子邮件并使用Apex类库的内置消息处理方法。另外,这个类有一个私有的帮助方法(inspectResults()),它不能被外部调用,因为它是私有的,但只在类中使用。这个帮助方法检查电子邮件发送调用的结果,并由sendMail()调用。

    public class EmailManager {
    
        // 公共方法
        public void sendMail(String address, String subject, String body) {
            // 创建一个电子邮件对象
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            String[] toAddresses = new String[] {address};
            mail.setToAddresses(toAddresses);
            mail.setSubject(subject);
            mail.setPlainTextBody(body);
            // 将此电子邮件传递给内置的sendEmail方法
            // 消息类的
            Messaging.SendEmailResult[] results = Messaging.sendEmail(
                                     new Messaging.SingleEmailMessage[] { mail });
            
            // 调用一个帮助器方法来检查返回的结果
            inspectResults(results);
        }
        
        // 辅助方法
        private static Boolean inspectResults(Messaging.SendEmailResult[] results) {
            Boolean sendResult = true;
            
            // sendEmail返回一个结果对象数组。
            // 遍历列表来检查结果。
            // 在这个类中,方法只发送一封邮件,
            // 所以我们应该只有一个结果。
            for (Messaging.SendEmailResult res : results) {
                if (res.isSuccess()) {
                    System.debug('Email sent successfully');
                }
                else {
                    sendResult = false;
                    System.debug('发生以下错误:' + res.getErrors());                 
                }
            }
            
            return sendResult;
        }
    
    }
  4. 点击Ctrl + S保存你的课程。

    超越基础

    刚刚保存的类使用面向对象编程(OOP)。该类封装了与管理电子邮件有关的方法。为了成为OOP的一个完美的例子,类还将包含成员变量(属性)和访问器方法来访问这些属性,但为了简单起见,我们的类没有这些。

    Salesforce在您保存时编译您的类。

调用方法发送电子邮件

我们来调用公共方法。我们将使用匿名的Apex执行来执行此操作。匿名Apex允许您即时运行代码行,并且是调用Apex的一种便捷方式,尤其是测试功能。与任何其他Apex执行一样,也会生成调试日志结果。

注意

还有其他方法可以通过触发器来调用Apex。您将在另一个模块中了解有关触发器的更多信息。

  1. 在开发者控制台中,点击 Debug | Open Execute Anonymous Window.
  2. 在打开的窗口中,输入以下内容。用您的电子邮件地址替换“您的电子邮件地址”。
    EmailManager em = new EmailManager();
    em.sendMail('Your email address', 'Trailhead Tutorial', '123 body');
    
  3. 点击 Execute.

    现在这个方法已经执行了,你应该在收件箱里收到一封电子邮件。查看你的电子邮件!

检查调试日志

调试日志对于调试代码很有用。当Apex方法执行时,调用会记录在调试日志中。而且,您可以将自己的调试消息写入日志,这有助于在出现错误时调试代码。由sendMail()调用的inspectResults()辅助方法使用System.debug()方法将消息写入日志,以指示电子邮件发送操作是成功还是有错误。您可以在执行该方法时生成的调试日志中查找这些消息。

  1. 在开发者控制台中,点击日志标签,然后双击列表中最近的日志。
  2. 选择 Debug Only 来过滤日志,以便仅显示System.debug()语句的日志行。
    Filter the debug log in the Developer Console to view debug messages

    注意

    另外,您可以通过在“筛选”字段中搜索任何关键字来筛选调试日志,或者通过选择任何其他选项来筛选调试日志。有关更多信息,请参阅日志检查器帮助。

    在过滤的日志视图中,您会看到以下消息,假设电子邮件发送没有错误。

    DEBUG|Email sent successfully

调用静态方法

因为我们类中的sendMail()方法不能访问类成员变量,所以它不需要是一个实例方法。让我们通过添加静态关键字到它的声明来将其更改为静态方法。静态方法比实例方法更容易调用,因为它们不需要在类的实例上调用,而是直接在类名上调用。

  1. 在开发人员控制台中,找到EmailManager类的打开选项卡,并将sendMail()方法定义的第一行修改为以下内容(唯一的变化是添加的static关键字)。
    public static void sendMail(String address, String subject, String body) {
  2. 按Ctrl + S保存该类。
  3. 修改“执行匿名”窗口中的语句以调用类名称上的静态方法。
    EmailManager.sendMail('Your email address', 'Trailhead Tutorial', '123 body');
    
  4. 点击 Execute.

    现在这个方法已经执行了,你可以像在前面的步骤一样检查你的电子邮件,以及可选的调试日志。

Apex 测试(3)

学习目标

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

  • 创建一个测试工具类。
  • 使用测试实用程序方法为各种测试用例设置测试数据。
  • 执行一个类中的所有测试方法。

为Apex测试创建测试数据

使用测试实用程序类为测试数据设置添加可重用的方法。

先决条件

如果您还没有这样做,请完成上一个单元“测试Apex触发器”中的先决条件。

添加一个测试工具类

让我们通过用一个实用程序类方法的调用替换测试数据创建来重构以前的测试方法。首先,您需要创建测试实用程序类。

TestDataFactory类是一种特殊的类,它是一个公共类,它是用isTest注释的,只能从正在运行的测试中访问。测试工具类包含可以被测试方法调用来执行有用任务的方法,例如设置测试数据。测试工具类从组织的代码大小限制中排除。

要添加TestDataFactory类:

  1. 在开发者控制台中,点击 File | New | Apex Class, 然后输入TestDataFactory作为类名,然后单击OK。
  2. 用下面的代码替换默认的类体。
    @isTest
    public class TestDataFactory {
        public static List<Account> createAccountsWithOpps(Integer numAccts, Integer numOppsPerAcct) {
            List<Account> accts = new List<Account>();
            
            for(Integer i=0;i<numAccts;i++) {
                Account a = new Account(Name='TestAccount' + i);
                accts.add(a);
            }
            insert accts;
            
            List<Opportunity> opps = new List<Opportunity>();
            for (Integer j=0;j<numAccts;j++) {
                Account acct = accts[j];
                // 对于刚插入的每个帐户,添加机会
                for (Integer k=0;k<numOppsPerAcct;k++) {
                    opps.add(new Opportunity(Name=acct.Name + ' Opportunity ' + k,
                                           StageName='Prospecting',
                                           CloseDate=System.today().addMonths(1),
                                           AccountId=acct.Id));
                }
            }
            // 插入所有帐户的所有机会
            insert opps;
            
            return accts;
        }
    }
    此测试实用程序类包含一个静态方法createAccountsWithOpps(),该方法接受numAccts参数中保存的帐户数量以及为每个帐户(保存在numOppsPerAcct参数中)创建的相关机会数量。方法中的第一个循环创建了指定数量的帐户并将其存储在accts列表变量中。在第一个循环之后,将调用insert()DML语句在数据库的列表中创建所有帐户。

第二个循环创造了机会。由于每个机会组都链接到一个客户,所以外部循环通过客户进行迭代,并包含一个嵌套的循环,为当前客户创建相关的机会。下次运行嵌套循环时,使用add()方法将机会添加到同一个列表中。使用AccountId字段将机会链接到其父帐户。创建的所有机会总数是机会数量与客户数量的乘积(numOppsPerAcct * numAccts)。接下来,在循环之外有效地调用insert()DML语句,以便仅在一个调用中为所有帐户创建集合中的所有机会。

最后,这个方法返回新帐户的列表。

注意

尽管此方法不会返回相关的机会,但您可以通过编写SOQL查询来获取这些记录,该查询利用Account和Opportunity之间的现有关系,例如Testing Apex Triggers中的触发器中使用的查询。

调用测试数据创建的实用程序方法

现在您已经添加了测试实用程序类,请修改测试类以利用此类。在TestAccountDeletion类中,替换以// Test data setup开始的块,并以insert opp结束:

        // 测试数据设置
        // 通过调用实用程序方法创建一个拥有一个机会的帐户
        Account[] accts = TestDataFactory.createAccountsWithOpps(1,1);
TestDataFactory.createAccountsWithOpps(1,1)调用返回的数组包含一个Account sObject。

这是修改后的测试方法。更短的版本!

@isTest
private class TestAccountDeletion {

    @isTest static void TestDeleteAccountWithOneOpportunity() {
        // 测试数据设置
        // 通过调用实用程序方法创建一个拥有一个机会的帐户
        Account[] accts = TestDataFactory.createAccountsWithOpps(1,1);
        
        // 执行测试
        Test.startTest();
        Database.DeleteResult result = Database.delete(accts[0], false);
        Test.stopTest();

        // 验证删除是否应该被触发器停止,
        // 检查我们是否收到错误
        System.assert(!result.isSuccess());
        System.assert(result.getErrors().size() > 0);
        System.assertEquals('不能删除有相关机会的帐户.',
                             result.getErrors()[0].getMessage());
    }        
}

测试不同的条件

一种测试方法不足以测试触发器的所有可能输入。我们需要测试一些其他条件,例如何时删除没有机会的客户。我们还需要使用批量数量的记录来测试相同的方案,而不是只记录一个记录。这里是包含三个附加测试方法的测试类的更新版本。保存此类更新的版本。

@isTest
private class TestAccountDeletion {

    @isTest static void TestDeleteAccountWithOneOpportunity() {
        // 测试数据设置
        // 通过调用实用程序方法创建一个拥有一个机会的帐户
        Account[] accts = TestDataFactory.createAccountsWithOpps(1,1);
        
        // Perform test
        Test.startTest();
        Database.DeleteResult result = Database.delete(accts[0], false);
        Test.stopTest();

        // 验证删除是否应该被触发器停止,
        // 检查我们是否收到错误
        System.assert(!result.isSuccess());
        System.assert(result.getErrors().size() > 0);
        System.assertEquals('不能删除有相关机会的帐户',
                             result.getErrors()[0].getMessage());
    }
    
    @isTest static void TestDeleteAccountWithNoOpportunities() {
        // 测试数据设置
        // 通过调用实用程序方法创建一个没有机会的帐户
        Account[] accts = TestDataFactory.createAccountsWithOpps(1,0);
        
        // 执行测试
        Test.startTest();
        Database.DeleteResult result = Database.delete(accts[0], false);
        Test.stopTest();

        //验证删除是否成功
        System.assert(result.isSuccess());
    }
    
    @isTest static void TestDeleteBulkAccountsWithOneOpportunity() {
        // 测试数据设置
        // 通过调用一个实用程序方法创建每个客户
        Account[] accts = TestDataFactory.createAccountsWithOpps(200,1);
        
        // 执行测试
        Test.startTest();
        Database.DeleteResult[] results = Database.delete(accts, false);
        Test.stopTest();

        // 验证每个记录。
        // 在这种情况下,删除应该已经被触发器停止,
        // 检查我们是否收到错误
        for(Database.DeleteResult dr : results) {
            System.assert(!dr.isSuccess());
            System.assert(dr.getErrors().size() > 0);
            System.assertEquals('不能删除有相关机会的帐户',
                                 dr.getErrors()[0].getMessage());
        }
    }
    
    @isTest static void TestDeleteBulkAccountsWithNoOpportunities() {
        // 测试数据设置
        // 通过调用实用程序方法创建没有机会的帐户
        Account[] accts = TestDataFactory.createAccountsWithOpps(200,0);
        
        // 执行测试
        Test.startTest();
        Database.DeleteResult[] results = Database.delete(accts, false);
        Test.stopTest();

        // 对于每条记录,验证删除是否成功
        for(Database.DeleteResult dr : results) {
            System.assert(dr.isSuccess());
        }
    }
}
运行所有测试方法

最后一步是在我们的测试类中运行测试方法,现在该类包含更全面的测试,并被重构为使用测试数据工厂。由于您已经在TestAccountDeletion类中运行了测试,因此您可以重新运行此测试类以运行其所有测试方法。

  1. 要执行相同的测试运行,请单击测试选项卡,选择您的测试运行,然后单击Test | Rerun.
  2. 通过展开最新的测试运行来检查“测试”选项卡中的结果。测试运行应报告所有四个测试通过!

Apex 测试(2)

学习目标

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

  • 编写对单个记录操作触发的触发器的测试。
  • 执行一个类中的所有测试方法。

测试Apex 触发器

在部署触发器之前,编写单元测试以执行触发触发器的操作并验证预期的结果。
让我们测试一下我们之前在Writing Apex Triggers单元中使用的触发器。如果一个客户记录有相关的机会,AccountDeletion触发器将阻止记录的删除。

先决条件

  1. 如果您尚未添加AccountDeletion触发器,请按照下列步骤操作。
    1. 在开发者控制台中,点击File | New | Apex Trigger.
    2. 输入AccountDeletion作为触发器名称,然后选择sObject的Account。点击Submit.
    3. 用下面的代码替换默认的代码。
      trigger AccountDeletion on Account (before delete) {
         
          // 如果他们有相关的联系人,防止删除帐户。
          for (Account a : [SELECT Id FROM Account
                           WHERE Id IN (SELECT AccountId FROM Opportunity) AND
                           Id IN :Trigger.old]) {
              Trigger.oldMap.get(a.Id).addError(
                  '不能删除有相关机会的帐号');
          }
          
      }
  2. 如果您在之前的单元中添加了AccountDeletion触发器,但已将其禁用,以便系统可以检查您的挑战,请重新启用它。
    1. 从设置中搜索 Apex Triggers.
    2. 在Apex触发器页面上,单击AccountDeletion触发器旁边的Edit
    3. 选择Is Active.
    4. 点击Save.
  3. 如果您的组织包含以前的单元(称为AddRelatedRecord,CalloutTrigger或HelloWorldTrigger)的触发器,请禁用它们。例如,要禁用AddRelatedRecord触发器:
    1. 从设置中搜索 Apex Triggers.
    2. 在Apex触发器页面上,单击AddRelatedRecord触发器旁边的 Edit .
    3. 取消选择Is Active.
    4. 点击Save.
  4. 要禁用HelloWorldTrigger和CalloutTrigger触发器,请重复上述步骤。

添加和运行单元测试

首先,我们开始添加一个测试方法。这个测试方法验证触发器设计要做什么(正面情况):防止一个帐户被删除,如果它有相关的机会。

  1. 在开发者控制台中,点击 File | New | Apex Class.
  2. 输入TestAccountDeletion作为类名称,然后单击 OK.
  3. 用下面的代码替换默认的类体。
    @isTest
    private class TestAccountDeletion {
    
        @isTest static void TestDeleteAccountWithOneOpportunity() {
            // 测试数据设置
            // 创建一个有机会的帐户,然后尝试删除它
            Account acct = new Account(Name='Test Account');
            insert acct;
            Opportunity opp = new Opportunity(Name=acct.Name + ' Opportunity',
                                           StageName='Prospecting',
    CloseDate=System.today().addMonths(1),
                                           AccountId=acct.Id);
            insert opp;
            
            // 执行测试
            Test.startTest();
            Database.DeleteResult result = Database.delete(acct, false);
            Test.stopTest();
    
            // 验证
            // 在这种情况下,删除应该已经被触发器停止,
            // 确认我们收到了错误
            System.assert(!result.isSuccess());
            System.assert(result.getErrors().size() > 0);
            System.assertEquals('不能删除有相关机会的帐户',
                                 result.getErrors()[0].getMessage());
        }
        
    }

    测试方法首先建立一个机会的测试帐户。接下来,它将删除测试帐户,该帐户触发AccountDeletion触发器。测试方法通过检查Database.delete()调用的返回值来验证触发器是否阻止删除测试帐户。返回值是一个Database.DeleteResult对象,其中包含有关删除操作的信息。测试方法验证删除不成功并验证获得的错误消息。

  4. 要运行此测试,请单击 Test | New Run.
  5. 在Test Classes下,单击TestAccountDeletion。
  6. 要将TemperatureConverterTest类中的所有方法添加到测试运行中,请单击添加 Add Selected.
  7. 点击Run.

    在最新运行的“测试”选项卡中找到测试结果。

TestAccountDeletion测试类只包含一个测试方法,用于测试单个帐户记录。此外,这个测试是针对正面的情况。始终测试更多方案,以确保触发器在所有情况下都能正常工作,包括删除没有机会的帐户和批量帐户删除。

测试数据是在测试方法内部设置的,添加更多的测试方法会耗费时间。如果您有许多测试方法,请将测试数据创建放在测试实用程序类中,并从多个测试方法中调用该实用程序类。下一个单元将向您展示如何利用测试工具类并添加更多的测试方法。

告诉我更多

测试方法包含Test.startTest()和Test.stopTest()方法对,该方法对代码块进行分隔,得到一组新的控制器限制。 在此测试中,测试数据设置在执行测试之前使用两个DML语句。 要测试Apex代码在限速范围内运行,请将数据设置的限制使用与测试隔离。 要隔离数据设置过程的限制使用,请将测试调用包含在Test.startTest()和Test.stopTest()块中。 测试异步Apex时也使用此测试块。 有关更多信息,请参阅使用限制,startTest和stopTest。

注意

开发人员控制台的一个已知问题是,在运行测试子集时,无法正确更新代码覆盖率。 要更新代码覆盖率结果,请使用 Test | Run All 而不是Test | New Run.

Apex 测试(1)

学习目标

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

  • 描述Apex单元测试的主要优点。
  • 用测试方法定义一个类。
  • 执行班级中的所有测试方法并检查故障。
  • 创建并执行一组测试类。

Apex 单元测试

Apex测试框架使您能够为Force.com平台上的Apex类和触发器编写和执行测试。 Apex单元测试可确保您的Apex代码的高质量,并让您满足部署Apex的要求。

测试是成功实现长期发展的关键,也是开发过程的关键组成部分。 Apex测试框架可以轻松测试您的Apex代码。 Apex代码只能在沙箱环境或开发者组织中编写,而不能在生产环境中编写。 Apex代码可以从沙箱部署到生产组织。此外,应用程序开发人员可以通过将软件包上传到Force.com AppExchange,将Apex代码从开发人员组织分发给客户。除了对质量保证至关重要之外,Apex单元测试也是部署和分销Apex的要求。以下是Apex单元测试的好处。

  • 确保您的Apex类和触发器按预期工作
  • 拥有一套可以在每次更新类和触发器时重新运行的回归测试,以确保您对应用程序进行的未来更新不会破坏现有的功能
  • 满足部署Apex生产或通过包装向客户分销Apex的代码覆盖要求
  • 高质量的应用程序交付给生产组织,使生产用户的工作效率更高
  • 高品质的应用程序交付给包用户,这增加了客户的信任

注意

在每次重大服务升级之前,Salesforce都会通过名为Apex Hammer的流程代表您运行所有Apex测试。 Hammer进程在当前版本和下一版本中运行,并比较测试结果。此过程可确保您的自定义代码中的行为未因服务升级而改变。锤子过程选择性地选择组织,并且不运行在所有组织。发现的问题是基于特定的标准进行分类的。 Salesforce致力于解决每个新版本之前发现的所有问题。

维护数据的安全性是我们的首要任务。我们不会查看或修改您的组织中的任何数据,并且所有测试都是在安全的数据中心中运行的副本中完成的。

代码覆盖要求的部署

在部署代码或将其打包为Force.com AppExchange之前,至少有75%的Apex代码必须由测试覆盖,所有这些测试都必须通过。另外,每个触发器都必须有一定的覆盖范围。尽管代码覆盖率是部署的要求,但是不要只写测试来满足这个要求。确保在您的应用中测试常见用例,包括正面和负面的测试用例,以及批量和单一记录处理。

测试方法语法

测试方法不带任何参数,并具有以下语法:

@isTest static void testName() {
    // code_block
}
或者,一个测试方法可以有这样的语法:
static testMethod void testName() {
    // code_block
}
使用isTest注释而不是testMethod关键字更灵活,因为您可以在注释中指定参数。我们稍后会介绍一个这样的参数。

测试方法的可见性并不重要,因此将测试方法声明为公共或私有并不会造成影响,因为测试框架总是能够访问测试方法。为此,语法中省略了访问修饰符。

测试方法必须在测试类中定义,这些类是用isTest注释的类。这个样本类用一种测试方法显示了一个测试类的定义。

@isTest
private class MyTestClass {
    @isTest static void myTest() {
        // code_block
    }
}
测试类可以是私有的或公共的。如果您仅使用测试类进行单元测试,则将其声明为私有。公共测试类通常用于测试数据工厂类,稍后会介绍。

单元测试示例:测试TemperatureConverter类

下面这个简单的例子是三个测试方法的测试类。正在测试的类方法以华氏温度作为输入。它将此温度转换为摄氏温度并返回转换后的结果。让我们添加自定义类和它的测试类。

  1. 在开发者控制台中,点击 File | New | Apex Class, 然后输入TemperatureConverter作为类名称,然后单击OK.
  2. 用下面的代码替换默认的类体。
    public class TemperatureConverter {
        // 拍摄华氏温度并返回摄氏温度。
        public static Decimal FahrenheitToCelsius(Decimal fh) {
            Decimal cs = (fh - 32) * 5/9;
            return cs.setScale(2);
        }
    }
  3. 按下Ctrl + S保存你的课程。
  4. 重复之前的步骤来创建TemperatureConverterTest类。添加以下这个类。
    @isTest
    private class TemperatureConverterTest {
    
        @isTest static void testWarmTemp() {
            Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70);
            System.assertEquals(21.11,celsius);
        }
        
        @isTest static void testFreezingPoint() {
            Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32);
            System.assertEquals(0,celsius);
        }
    
        @isTest static void testBoilingPoint() {
            Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212);        
            System.assertEquals(100,celsius,'不期望沸点温度');
        } 
        
        @isTest static void testNegativeTemp() {
            Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10);
            System.assertEquals(-23.33,celsius);
        }
          
    }
    TemperatureConverterTest测试类通过以华氏温度的不同输入调用该方法来验证该方法是否按预期工作。每种测试方法验证一种类型的输入:暖温度,冰点温度,沸点温度和负温度。验证是通过调用System.assertEquals()方法完成的,该方法有两个参数:第一个是期望值,第二个是实际值。这个方法有另一个版本,它接受第三个参数 – 一个描述比较的字符串,在testBoilingPoint()中使用。如果断言失败,则会记录此可选字符串。

让我们来运行这个类中的方法。

  1. 在开发者控制台中,点击 Test | New Run.
  2. 在Test Classes下,单击TemperatureConverterTest.
  3. 要将TemperatureConverterTest类中的所有测试方法添加到测试运行,请单击 Add Selected.
  4. 点击Run.
  5. 在“测试”选项卡中,您可以看到运行中的测试状态。展开测试运行,然后再次展开,直到看到运行的单个测试列表。他们都有绿色的选中标记。
    Inspect test results in the Developer Console

运行测试后,将自动为组织中的Apex类和触发器生成代码覆盖率。您可以在开发者控制台的“测试”标签中查看代码覆盖百分比。在这个例子中,你测试过的类,TemperatureConverter类,具有100%的覆盖率,如图所示。

View code coverage percentage in the Developer Console

注意

无论何时您修改Apex代码,请重新运行测试以刷新代码覆盖率结果。

开发人员控制台的一个已知问题是,在运行测试子集时,无法正确更新代码覆盖率。要更新代码覆盖率结果,请使用Test | Run All 而不是Test | New Run.

虽然一种测试方法会导致TemperatureConverter类的全面覆盖,但测试不同的输入以确保代码质量仍然很重要。显然,不可能验证每个数据点,但可以测试常见的数据点和不同的输入范围。例如,您可以验证传递正数和负数,边界值和无效参数值以验证负面行为。 TemperatureConverter类别的测试验证常用数据点,如沸腾温度和负温度。

TemperatureConverterTest测试等级不包括无效输入或边界条件。边界条件是关于最小值和最大值。在这种情况下,温度转换方法接受一个可以接受大于Double值的Decimal。对于无效的输入,没有无效的温度,但唯一的无效输入为空。转换方法如何处理这个值?在这种情况下,当Apex运行时将参数变量解引用以评估公式时,它会引发System.NullPointerException。您可以修改FahrenheitToCelsius()方法来检查无效输入,并在这种情况下返回null,然后添加一个测试来验证无​​效的输入行为。

到目前为止,所有的测试都通过了,因为类方法中使用的转换公式是正确的。但是那很无聊!让我们尝试模拟一个失败,看看断言失败时会发生什么。例如,让我们修改沸点温度测试,并传递沸点摄氏温度(0而不是100)的错误预期值。这导致相应的测试方法失败。

  1. 将testBoilingPoint()测试方法更改为以下。
        @isTest static void testBoilingPoint() {
            Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212);        
            // 模拟失败
            System.assertEquals(0,celsius,'不期望沸点温度');
        }
  2. 要执行相同的测试运行,请单击测试选项卡中的最新运行,然后单击 Test | Rerun.

    testBoilingPoint()中的断言失败,并引发一个致命错误(一个无法捕获的AssertException)。

  3. 通过展开最新的测试运行来检查“测试”选项卡中的结果。测试运行报告四分之一的测试失败。要获得有关失败的更多详细信息,请双击测试运行。

    详细的结果显示在单独的选项卡中,如图所示。

    Inspect results of a failed test in the Developer Console
  4. 要获取测试失败的错误消息,请双击失败测试的Errors列。你会看到以下内容。 Assertion Failed旁边的描述性文本是我们在System.assertEquals()语句中提供的文本。

    System.AssertException: Assertion Failed: 不期望沸点温度.: Expected: 0, Actual: 100.00

这些测试方法中的测试数据是数字而不是Salesforce记录。您将了解更多有关如何测试Salesforce记录以及如何在下一单元中设置数据的信息。

增加您的代码覆盖率

在编写测试时,尽可能实现最高的代码覆盖率。不要只瞄准75%的覆盖率,这是Force.com平台对部署和软件包的最低覆盖率。测试覆盖的测试用例越多,代码的健壮性就越高。有时,即使在为所有类方法编写测试方法之后,代码覆盖率也不是100%。一个常见原因是没有涵盖条件代码执行的所有数据值。例如,当你的类方法有if语句时,一些数据值往往会被忽略,这些语句会根据是否满足条件评估条件而导致不同的分支被执行。确保您的测试方法考虑到这些不同的值。

此示例包含类方法getTaskPriority(),它包含两个if语句。这种方法的主要任务是根据给定的导联状态返回一个优先级字符串值。该方法首先验证状态,如果状态无效则返回null。如果状态是CA,则方法返回“高”;否则,返回任何其他状态值的“正常”。

public class TaskUtil {
    public static String getTaskPriority(String leadState) {
        // 验证输入
        if (String.isBlank(leadState) || leadState.length() > 2) {
            return null;
        }
            
        String taskPriority;
        
        if (leadState == 'CA') {
             taskPriority = 'High'; 
        } else {
             taskPriority = 'Normal';
        }
        
        return taskPriority;
    }
}
注意

等号运算符(==)执行不区分大小写的字符串比较,因此不需要先将字符串转换为小写字母。这意味着传入’ca’或’Ca’将满足字符串字面值’CA’的相等条件。

这是getTaskPriority()方法的测试类。测试方法只需调用一个状态(’NY’)的getTaskPriority()。

@isTest
private class TaskUtilTest {
    @isTest static void testTaskPriority() {
        String pri = TaskUtil.getTaskPriority('NY');
        System.assertEquals('Normal', pri);
    }
}
让我们在开发者控制台中运行这个测试类(TaskUtilTest),并检查测试覆盖的相应TaskUtil类的代码覆盖率。测试运行结束后,TaskUtil的代码覆盖率显示为75%。如果您在开发人员控制台中打开此课程,则会看到六个蓝色(被覆盖)线条和两个红色(未被覆盖)线条,如图所示。
Lines covered for the TaskUtil class in the Developer Console

第五行没有被覆盖的原因是因为我们的测试类没有包含一个测试来传递一个无效的状态参数。同样,第11行没有被覆盖,因为测试方法没有通过“CA”作为状态。我们再添加两个测试方法来覆盖这些情况。以下显示了添加testTaskHighPriority()和testTaskPriorityInvalid()测试方法后的完整测试类。如果您重新运行此测试类,TaskUtil的代码覆盖率现在为100%!

@isTest
private class TaskUtilTest {
    @isTest static void testTaskPriority() {
        String pri = TaskUtil.getTaskPriority('NY');
        System.assertEquals('Normal', pri);
    }
    
    @isTest static void testTaskHighPriority() {
        String pri = TaskUtil.getTaskPriority('CA');
        System.assertEquals('High', pri);
    }
    
    @isTest static void testTaskPriorityInvalid() {
        String pri = TaskUtil.getTaskPriority('Montana');
        System.assertEquals(null, pri);
    }
}
创建并执行测试套件

测试套件是一起运行的Apex测试类的集合。例如,创建一套您每次准备部署时运行的测试,或者Salesforce发布新版本。在开发者控制台中设置一个测试套件来定义一组定期一起执行的测试类。

您现在在您的组织中有两个测试课程。这两个阶级是不相关的,但让我们暂时假装他们是。假设有些情况下你想运行这两个测试类,但不想运行你的组织中的所有测试。创建一个包含两个类的测试套件,然后在套件中执行测试。

  1. 在开发者控制台中,选择 Test | New Suite.
  2. 输入TempConverterTaskUtilSuite作为套件名称,然后单击OK.
  3. 选择TaskUtilTest,按住Ctrl键,然后选择TemperatureConverterTest
  4. 要将所选测试类添加到套件,请单击 >.

    Test suite editing window with two selected test classes

  5. 点击Save.
  6. 选择Test | New Suite Run.
  7. 选择TempConverterTaskUtilSuite,然后单击>将TempConverterTaskUtilSuite移动到选择的测试套件列。
  8. 点击Run Suites.
  9. 在“测试”选项卡上,监视测试运行状态。展开测试运行,然后再次展开,直到看到运行的单个测试列表。就像在单个测试方法的运行中一样,您可以双击方法名称以查看详细的测试结果。

创建测试数据

在测试方法中创建的Salesforce记录不会提交到数据库。当测试结束执行时,它们会回滚。这个回滚行为对于测试是很方便的,因为在测试执行后你不必清理你的测试数据。

默认情况下,除了访问设置和元数据对象(如User或Profile对象)外,Apex测试不能访问组织中预先存在的数据。为您的测试设置测试数据。创建测试数据可使您的测试更健壮,并防止组织中缺少或更改数据导致的故障。您可以直接在测试方法中创建测试数据,也可以使用实用程序测试课程,稍后您将会看到。

注意

尽管这样做不是最佳实践,但有时候测试方法需要访问预先存在的数据。要访问组织数据,请使用@isTest注释测试方法(SeeAllData = true)。本机中的测试方法示例不访问组织数据,因此不使用SeeAllData参数。

告诉我更多…

  • 您可以在每个组织中最多保存3 MB的Apex代码。用@isTest注释的测试类不计入此限制。
  • 即使测试数据回滚,也不会使用单独的数据库进行测试。因此,对于某些具有唯一约束的字段的sObjects,插入重复的sObject记录会导致错误。
  • 测试方法不发送电子邮件。
  • 测试方法不能调出外部服务。您可以在测试中使用模拟标注。
  • 在测试中执行的SOSL搜索返回空结果。为了确保可预测的结果,使用Test.setFixedSearchResults()来定义搜索返回的记录。

Salesforce触发器批量记录处理

学习目标

完成后,您将能够:

  • 编写对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]) {

// 做一些其他处理
}
}

执行批量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>();

// 遍历相关的机会
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. 在客户页面的机会相关列表中,找到新的机会狮子会&猫。 触发器自动添加了机会!

自己动手尝试

为Opportunity创建一个Apex触发器,该触发器将任务添加到任何设置为“Closed Won”的机会。

为了完成这个挑战,你需要添加一个机会触发器。 触发器将添加一个任务到插入或更新阶段的“Closed Won”的任何机会。 任务的主题必须是“Follow Up Test Task”。

  • Apex触发器必须被称为“ClosedOpportunityTrigger”
  • 在“ClosedOpportunityTrigger”处于活动状态的情况下,如果机会被插入或更新为“Closed Won”的阶段,那么它将具有使用“Follow Up Test Task”主题创建的任务。
  • 要将任务与机会相关联,请使用机会ID填写“WhatId”字段。
  • 这个挑战在一次操作中专门测试200条记录。

Salesforce触发器

编写触发器

Apex触发器使您能够在操作Salesforce中的记录(例如插入,更新或删除)事件之前或之后执行执行自定义操作。就像数据库系统支持触发器一样,Apex提供触发器支持来管理记录。
通常,您可以使用触发器根据特定条件执行操作,修改相关记录或限制某些操作的发生。您可以使用触发器在Apex中执行任何操作,包括执行SOQL和DML或调用自定义的Apex方法。

触发器不像验证规则或工作流那样通过点选的方式,而是需要编写代码。

触发语法

触发器定义的语法不同于类定义的语法。 触发器定义以trigger关键字开始。 之后是触发器的名称,触发器所关联的Salesforce对象以及触发的条件。 触发器具有以下语法:

trigger TriggerName on ObjectName (trigger_events) {
code_block
}

要在插入,更新,删除和取消删除操作之前或之后执行触发器,请在逗号分隔列表中指定多个触发器事件。 您可以指定的事件是:

  • before insert
  • before update
  • before delete
  • after insert
  • after update
  • after delete
  • after undelete

例如

在插入帐户并将消息写入调试日志之前,触发这个简单的触发器。

1.在开发人员控制台中,单击File | New | Apex Trigger.。
2.为触发器名称输入HelloWorldTrigger,然后选择sObject的Account。 点击提交。
3.用以下代码替换默认代码。
trigger HelloWorldTrigger on Account (before insert) {
System.debug(‘Hello World!’);
}
4.要保存,请按Ctrl + S。
5.要测试触发器,请创建一个帐户。
单击 Debug | Open Execute Anonymous Window.。
在新窗口中,添加以下内容,然后单击执行。
Account a = new Account(Name=’Test Trigger’);
insert a;
6.在调试日志中,找到Hello World! 声明。 日志还显示触发器已被执行。

触发器的类型

有两种类型的触发器。

  • Before triggers 在保存到数据库之前用于更新或验证记录值之前。
  • After triggers 使用触发器访问由系统设置的字段值(例如记录的Id或LastModifiedDate字段),并影响其他记录中的更改。 触发后触发器的记录是只读的。

使用上下文变量

要访问导致触发器触发的记录,请使用上下文变量。 例如,Trigger.New包含插入或更新触发器中插入的所有记录。 Trigger.Old在更新触发器中更新之前提供旧版本的sObjects,或在删除触发器中删除sObjects的列表。 当插入一个记录时,或者通过API或Apex批量插入大量记录时,触发器可以触发。 因此,上下文变量(如Trigger.New)只能包含一条记录或多条记录。 您可以遍历Trigger.New来获取每个单独的sObject。

这个例子是HelloWorldTrigger示例触发器的修改版本。 它遍历for循环中的每个帐户并更新每个帐户的说明字段。

trigger HelloWorldTrigger on Account (before insert) {
for(Account a : Trigger.New) {
a.Description = ‘New description’;
}
}

一些其他的上下文变量返回一个布尔值来指示触发器是否由于更新或其他事件而被触发。 当触发器组合多个事件时,这些变量很有用。 例如:

trigger ContextExampleTrigger on Account (before insert, after insert, after delete) {
if (Trigger.isInsert) {
if (Trigger.isBefore) {
// 插入前的过程
} else if (Trigger.isAfter) {
//插入后处理
}
}
else if (Trigger.isDelete) {
// 删除后处理
}
}

触发上下文变量

变量 用法
isExecuting 如果Apex代码的当前上下文是触发器,而不是Visualforce页面,Web服务或executeanonymous()API调用,则返回true。
isInsert 如果由于Salesforce用户界面,Apex或API的插入操作而触发此触发器,则返回true。
isUpdate 如果由于Salesforce用户界面,Apex或API的更新操作而触发此触发器,则返回true。
isDelete 如果由于Salesforce用户界面,Apex或API的删除操作而触发此触发器,则返回true。
isBefore 如果在保存任何记录之前触发此触发器,则返回true。
isAfter 如果在保存所有记录后触发此触发器,则返回true。
isUndelete 如果在从回收站中恢复记录(即从Salesforce用户界面,Apex或API取消删除操作之后)触发此触发器,则返回true。
new 返回sObject记录的新版本列表。
此sObject列表仅在插入,更新和取消删除触发器中可用,并且记录只能在before触发器中修改。
newMap ID对新对象记录的版本的映射。
此映射仅在更新之前,插入之后,更新之后和取消删除触发器之后可用。
old 返回sObject记录的旧版本列表。
该sObject列表仅在更新和删除触发器中可用。
oldMap 旧版本的sObject记录的ID映射。
此映射仅在更新和删除触发器中可用。
size 触发器调用中的记录总数,包括旧的和新的。

触发器调用类方法

您可以通过触发器调用公用方法。 调用其他类的方法可以重用代码,减少触发器的大小,并且可以提高Apex代码的维护。 它也允许你使用面向对象的编程。

以下示例触发器显示如何从触发器调用静态方法。 如果由于插入事件触发了该触发器,则此示例将在EmailManager类上调用静态sendMail()方法。 此实用程序方法向指定的收件人发送电子邮件,并包含插入的联系人记录数。

  1. 在开发者控制台中,点击 File | New | Apex Trigger.
  2. 输入ExampleTrigger作为触发器名称,然后选择Contact作为sObject。 点击 Submit.
  3. 将默认代码替换为以下内容,然后将sendMail()中的电子邮件地址占位符文本修改为您的电子邮件地址
    trigger ExampleTrigger on Contact (after insert, after delete) {
    if (Trigger.isInsert) {

Integer recordCount = Trigger.New.size();
// 从另一个类调用实用程序方法
EmailManager.sendMail(‘您的电子邮件地址’, ‘触发教程’,
recordCount + ‘ contact(s) were inserted.’);
}
else if (Trigger.isDelete) {
// 删除后处理
}
}

4. 要保存,请按 Ctrl+S.
5.测试触发器,请创建一个联系人。
单击 Debug | Open Execute Anonymous Window.
在新窗口中,添加以下内容,然后单击执行

Contact c = new Contact(LastName=’Test Contact’);
insert c;

6.在调试日志中,检查触发器是否被触发。 在日志末尾,找到实用程序方法编写的调试消息:DEBUG | Email发送成功
7.现在检查您是否收到一封电子邮件,其中正文1个联系人已被插入。

添加相关记录

触发器通常用于访问和管理与触发器上下文中的记录相关的记录 – 触发该触发器的记录。

如果没有机会与客户相关联,则此触发器为每个新的或更新的客户添加相关机会。 触发器首先执行SOQL查询,以获取触发器触发的帐户的所有子机会。 接下来,触发器遍历Trigger.New中的sObjects列表以获取每个帐户的sObject。 如果该帐户没有任何相关的机会sObjects,for循环会创建一个。 如果触发器创造了新的机会,最后的陈述将插入它们。

1.使用开发者控制台添加以下触发器(按照HelloWorldTrigger示例的步骤,但使用AddRelatedRecord作为触发器名称)。

trigger AddRelatedRecord on Account(after insert, after update) {
List<Opportunity> oppList = new List<Opportunity>();

// 在此触发器中获取帐户的相关机会
Map<Id,Account> acctsWithOpps = new Map<Id,Account>(
[SELECT Id,(SELECT Id FROM Opportunities) FROM Account WHERE Id IN :Trigger.New]);

// 为每个帐户添加一个机会,如果它还没有。
//遍历每个帐户。
for(Account a : Trigger.New) {
System.debug(‘acctsWithOpps.get(a.Id).Opportunities.size()=’ + acctsWithOpps.get(a.Id).Opportunities.size());
// 检查帐户是否有相关的机会。
if (acctsWithOpps.get(a.Id).Opportunities.size() == 0) {
// 如果没有,请添加一个默认机会
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用户界面中创建一个帐户,并将其命名为Apple和Orange。
3.在客户页面的机会相关列表中,找到新的机会。 触发器自动添加了这个机会!

使用触发器异常

您有时需要对某些数据库操作添加限制,例如在满足某些条件时防止保存记录。 为了防止在触发器中保存记录,在有问题的sObject上调用addError()方法。 addError()方法在触发器中引发致命错误。 错误消息显示在用户界面中并被记录。

如果具有相关机会,则以下触发器可防止删除帐户。 默认情况下,删除一个帐户会导致所有相关记录的级联删除。 这个触发器可以防止级联删除机会。 为自己尝试这个触发器! 如果你已经执行了前面的例子,你的组织有一个名为苹果和橙子的客户有一个相关的机会。 本示例使用该示例帐户。

1.使用开发者控制台添加以下触发器。
trigger AccountDeletion on Account (before delete) {

// 如果他们有相关的机会,防止删除帐户。
for (Account a : [SELECT Id FROM Account
WHERE Id IN (SELECT AccountId FROM Opportunity) AND
Id IN :Trigger.old]) {
Trigger.oldMap.get(a.Id).addError(
‘不能删除有相关机会的帐户。’);
}

}
2.在Salesforce用户界面中,导航到Apples&Oranges帐户的页面,然后单击删除。
3.在确认弹出窗口中,单击确定。
使用自定义错误消息查找验证错误:不能删除有相关机会的帐户。
4.禁用AccountDeletion触发器。 如果你保持这个触发器激活,你不能检查你的挑战。
从设置中搜索Apex触发器。
在Apex触发器页面上,单击AccountDeletion触发器旁边的编辑。
取消选择Is Active的。
点击Save

触发器和标注

Apex允许您打电话并将Apex代码与外部Web服务集成。 Apex调用外部Web服务称为标注。 例如,您可以调出股票报价服务以获取最新的报价。 当从触发器中进行标注时,标注必须异步完成,以便在等待外部服务的响应时触发器不会阻止您的工作。异步标注在后台进程中进行,并在收到响应时 外部服务返回它。

要从触发器中进行调用,请调用异步执行的类方法。 这种方法被称为未来的方法,并用@future(callout = true)进行注释。 此示例类包含制作标注的未来方法。

public class CalloutClass {
@future(callout=true)
public static void makeCallout() {
HttpRequest request = new HttpRequest();
// 设置端点URL。
String endpoint = ‘http://yourHost/yourService’;
request.setEndPoint(endpoint);
// 将HTTP动词设置为GET。
request.setMethod(‘GET’);
// 发送HTTP请求并获得响应。
HttpResponse response = new HTTP().send(request);
}
}

这个例子展示了调用类中的方法来异步调用标注的触发器。

trigger CalloutTrigger on Account (before insert, before update) {
CalloutClass.makeCallout();
}

本部分仅提供了标注的概述,并不打算详细说明标注。 有关更多信息,请参阅Apex Developer Guide中的使用Apex调用标注。

自己动手做

根据自定义字段创建与配送地址邮编匹配的配送地址邮编的帐户的Apex触发器。

对于此挑战,您需要创建一个触发器,在插入或更新之前检查复选框,如果复选框字段为true,则将装运邮政编码(其API名称为ShippingPostalCode)设置为与帐单邮政 代码(BillingPostalCode)。

  • Apex触发器必须被称为“AccountAddressTrigger”。
  • 帐户对象将需要一个新的自定义复选框,应该有字段标签“匹配帐单地址”和字段名称“Match_Billing_Address”。 生成的API名称应该是“Match_Billing_Address__c”。
  • 在“AccountAddressTrigger”处于活动状态时,如果某个帐户具有帐单邮政编码且“Match_Billing_Address__c”为true,那么该记录应具有在插入或更新时匹配的运输邮政编码。