Apex REST外部调用

学习目标

完成后,您将能够:

  • 执行外呼以接收来自外部服务的数据。
  • 执行外呼将数据发送到外部服务。
  • 使用模拟标注测试外呼。

HTTP和外呼基础

REST外呼基于HTTP。要了解外呼如何工作,了解一些有关HTTP的内容很有帮助。每个外呼请求都与HTTP方法和端点相关联。 HTTP方法指示需要什么类型的动作。
Apex callouts to an external service

最简单的请求是一个GET请求(GET是一个HTTP方法)。 GET请求意味着发送者想要从服务器获取关于资源的信息。当服务器接收并处理此请求时,它将请求信息返回给收件人。 GET请求与导航到浏览器中的地址相似。当您访问网页时,浏览器会在幕后执行GET请求。在浏览器中,导航的结果是显示的新HTML页面。用外呼,结果是响应对象。

为了说明GET请求的工作方式,打开浏览器并导航到以下URI: https://th-apex-http-callout.herokuapp.com/animals.您的浏览器以奇怪的格式显示动物列表,因为服务以JSON格式返回响应。有时GET响应也是用XML格式化的。

以下是常用HTTP方法的说明。

表1.一些常用的HTTP方法

HTTP方法 描述
GET 检索由URL标识的数据
POST 创建资源或将数据发布到服务器。
DELETE 删除由URL标识的资源。
PUT 创建或替换请求正文中发送的资源。

如果您有空闲时间,请在参考资料部分浏览所有HTTP方法的详尽列表。

除了HTTP方法之外,每个请求都会设置一个URI,这是服务所在的端点地址。例如,一个端点可以是 http://www.example.com/api/resource.在 “HTTP and Callout Basics” 单元中的示例中,端点是https://th-apex-http-callout.herokuapp.com/animals.

当服务器处理请求时,它在响应中发送一个状态码。状态码指示请求是否成功处理或是否遇到错误。如果请求成功,服务器会发送一个200的状态码。您可能已经看到一些其他的状态码,例如404没有找到文件,500没有找到内部服务器错误。

如果在浏览HTTP方法列表后仍然有空闲时间,请查看参考资料部分的所有响应状态代码列表。如果你晚上睡得很困难,这两个资源可以帮助你。

从服务获取数据

现在是时候把你的新HTTP知识用于一些Apex标注。本示例向Web服务发送GET请求以获取林地生物列表。该服务以JSON格式发送响应。 JSON本质上是一个字符串,所以内置的JSONParser类将它转换为一个对象。然后,我们可以使用该对象将每个动物的名称写入调试日志。
在本机运行示例之前,您需要使用“授权端点地址”部分中的步骤来授权标注的端点URL , https://th-apex-http-callout.herokuapp.com, 使用“授权端点地址”部分的步骤。
  1. 从设置档(打开设备齿轮图标)打开开发者控制台。 (Setup gear icon).
  2. 在开发者控制台中,选择 Debug | Open Execute Anonymous Window.
  3. 删除现有的代码并插入下面的代码片段
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(‘https://th-apex-http-callout.herokuapp.com/animals’);
    request.setMethod(‘GET’);
    HttpResponse response = http.send(request);
    // 如果请求成功,则解析JSON响应。
    if (response.getStatusCode() == 200) {
    // 将JSON字符串反序列化为原始数据类型的集合。
    Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
    // “动物”键中的值作为列表进行投射
    List<Object> animals = (List<Object>) results.get(‘animals’);
    System.debug(‘Received the following animals:’);
    for (Object animal: animals) {
    System.debug(animal);
    }
    }
  4. 选择 Open Log, 然后单击Execute.
  5. 调试日志打开后,选择 Debug Only 查看System.debug语句的输出。
    显示动物的名字。

我们例子中的JSON相当简单并且易于解析。对于更复杂的JSON结构,可以使用JSON2Apex。该工具生成用于解析JSON结构的强类型的Apex代码。您只需粘贴JSON,该工具就会为您生成必要的Apex代码。荣耀!

发送数据到服务

HTTP 外呼的另一个常见用例是将数据发送到服务。例如,当您购买最新的贾斯汀·比伯(Justin Bieber)专辑或评论您最喜欢的“Cat in a Shark Costume Chases a Duck While Riding a Roomba”视频时,您的浏览器正在提交POST请求以提交数据。我们来看看我们如何在Apex中发送数据。
本示例向Web服务发送POST请求以添加动物名称。新名称作为JSON字符串添加到请求正文中。请求Content-Type头设置为让服务知道发送的数据是JSON格式,以便它可以适当地处理数据。该服务通过发送状态代码和所有动物列表(包括您添加的动物)作出响应。如果请求已成功处理,则状态码将返回201,因为已创建资源。如果返回201以外的任何内容,则将响应发送到调试日志。
  1. 从设置档(打开设备齿轮图标)打开开发者控制台 (Setup gear icon).
  2. 在开发者控制台中,选择 Debug | Open Execute Anonymous Window.
  3. 删除任何现有的代码并插入以下片段。
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(‘https://th-apex-http-callout.herokuapp.com/animals’);
    request.setMethod(‘POST’);
    request.setHeader(‘Content-Type’, ‘application/json;charset=UTF-8’);
    // 将主体设置为JSON对象
    request.setBody(‘{“name”:”mighty moose”}’);
    HttpResponse response = http.send(request);
    // 解析JSON响应
    if (response.getStatusCode() != 201) {
    System.debug(‘返回的状态代码不是预期的: ‘ +
    response.getStatusCode() + ‘ ‘ + response.getStatus());
    } else {
    System.debug(response.getBody());
    }

  4. 选择打开 Open Log, 然后单击 Execute.
  5. 调试日志打开时,请选择 Debug Only 查看System.debug语句的输出。动物列表中的最后一项是 “mighty moose”.

测试外呼

外呼测试有好消息和坏消息。坏消息是Apex测试方法不支持外呼,执行标注的测试失败。好消息是测试运行时允许你“mock”外呼。模拟标注允许您指定在测试中返回的响应,而不是实际调用Web服务。你实际上在告诉运行时,“我知道这个web服务将会返回什么,所以不要在测试过程中调用它,而是返回这个数据。”在你的测试中使用模拟标注有助于确保你获得足够的代码覆盖率,代码由于标注而被跳过。

先决条件

在编写测试之前,让我们创建一个类,其中包含我们在“将数据发送到服务”单元中匿名执行的GET和POST请求示例。这些示例稍有修改,以便请求在方法和返回值中,但它们与前面的示例基本相同。

  1. 在开发者控制台中,选择 File | New | Apex Class.
  2. 对于课程名称,请输入AnimalsCallouts 然后单击OK.
  3. 将自动生成的代码替换为以下类定义。

    public class AnimalsCallouts {

    public static HttpResponse makeGetCallout() {
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(‘https://th-apex-http-callout.herokuapp.com/animals’);
    request.setMethod(‘GET’);
    HttpResponse response = http.send(request);
    //如果请求成功,则解析JSON响应。
    if (response.getStatusCode() == 200) {
    // 将JSON字符串反序列化为原始数据类型的集合。
    Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
    // 将“animals”键中的值作为列表进行投射
    List<Object> animals = (List<Object>) results.get(‘animals’);
    System.debug(‘Received the following animals:’);
    for (Object animal: animals) {
    System.debug(animal);
    }
    }
    return response;
    }

    public static HttpResponse makePostCallout() {
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(‘https://th-apex-http-callout.herokuapp.com/animals’);
    request.setMethod(‘POST’);
    request.setHeader(‘Content-Type’, ‘application/json;charset=UTF-8’);
    request.setBody(‘{“name”:”mighty moose”}’);
    HttpResponse response = http.send(request);
    // 解析JSON响应
    if (response.getStatusCode() != 201) {
    System.debug(‘预期不会返回状态码: ‘ +
    response.getStatusCode() + ‘ ‘ + response.getStatus());
    } else {
    System.debug(response.getBody());
    }
    return response;
    }

    }

  4. 按下 CTRL+S 保存..

使用StaticResourceCalloutMock测试外呼

要测试您的标注,请通过实现接口或使用静态资源来使用模拟标注。在这个例子中,我们稍后使用静态资源和模拟接口。静态资源包含要返回的响应主体。同样,使用模拟标注时,请求不会发送到端点。相反,Apex运行时知道查找在静态资源中指定的响应,并返回它。 Test.setMock方法通知运行时在测试方法中使用模拟标注。让我们看看模拟标注在行动。首先,我们创建一个包含JSON格式字符串的静态资源,用于GET请求。

  1. 在开发者控制台中,选择 File | New | Static Resource.
  2. 对于名称,输入GetAnimalResource.
  3. 对于MIME类型,选择text/plain, 即使使用JSON.
  4. 点击Submit.
  5. 在为资源打开的选项卡中,插入以下内容。确保它全部在一行上,不会中断到下一行。这个内容就是模拟标注返回的内容。这是三个林地生物阵列。
    {“animals”: [“pesky porcupine”, “hungry hippo”, “squeaky squirrel”]}
  6. 点击CTRL+S保存.

您已成功创建您的静态资源!现在,让我们为使用此资源的标注添加一个测试。

  1. 在开发者控制台中,选择 File | New | Apex Class.
  2. 对于类名称,输入 AnimalsCalloutsTest 然后单击OK.
  3. 将自动生成的代码替换为以下测试类定义。

    @isTest
    private class AnimalsCalloutsTest {

    @isTest static void testGetCallout() {
    // 基于静态资源创建模拟响应
    StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
    mock.setStaticResource(‘GetAnimalResource’);
    mock.setStatusCode(200);
    mock.setHeader(‘Content-Type’, ‘application/json;charset=UTF-8’);
    // 外呼与模拟响应相关联
    Test.setMock(HttpCalloutMock.class, mock);
    // 调用方法来测试
    HttpResponse result = AnimalsCallouts.makeGetCallout();
    // 验证模拟响应不为空
    System.assertNotEquals(null,result,
    ‘标注返回了一个空响应。’);
    // 验证状态码
    System.assertEquals(200,result.getStatusCode(),
    ‘状态码不是200.’);
    // 验证内容类型
    System.assertEquals(‘application/json;charset=UTF-8’,
    result.getHeader(‘Content-Type’),
    ‘The content type value is not expected.’);
    // 验证数组包含3个项目
    Map<String, Object> results = (Map<String, Object>)
    JSON.deserializeUntyped(result.getBody());
    List<Object> animals = (List<Object>) results.get(‘animals’);
    System.assertEquals(3, animals.size(),
    ‘数组只应该包含3个项目’);
    }

    }

  4. 点击CTRL+S 保存.
  5. 选择 Test | Always Run Asynchronously. 如果不选择“始终运行异步”,则测试运行只包含一个同步运行的类。您只能从“测试”选项卡打开日志,以进行同步测试运行。
    要运行测试,请选择Test | New Run.
  6. 从Test Classes列表中选择 AnimalsCalloutsTest.
  7. 点击Add Selected | Run.

测试结果显示在测试运行ID下的测试选项卡中。测试执行完成后,展开测试运行以查看详细信息。现在在“代码总体覆盖率”窗格中双击AnimalCallouts,查看测试覆盖了哪些行。

使用HttpCalloutMock测试外呼

为了测试你的POST标注,我们提供了一个HttpCalloutMock接口的实现。此接口使您能够指定在响应方法中发送的响应。您的测试类指示Apex运行时通过再次调用Test.setMock发送此假响应。对于第一个参数,传递HttpCalloutMock.class。对于第二个参数,传递一个AnimalsHttpCalloutMock的新实例,它是您的HttpCalloutMock的接口实现。 (在这个例子之后的例子中,我们将写AnimalsHttpCalloutMock)

Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock());

现在添加实现HttpCalloutMock接口的类来拦截标注。如果在测试上下文中调用HTTP标注,则不会进行标注。相反,您会收到您在AnimalsHttpCalloutMock中的响应方法实现中指定的模拟响应。

  1. 在开发者控制台中,选择 File | New | Apex Class.
  2. 对于类名,输入 AnimalsHttpCalloutMock然后单击OK.
  3. 自动生成的代码替换为以下类定义。
    @isTest
    global class AnimalsHttpCalloutMock implements HttpCalloutMock {
    // 实现这个接口方法
    global HTTPResponse respond(HTTPRequest request) {
    // 创建一个假的回应
    HttpResponse response = new HttpResponse();
    response.setHeader(‘Content-Type’, ‘application/json’);
    response.setBody(‘{“animals”: [“majestic badger”, “fluffy bunny”, “scary bear”, “chicken”, “mighty moose”]}’);
    response.setStatusCode(200);
    return response;
    }
    }
  4. 点击CTRL+S保存.

在您的测试类中,创建testPostCallout方法来设置模拟标注,如下例所示。 testPostCallout方法调用Test.setMock,然后调用AnimalsCallouts类中的makePostCallout方法。然后验证返回的响应是您在模拟实现的响应方法中指定的。

  1. 修改测试类AnimalsCalloutsTest添加第二个测试方法。单击类选项卡,然后在右括号之前添加以下方法。
    @isTest static void testPostCallout() {
    // 设置模拟外呼类
    Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock());
    //这会导致一个假的响应被发送
    // 从实现HttpCalloutMock的类中
    HttpResponse response = AnimalsCallouts.makePostCallout();
    //验证收到的响应是否包含假值
    String contentType = response.getHeader(‘Content-Type’);
    System.assert(contentType == ‘application/json’);
    String actualValue = response.getBody();
    System.debug(response.getBody());
    String expectedValue = ‘{“animals”: [“majestic badger”, “fluffy bunny”, “scary bear”, “chicken”, “mighty moose”]}’;
    System.assertEquals(actualValue, expectedValue);
    System.assertEquals(200, response.getStatusCode());
    }
  2. 点击CTRL+S 保存.
  3. 选择 Test | New Run.
  4. 从Test Classes列表中选择AnimalsCalloutsTest.
  5. 点击 Add Selected | Run.

    测试结果显示在“测试”选项卡下的新测试运行ID下。当测试执行完成时,展开测试运行以查看有关这两个测试的详细信息。

更多…

了解如何在触发器和异步Apex中使用标注,以及如何制作异步标注。
当从一个方法进行标注时,该方法在执行后续代码行之前,等待外部服务发回标注响应。或者,您可以将标注代码置于用@future(callout = true)注释的异步方法中,或者使用Queueable Apex。这样,标注在单独的线程上运行,调用方法的执行不会被阻止。

从触发器进行标注时,标注不得在等待响应时阻止触发过程。为了使触发器能够进行标注,包含标注代码的方法必须使用@future(callout = true)进行注释,以便在单独的线程中运行。

自己动手尝试

创建一个调用REST端点的Apex类并编写一个测试类。

为了通过这个练习,创建一个调用REST端点的Apex类来返回一个动物的名字,编写单元测试,使用模拟响应实现类的100%代码覆盖率,并运行Apex测试。

  • Apex类必须被称为“AnimalLocator”,有一个“getAnimalNameById”方法接受一个Integer并返回一个String。
  • ‘getAnimalNameById’方法必须使用传入方法的ID调用https://th-apex-http-callout.herokuapp.com/animals/:id。 该方法返回“name”属性(即动物名称)的值。
  • 创建一个名为AnimalLocatorTest的测试类,该类使用名为AnimalLocatorMock的模拟类模拟标注响应。
  • 单元测试必须涵盖AnimalLocator类中包含的所有代码行,从而得到100%的代码覆盖率。
  • R在尝试验证这个挑战之前,至少运行一次测试类(通过“全部运行”测试开发者控制台)。

Apex集成概述

学习目标

完成后,您将能够:

  • 描述Web服务和HTTP标注之间的差异。
  • 使用远程站点设置授权外部站点。

从Apex向外部服务进行调用

Apex 外部调用使您能够将Apex代码与外部服务紧密集成。调用外部Web服务或从Apex代码发送HTTP请求,然后接收响应。

Apex标注有两种方式。

  • SOAP Web服务的Web服务调用使用XML,通常需要WSDL文档来生成代码。
  • 服务的HTTP标注通常使用REST和JSON。

这两种类型的标注在向服务发送请求和接收响应方面是相似的。但是,虽然基于WSDL的标注适用于SOAP Web服务,但HTTP标注可以与任何HTTP服务(SOAP或REST)一起使用。

所以你现在可能正在问自己:“我该用哪一个?”尽可能使用HTTP服务。这些服务通常更容易交互,需要更少的代码,并使用易读的JSON。在过去的几年中,所有“cool kids”都已经转向REST服务,但这并不是说SOAP Web服务是不好的。他们已经永远(在互联网时代),通常用于企业应用程序。他们不会很快离开。当与传统应用程序集成或需要正式交换格式或有状态操作的事务时,您可能主要使用SOAP。在这个模块中,我们将介绍SOAP,但是我们将大部分时间都花在REST上。

授权端点地址

我们热爱Salesforce的安全!所以,任何时候您都可以向外部网站发送宣传信息,我们希望确保其已获得授权。未经事先批准,我们不能将代码无条件地发送给任何端点。在开始使用标注之前,请在“远程站点设置”页面上更新您的组织的已批准站点的列表。
我们将在这个模块中使用以下端点,现在继续添加它们。如果您忘记添加终端,请相信我,当您尝试运行您的代码时,您会收到提醒。我们会调用以下网站。
  • https://th-apex-http-callout.herokuapp.com
  • https://th-apex-soap-service.herokuapp.com

按照以下步骤授权这两个端点URL。

  1. 从“设置”中,在“快速查找”框中输入Remote Site Settings,然后单击 Remote Site Settings.
  2. 单击New Remote Site.
  3. 对于远程站点名称,输入 animals_http.
  4. 对于远程站点URL,请输入 https://th-apex-http-callout.herokuapp.com. 该URL授权端点的所有子文件夹,如https://th-apex-http-callout.herokuapp.com/path1https://th-apex-http-callout.herokuapp.com/path2.
  5. 对于描述,请输入 Trailhead animal service: HTTP.
  6. 点击 Save & New.
  7. 对于第二个远程站点名称,输入 animals_soap.
  8. 对于远程站点URL,请输入 https://th-apex-soap-service.herokuapp.com.
  9. 有关说明,请输入 Trailhead animal service: SOAP.
  10. 点击Save.

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,那么该记录应具有在插入或更新时匹配的运输邮政编码。