Apex 异步(5)schedule

学习目标

完成这个单位后,你会知道:

  • 何时使用预定的Apex。
  • 如何监控计划的工作。
  • 计划的Apex语法。
  • 计划的方法最佳实践。

计划的Apex

Apex Scheduler让您延迟执行,以便您可以在指定的时间运行Apex类。对于使用Batch Apex的每日或每周维护任务来说,这是理想之选。要利用调度程序,编写一个实现Schedulable接口的Apex类,然后按照特定的调度安排它执行。

计划的Apex 语法

为了调用Apex类在特定时间运行,首先实现类的Schedulable接口。然后,使用System.schedule方法安排类的实例在特定时间运行。

global class SomeClass implements Schedulable {
    global void execute(SchedulableContext ctx) {
        // 真棒代码在这里
    }
}

该类实现了Schedulable接口,并且必须实现此接口包含的唯一方法,即execute方法。

这个方法的参数是一个SchedulableContext对象。在课程安排好之后,将创建一个CronTrigger对象来表示计划的作业。它提供了一个getTriggerId方法,该方法返回一个CronTrigger API对象的ID。

示例代码

这个类查询当前应该关闭的开放机会,并创建一个任务来提醒所有者更新机会。

global class RemindOpptyOwners implements Schedulable {

    global void execute(SchedulableContext ctx) {
        List<Opportunity> opptys = [SELECT Id, Name, OwnerId, CloseDate 
            FROM Opportunity 
            WHERE IsClosed = False AND 
            CloseDate < TODAY];
        // 为列表中的每个机会创建一个任务
        TaskUtils.remindOwners(opptys);
    }
    
}
您可以安排您的课程以编程方式或从Apex调度程序用户界面运行。

使用System.Schedule方法

在使用Schedulable接口实现类之后,使用System.Schedule方法执行它。 System.Schedule方法使用用户的时区作为所有调度的基础,但是以系统模式运行 – 所有类都被执行,无论用户是否有执行类的权限。

注意

如果您计划通过触发器安排课程,请特别小心。您必须能够保证触发器不会添加比限制更多的计划作业分类。特别是,考虑API批量更新,导入向导,通过用户界面进行的大容量记录更改以及一次可以更新多个记录的所有情况。

System.Schedule方法有三个参数:作业的名称,用于表示作业计划运行的时间和日期的CRON表达式以及类的名称。

RemindOpptyOwners reminder = new RemindOpptyOwners();
// 秒分钟小时数Day_of_month Month Day_of_week optional_year
String sch = '20 30 8 10 2 ?';
String jobID = System.schedule('Remind Opp Owners', sch, reminder);
有关用于计划的CRON表达式的更多信息,请参阅Apex Scheduler中的“使用System.Schedule方法”部分。


从UI调度作业

您也可以使用用户界面安排课程。

  1. 从安装程序中,在快速查找框中输入Apex,然后选择 Apex Classes.
  2. 点击 Schedule Apex.
  3. 对于工作名称,请输入类似Daily Oppty Reminder的内容。
  4. 单击Apex类旁边的查找按钮,然后为搜索项输入*以获取所有可以调度的类的列表。在搜索结果中,单击您的计划课程的名称。
  5. 为频率选择每周或每月,并设置所需的频率。
  6. 选择开始和结束日期,以及首选开始时间。
  7. 点击 Save.

测试计划的 Apex

就像我们到目前为止所介绍的其他异步方法一样,对于Scheduled Apex,您还必须确保计划的作业在测试结果之前完成。为此,请在System.schedule方法周围再次使用startTest和stopTest,以确保在继续测试之前完成处理。

@isTest
private class RemindOppyOwnersTest {

    // CRON表情:3月15日午夜。
    // 因为这是一个测试,所以在Test.stopTest()之后立即执行作业。
    public static String CRON_EXP = '0 0 0 15 3 ? 2022';

    static testmethod void testScheduledJob() {

        // 创建一些过时的机会记录
        List<Opportunity> opptys = new List<Opportunity>();
        Date closeDate = Date.today().addDays(-7);
        for (Integer i=0; i<10; i++) {
            Opportunity o = new Opportunity(
                Name = 'Opportunity ' + i,
                CloseDate = closeDate,
                StageName = 'Prospecting'
            );
            opptys.add(o);
        }
        insert opptys;
        
        // 获取我们刚刚插入的机会的ID
        Map<Id, Opportunity> opptyMap = new Map<Id, Opportunity>(opptys);
        List<Id> opptyIds = new List<Id>(opptyMap.keySet());

        Test.startTest();
        // 安排测试工作
        String jobId = System.schedule('ScheduledApexTest',
            CRON_EXP, 
            new RemindOpptyOwners());         
        // 验证计划作业尚未运行。
        List<Task> lt = [SELECT Id 
            FROM Task 
            WHERE WhatId IN :opptyIds];
        System.assertEquals(0, lt.size(), 'Tasks exist before job has run');
        // 停止测试将同步运行作业
        Test.stopTest();
        
        // 现在计划的作业已经执行,
        // 检查我们的任务是否被创建
        lt = [SELECT Id 
            FROM Task 
            WHERE WhatId IN :opptyIds];
        System.assertEquals(opptyIds.size(), 
            lt.size(), 
            'Tasks were not created');

    }
}

要记住的事情

计划的Apex包含许多需要注意的项目(有关详细信息,请参阅参考资料部分中的Apex Scheduler),但一般情况下:
  • 一次只能有100个预定的Apex作业,每24小时有最多的预定Apex执行次数。有关详细信息,请参阅参考资料部分中的执行调控器和限制。
  • 如果您计划通过触发器安排课程,请特别小心。您必须能够保证触发器不会添加比限制更多的计划作业。
  • 计划的Apex不支持同步Web服务标注。为了能够进行标注,通过将标注放在用@future(callout = true)注释的方法中进行异步标注,并从预定的Apex中调用此方法。但是,如果预定的Apex执行批处理作业,则批处理类将支持标注。

Apex 异步(4)enqueue

学习目标

完成这个单位后,你会知道:

  • 何时使用Queueable接口。
  • 排队和未来方法之间的区别。
  • 可排队的Apex语法。
  • 可排队方法的最佳实践。

Queueable Apex

Queueable Apex在15年冬季发布,基本上是未来方法的一个超集,并带有一些额外的优势。我们把未来方法的简单性和Batch Apex的力量混合在一起形成了Queueable Apex!它为您提供了平台为您序列化的类结构,简化的界面,无需启动和结束方法,甚至允许您使用不仅仅是原始参数!它由一个简单的System.enqueueJob()方法调用,该方法返回一个可以监视的作业ID。它击败切片面包手!

Queueable Apex允许您提交与未来方法类似的异步处理作业,并具有以下附加优势:

  • 非原始类型:您的Queueable类可以包含非原始数据类型的成员变量,如sObjects或自定义Apex类型。这些对象可以在作业执行时被访问。
  • 监视:通过调用System.enqueueJob方法提交作业时,该方法将返回AsyncApexJob记录的ID。您可以使用此ID来标识您的作业并监视其进度,可以通过Apex Jobs页面中的Salesforce用户界面,也可以通过从AsyncApexJob查询记录来以编程方式进行监视。
  • 链接工作:通过从正在运行的工作中启动第二份工作,可以将一份工作链接到另一份工作上。链接作业是有用的,如果你需要做一些顺序处理。

 

Queueable与Future

因为可排队的方法在功能上等同于未来的方法,所以大多数情况下你可能希望使用可排队方法而不是将来的方法。但是,这并不意味着您现在应该回头重构所有未来的方法。如果您未来的方法超出了总督限制,或者如果您认为未来的方法需要更高的限制,则可以使用“未来方法更高限制”试验增加未来方法的限制。

使用未来的方法而不是可排队的另一个原因是当你的功能有时被同步执行,有时是异步执行的。以这种方式重构一个方法要比转换成一个可排队的类容易得多。当您发现现有代码的一部分需要移动到异步执行时,这很方便。你可以简单地创建一个相似的将来的方法来包装你的同步方法,就像这样:

@future
static void myFutureMethod(List<String> params) {
    // 调用同步方法
    mySyncMethod(params);
}
Queueable 语法

To use Queueable Apex, simply implement the Queueable interface.

public class SomeClass implements Queueable { 
    public void execute(QueueableContext context) {
        // 真棒代码在这里
    }
}

示例代码

一个常见的情况是采用一些sObject记录集,执行一些处理,例如向外部REST端点发出调用,或执行一些计算,然后异步地在数据库中更新它们。由于@future方法仅限于原始数据类型(或数组或基元集合),所以可排队的Apex是理想的选择。下面的代码采取帐户记录的集合,为每个记录设置parentId,然后更新数据库中的记录。

public class UpdateParentAccount implements Queueable {
    
    private List<Account> accounts;
    private ID parent;
    
    public UpdateParentAccount(List<Account> records, ID id) {
        this.accounts = records;
        this.parent = id;
    }

    public void execute(QueueableContext context) {
        for (Account account : accounts) {
          account.parentId = parent;
          // 执行其他处理或标注
        }
        update accounts;
    }
    
}
要将此类添加为队列中的作业,请执行以下代码:
// 找到'NY'的所有帐户
List<Account> accounts = [select id from account where billingstate = ‘NY’];
// 为所有记录找到一个特定的父帐户
Id parentId = [select id from account where name = 'ACME Corp'][0].Id;

// 实例化一个Queueable类的新实例
UpdateParentAccount updateJob = new UpdateParentAccount(accounts, parentId);

// 排队处理作业
ID jobID = System.enqueueJob(updateJob);

提交可执行的队列类之后,作业将被添加到队列中,并在系统资源可用时进行处理。

您可以使用新的作业ID来监视进度,无论是通过Apex Jobs页面还是以编程方式查询AsyncApexJob:

SELECT Id, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobID

测试Queueable Apex

以下代码示例演示如何在测试方法中测试可排队作业的执行情况。它看起来非常类似于Batch Apex测试。为了确保可排队的进程在测试方法内运行,作业被提交到Test.startTest和Test.stopTest块之间的队列中。在Test.stopTest语句之后,系统同步执行在测试方法中启动的所有异步进程。接下来,测试方法通过查询作业更新的帐户记录来验证可排队作业的结果。

@isTest
public class UpdateParentAccountTest {

    @testSetup 
    static void setup() {
        List<Account> accounts = new List<Account>();
        // 添加一个父帐户
        accounts.add(new Account(name='Parent'));
        // 添加100个子帐户
        for (Integer i = 0; i < 100; i++) {
            accounts.add(new Account(
                name='Test Account'+i
            ));
        }
        insert accounts;
    }
    
    static testmethod void testQueueable() {
        // 查询测试数据传递给队列类
        Id parentId = [select id from account where name = 'Parent'][0].Id;
        List<Account> accounts = [select id, name from account where name like 'Test Account%'];
        // 创建我们的Queueable实例
        UpdateParentAccount updater = new UpdateParentAccount(accounts, parentId);
        // startTest / stopTest block强制异步进程运行
        Test.startTest();        
        System.enqueueJob(updater);
        Test.stopTest();        
        // 验证作业运行。检查记录现在是否有正确的parentId
        System.assertEquals(100, [select count() from account where parentId = :parentId]);
    }
    
}

 

Chaining Jobs

Queueable Apex的最大特点之一就是职位链。如果您需要连续运行作业,Queueable Apex可以让您的生活更轻松。要将作业链接到其他作业,请从可排队类的execute()方法提交第二个作业。您可以从正在执行的作业中仅添加一个作业,这意味着每个父作业只能存在一个子作业。例如,如果您有另一个名为SecondJob的类实现Queueable接口,则可以将此类添加到execute()方法中的队列中,如下所示:

public class FirstJob implements Queueable { 
    public void execute(QueueableContext context) { 
        //真棒处理逻辑在这里通过提交下一份工作把这份工作链接到下一份工作
        System.enqueueJob(new SecondJob());
    }
}

再次,测试有一个稍微不同的模式。您不能在Apex测试中链接可排队的作业,这样做会导致错误。为了避免令人讨厌的错误,您可以通过在链接作业之前调用Test.isRunningTest()来检查Apex是否在测试环境中运行。

 

要记住的事情

Queueable Apex是一个伟大的新工具,但有几件事值得注意:

  • 针对异步Apex方法执行的共享限制,排队作业的执行计数一次。
  • 在一个事务中,您可以使用System.enqueueJob将最多50个作业添加到队列中。
  • 在链接作业时,只能使用System.enqueueJob从正在执行的作业中添加一个作业,这意味着每个父排队作业只能存在一个子作业。从同一个可排队的作业开始多个子作业是一个禁忌。
  • 链式作业的深度没有限制,这意味着你可以链接一个作业到另一个作业,并重复这个过程,每个新的子作业链接到一个新的子作业。但是,对于Developer Edition和Trial orgs,链接作业的最大堆栈深度为5,这意味着您可以链接作业四次,链中的最大作业数为5,包括初始父排队作业。

Apex 异步(3)批处理

学习目标

完成这个单位后,你会知道:

  • 在哪里使用Batch Apex。
  • 使用批次时Apex限制较高。
  • Batch Apex语法。
  • 批量Apex最佳实践。

Batch Apex

Batch Apex用于运行大型工作(认为数千或数百万条记录!),这将超出正常的处理限制。使用Batch Apex,您可以批量异步处理记录(因此名称为“Batch Apex”)以保持平台限制。如果您有很多记录要处理,例如数据清理或归档,Batch Apex可能是您最好的解决方案。

Batch Apex的工作原理如下。假设您想使用Batch Apex处理100万条记录。对于您正在处理的每批记录,批处理类的执行逻辑被调用一次。每次调用批次类时,作业将被放置在Apex作业队列中,并作为离散事务执行。这个功能有两个好处:

  • 每笔交易都以一组新的限额开始,更容易确保您的代码保持在限额执行范围内。
  • 如果一个批处理失败,则所有其他成功的批处理事务都不会回滚。

Batch Apex语法

要编写批量Apex类,您的类必须实现Database.Batchable接口并包含以下三种方法:

start
用于收集要传递给接口方法的记录或对象执行处理。此方法在Batch Apex作业开始时调用一次,并返回一个Database.QueryLocator对象或一个包含传递给作业的记录或对象的Iterable。

大多数情况下,QueryLocator通过简单的SOQL查询来生成批量作业中的对象范围。但是,如果您需要做一些疯狂的事情,比如在传递给execute方法之前循环调用API的结果或预处理记录,则可能需要查看参考资料部分中的自定义迭代器链接。

使用QueryLocator对象,SOQL查询检索的记录总数限制被绕过,并且您可以查询多达5000万条记录。但是,对于Iterable,由SOQL查询检索的记录总数的限制仍然是强制执行的。

execute

对传递给方法的每个块或“批量”数据执行实际处理。默认的批量大小是200条记录。不保证记录批按从启动方法收到的顺序执行。

该方法采取以下措施:

  • 对Database.BatchableContext对象的引用。
  • sObject的列表,例如List <sObject>或者参数化类型列表。如果您正在使用Database.QueryLocator,请使用返回的列表。
finish
用于执行后处理操作(例如,发送电子邮件)并在所有批处理完成后调用一次。

以下是Batch Apex类的框架:

global class MyBatchClass implements Database.Batchable<sObject> {

    global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
        // 收集要传递的记录或对象的批处理执行
    }

    global void execute(Database.BatchableContext bc, List<P> records){
        // 处理每批记录
    }    

    global void finish(Database.BatchableContext bc){
        // 执行任何后期处理操作
    }    

}

调用批处理类

要调用批处理类,只需实例化它,然后调用Database.executeBatch与实例:

MyBatchClass myBatchObject = new MyBatchClass(); 
Id batchId = Database.executeBatch(myBatchObject);
您还可以选择传递第二个作用域参数,以指定应该传递到每个批处理的执行方法的记录数。专家提示:如果您遇到管理员限制,您可能需要限制此批量。
Id batchId = Database.executeBatch(myBatchObject, 100);
每个批次的Apex调用都将创建一个AsyncApexJob记录,以便您可以跟踪作业的进度。您可以通过SOQL查看进度或在Apex作业队列中管理作业。我们马上谈谈Job Queue。
AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors FROM AsyncApexJob WHERE ID = :batchId ];
Batch Apex使用状态

Batch Apex通常是无状态的。批量Apex作业的每次执行都被视为离散事务。例如,包含1,000条记录并使用默认批量大小的批量Apex作业被视为每个200条记录的五个事务。

如果在类定义中指定Database.Stateful,则可以在所有事务中维护状态。当使用Database.Stateful时,只有实例成员变量在事务之间保留它们的值。维护状态对于计算或汇总处理记录非常有用。在下一个示例中,我们将更新批处理作业中的联系人记录,并要跟踪受影响的总记录,以便我们可以将其包含在通知电子邮件中。

Batch Apex例子代码

现在你已经知道如何编写一个Batch Apex类了,我们来看一个实际的例子。假设您有一项业务要求,即美国公司的所有联系人都必须将其母公司的帐单邮寄地址作为邮寄地址。不幸的是,用户输入的新联系人没有正确的地址!用户不会学习?!编写一个批处理Apex类,确保这个要求被强制执行。

以下示例类查找使用QueryLocator由start()方法传入的所有帐户记录,并使用其帐户的邮寄地址更新关联的联系人。最后,它发送一个包含批量作业结果的电子邮件,因为我们使用Database.Stateful来跟踪状态,所以更新记录的数量。

global class UpdateContactAddresses implements 
    Database.Batchable<sObject>, Database.Stateful {
    
    // 实例成员在事务中保持状态
    global Integer recordsProcessed = 0;

    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT ID, BillingStreet, BillingCity, BillingState, ' +
            'BillingPostalCode, (SELECT ID, MailingStreet, MailingCity, ' +
            'MailingState, MailingPostalCode FROM Contacts) FROM Account ' + 
            'Where BillingCountry = \'USA\''
        );
    }

    global void execute(Database.BatchableContext bc, List<Account> scope){
        // 处理每批记录
        List<Contact> contacts = new List<Contact>();
        for (Account account : scope) {
            for (Contact contact : account.contacts) {
                contact.MailingStreet = account.BillingStreet;
                contact.MailingCity = account.BillingCity;
                contact.MailingState = account.BillingState;
                contact.MailingPostalCode = account.BillingPostalCode;
                // 添加联系人列表进行更新
                contacts.add(contact);
                // 增加实例成员计数器
                recordsProcessed = recordsProcessed + 1;
            }
        }
        update contacts;
    }    

    global void finish(Database.BatchableContext bc){
        System.debug(recordsProcessed + ' records processed. Shazam!');
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, 
            JobItemsProcessed,
            TotalJobItems, CreatedBy.Email
            FROM AsyncApexJob
            WHERE Id = :bc.getJobId()];
        // 调用一些实用程序发送电子邮件
        EmailUtils.sendMessage(a, recordsProcessed);
    }    

}
代码应该是相当直接的,但在现实中可能会有点抽象。以下是更详细的内容:
  • start方法提供了执行方法将在单个批处理中处理的所有记录的集合。它通过使用SOQL查询调用Database.getQueryLocator来返回要处理的记录列表。在这种情况下,我们只需查询“美国”的帐单国家/地区的所有帐户记录。
  • 每个批次的200条记录都在执行方法的第二个参数中传递。 execute方法将每个联系人的邮寄地址设置为帐户的帐单地址,并增加recordsProcessed以跟踪处理的记录数。
  • 作业完成后,完成方法将在AsyncApexJob对象(列出有关批处理作业的信息的表)上执行查询,以获取作业的状态,提交者的电子邮件地址以及其他信息。然后它会发送一封通知邮件给作业提交者,其中包括工作信息和联系人数量的更新。

测试Batch Apex

由于Apex开发和测试齐头并进,所以我们如何测试上述批次类。简而言之,我们插入一些记录,调用Batch Apex类,然后用正确的地址声明记录已经正确更新。

@isTest
private class UpdateContactAddressesTest {

    @testSetup 
    static void setup() {
        List<Account> accounts = new List<Account>();
        List<Contact> contacts = new List<Contact>();
        // 插入10个帐户
        for (Integer i=0;i<10;i++) {
            accounts.add(new Account(name='Account '+i, 
                billingcity='New York', billingcountry='USA'));
        }
        insert accounts;
        // 找到刚插入的帐号。为每个添加联系人
        for (Account account : [select id from account]) {
            contacts.add(new Contact(firstname='first', 
                lastname='last', accountId=account.id));
        }
        insert contacts;
    }

    static testmethod void test() {        
        Test.startTest();
        UpdateContactAddresses uca = new UpdateContactAddresses();
        Id batchId = Database.executeBatch(uca);
        Test.stopTest();

        // 在测试停止之后,声明记录被正确更新
        System.assertEquals(10, [select count() from contact where MailingCity = 'New York']);
    }
    
}
设置方法插入10个帐户记录,开票城市为“纽约”,开票国家为“美国”。然后为每个帐户创建一个关联的联系人记录。该数据由批次类使用。

注意

确保插入的记录数小于批量大小200,因为测试方法只能执行一个批次总数。

在测试方法中,UpdateContactAddresses批次类被实例化,通过调用Database.executeBatch并将其传递给批次类的实例来调用。

对Database.executeBatch的调用包含在Test.startTest和Test.stopTest块中。这就是所有魔法发生的地方。在调用Test.stopTest之后执行作业。包含在Test.startTest和Test.stopTest中的任何异步代码都是在Test.stopTest之后同步执行的。

最后,测试通过检查结算城市“纽约”的联系人记录数量与插入的记录数量(即10)相匹配来验证所有联系人记录已被正确更新。

最佳实践

与未来的方法一样,在使用Batch Apex时,您还是要记住一些事情。为确保快速执行批处理作业,请尽量减少Web服务调出时间并调整批处理Apex代码中使用的查询。批处理作业执行的时间越长,当许多作业在队列中时,其他排队的作业就越有可能被延迟。最佳实践包括:

  • 如果您有多个批次的记录,请仅使用Batch Apex。如果您没有足够的记录来运行多个批次,则最好使用Queueable Apex。
  • 调整任何SOQL查询以尽可能快地收集要执行的记录。
  • 尽量减少创建的异步请求的数量,以尽量减少延迟的机会。
  • 如果您打算从触发器调用批处理作业,请特别小心。您必须能够保证触发器不会添加比限制更多的批处理作业。

Apex 异步(2)使用future

学习目标

完成这个单位后,你会知道:

  • 何时使用未来的方法。
  • 使用未来方法的局限性。
  • 如何使用未来的标注方法。
  • 未来方法的最佳实践。

Future Apex

未来的Apex用于在单独的线程中运行进程,稍后系统资源可用。

注意:从技术上讲,您使用@future注释来标识异步运行的方法。但是,由于“使用@future注解标识的方法”很费力,通常被称为“将来的方法”,所以我们将在本模块的其余部分中引用它们。

在使用同步处理时,所有的方法调用都是由执行Apex代码的同一个线程来完成的,在这个过程完成之前不会进行额外的处理。你可以使用未来的方法进行任何你想在自己的线程中异步运行的操作。这提供了不阻止用户执行其他操作并为该过程提供更高调控器和执行限制的好处。每个人都是异步处理的赢家。

未来的方法通常用于:

  • 标注到外部Web服务。如果从触发器或执行DML操作之后进行标注,则必须使用将来的或可排队的方法。触发器中的标注将保持数据库连接在标注的生命周期中处于打开状态,这是多租户环境中的“否定”。
  • 如果时间允许,例如某种资源密集型计算或记录处理,您希望在自己的线程中运行的操作。
  • 隔离不同sObject类型的DML操作以防止混合的DML错误。这是一个边缘情况,但你偶尔会遇到这个问题。有关更多详细信息,请参阅无法在DML操作中一起使用的sObjects。

Future方法语法

Future的方法必须是静态方法,并且只能返回一个void类型。指定的参数必须是原始数据类型,原始数据类型数组或原始数据类型的集合。值得注意的是,未来的方法不能将标准或自定义对象作为参数。一个常见的模式是传递一个你想异步处理的记录ID列表。

global class SomeClass {
  @future
  public static void someFutureMethod(List<Id> recordIds) {
    List<Account> accounts = [Select Id, Name from Account Where Id IN :recordIds];
    // 处理帐户记录做真棒的东西
  }
}

注意

对象不能作为参数传递给未来方法的原因是因为对象可以在调用方法的时间和实际执行的时间之间改变。请记住,未来的方法在系统资源可用时执行。在这种情况下,未来的方法在实际执行时可能会有一个旧的对象值,这会导致各种不好的事情发生。

重要的是要注意,未来的方法不能保证按照所调用的顺序执行。同样,未来的方法也不能保证按照它们被调用的顺序执行。如果您需要这种类型的功能,那么Queueable Apex可能是更好的解决方案。当使用未来的方法时,也可能会同时运行两个未来的方法,如果两个方法更新相同的记录,可能会导致记录锁定和令人讨厌的运行时错误。

示例标注代码

要为外部服务或API创建Web服务标注,可以使用标记为(callout = true)的未来方法创建Apex类。下面的类具有在不允许标注的情况下同步和异步调用标注的方法。我们在自定义日志对象中插入一条记录来跟踪标注的状态,仅仅是因为日志记录总是很有趣!

public class SMSUtils {

    // 从触发器中调用异步,等等,不允许标注。
    @future(callout=true)
    public static void sendSMSAsync(String fromNbr, String toNbr, String m) {
        String results = sendSMS(fromNbr, toNbr, m);
        System.debug(results);
    }

    // 从控制器等呼叫立即处理
    public static String sendSMS(String fromNbr, String toNbr, String m) {
        // 调用“发送”将会产生一个标注
        String results = SmsMessage.send(fromNbr, toNbr, m);
        insert new SMS_Log__c(to__c=toNbr, from__c=fromNbr, msg__c=results);
        return results;
    }

}

测试类

测试未来的方法与典型的Apex测试有点不同。要测试将来的方法,请将测试代码放在startTest和stopTest测试方法之间。系统收集startTest之后所做的所有异步调用。当执行stopTest时,所有这些收集的异步进程将同步运行。然后您可以断言异步调用正常运行。

注意

测试代码实际上无法将标注发送到外部系统,因此您必须“嘲笑”测试覆盖的标注。查看Apex集成服务模块,获取有关模拟测试标注的完整详细信息。

这是我们用于测试的模拟标注类。 Apex测试框架利用这个“模拟”响应,而不是实际调出REST API端点。

@isTest
global class SMSCalloutMock implements HttpCalloutMock {
    global HttpResponse respond(HttpRequest req) {
        // 创建一个假的回应
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('{"status":"success"}');
        res.setStatusCode(200);
        return res; 
    }
}
测试类包含一个测试方法,它测试异步和同步方法,因为前者调用后者。
@IsTest
private class Test_SMSUtils {

  @IsTest
  private static void testSendSms() {
    Test.setMock(HttpCalloutMock.class, new SMSCalloutMock());
    Test.startTest();
      SMSUtils.sendSMSAsync('111', '222', 'Greetings!');
    Test.stopTest();
    // 运行标注并检查结果
    List<SMS_Log__c> logs = [select msg__c from SMS_Log__c];
    System.assertEquals(1, logs.size());
    System.assertEquals('success', logs[0].msg__c);
  }

}

最佳实践

由于每个将来的方法调用都会向异步队列添加一个请求,因此避免在短时间内添加大量未来请求的设计模式。如果您的设计有可能一次添加2000个或更多请求,则由于流量控制,请求可能会延迟。以下是您想要记住的一些最佳做法:

  • 确保将来的方法尽可能快地执行。
  • 如果使用Web服务标注,请尝试将所有标注捆绑在一起,而不是为每个标注使用单独的未来方法。
  • 大规模进行彻底的测试。测试触发排队@future调用的触发器能够处理200条记录的触发器集合。这有助于确定在目前和未来的设计中是否会出现延迟。
  • 考虑使用Batch Apex而不是将来的方法来异步处理大量的记录。这比为每条记录创建未来请求更有效率。

要记住的事情

未来的方法是一个伟大的工具,但拥有巨大的权力是很大的责任。在使用它们时,请注意以下几点:

  • 使用未来注解的方法必须是静态方法,并且只能返回一个void类型。
  • 指定的参数必须是原始数据类型,原始数据类型数组或原始数据类型的集合;未来的方法不能把对象当作论据。
  • 未来的方法将不一定按照它们被调用的顺序执行。另外,有可能两个未来的方法可能同时运行,如果这两个方法正在更新相同的记录,则可能导致记录锁定。
  • 未来的方法不能用于getMethodName(),setMethodName()中的Visualforce控制器中,也不能用于构造器中。
  • 你不能从将来的方法调用未来的方法。在运行未来的方法时,你也不能调用一个调用未来方法的触发器。请参阅参考资料中的链接以防止递归未来方法调用。
  • getContent()和getContentAsPDF()方法不能用于将来注释的方法。
  • 您每个Apex调用将被限制为50个未来呼叫,并且在24小时内对呼叫数量有额外的限制。有关限制的更多信息,请参阅下面的链接。

Apex 异步(1)

学习目标

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

  • 解释同步和异步处理之间的区别。
  • 选择在各种情况下使用哪种异步Apex。

异步Apex

简而言之,异步Apex用于在稍后的时间在单独的线程中运行进程。

异步进程是一个进程或函数,在用户不必等待任务完成的情况下,在后台执行任务。

这是一个真实世界的例子。比方说,在每周的跳舞革命练习之前,你有一份要完成的事情。你的车发出一个有趣的噪音,你需要一个不同颜色的发胶,你必须从你妈妈的房子拿起你的制服。你可以把你的汽车交给机修工,在完成清单的其余部分(同步处理)之前先等你的汽车修理好,或者你可以把它留在那里,完成其他事情,并且在修理之前让商店给你打电话(异步处理)。如果你想在练习之前及时回家锻炼你的斯潘德克斯,异步处理可以让你在相同的时间内完成更多的工作,而不需要等待。

您通常会使用Asynchronous Apex调出外部系统,需要更高限制的操作以及需要在特定时间运行的代码。异步处理的主要好处包括:

用户效率

假设有一个进程在创建一个机会时对一个自定义对象进行许多计算。执行这些计算所需的时间范围可以从轻微的烦恼到用户的生产力封锁。由于这些计算不会影响用户目前正在做的事情,所以让他们等待长时间运行的过程并不是有效利用他们的时间。通过异步处理,用户可以继续工作,处理可以在后台完成,用户可以方便地看到结果。

可扩展性

通过在将来某个时间点资源可用时允许平台的某些功能执行,可以快速管理和缩放资源。这允许平台使用并行处理来处理更多作业。

更高的限制
异步进程在新线程中启动,具有更高的调控和执行限制。说实话,不是每个人都想要更高的州长和执行限制吗?

异步Apex有许多不同的风格。我们稍后会详细介绍每一个细节,但这里有一个高层次的概述。

类型 概观 常见的情况
未来的方法 在自己的线程中运行,并且在资源可用之前不要启动。 Web服务标注。
批次Apex 运行超过正常处理限制的大型作业。 数据清理或存档记录。
可排队的 Apex 与未来的方法类似,但提供额外的作业链,并允许使用更复杂的数据类型。 使用外部Web服务执行顺序处理操作。
计划的 Apex 安排Apex在指定的时间运行。 每日或每周的任务。

还值得注意的是,这些不同类型的异步操作不是相互排斥的。例如,一个常见的模式是启动预定Apex作业中的批量Apex作业。

增加总督和执行限制

运行异步Apex的主要好处之一是更高的调控和执行限制。例如,使用异步调用时,SOQL查询的数量从100到200个查询翻倍。异步调用的总堆大小和最大CPU时间也相似。

不仅可以通过异步获得更高的限制,而且这些控制器限制独立于最初排队异步请求的同步请求的限制。这是一口,但实质上,你有两个单独的Apex调用,并且是超过两倍的处理能力。例如,当您想在当前事务中尽可能多地执行处理时,当您开始接近管理员限制时,可以非常方便地继续操作。

如果您喜欢阅读关于堆大小,最大执行时间和总体限制的信息,请参阅执行调控器和限制以获取令人信服的详细信息。

异步处理如何工作

异步处理在多租户环境中存在一些挑战:

确保处理的公正性

确保每个客户获得公平的处理资源份额。

确保容错
确保没有异步请求由于设备或软件故障而丢失。

该平台使用基于队列的异步处理框架。此框架用于管理每个实例中的多个组织的异步请求。请求生命周期由三部分组成:

入队

请求被放入队列中。这可能是Apex批量请求,未来的Apex请求或其他许多请求中的一个。该平台将排队请求以及相应的数据来处理该请求。

坚持

排队的请求被持久化。请求被存储在永久性存储器中以用于故障恢复并提供事务处理能力。

出列
已排队的请求将从队列中移除并进行处理。事务管理在这一步发生,以确保消息不会丢失,如果有处理失败。

每个请求都由一个处理程序处理。处理程序是执行特定请求类型的函数的代码。处理程序由构成实例的每个应用程序服务器上的有限数量的工作线程执行。线程从排队框架中请求工作,并在收到时启动特定的处理程序来完成工作。

资源保护

异步处理的优先级低于通过浏览器和API进行的实时交互。为了确保有足够的资源来处理增加的计算资源,排队框架监视系统资源(如服务器内存和CPU使用率),并在超过阈值时减少异步处理。这是说多租户系统保护自己的一种奇特的方式。如果一个组织尝试“吞噬”超过其资源份额,则异步处理将暂停,直到达到正常阈值。不管是处理时间还是没有保证,最终都会结束。

Salesforce API(9)Web服务

学习目标

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

  • 描述两种类型的Apex Web服务并提供这些服务的高级概述。
  • 创建一个包含每个HTTP方法的方法的Apex REST类。
  • 使用端点调用自定义的Apex REST方法。
  • 通过以JSON格式发送请求主体,将数据传递给自定义的Apex REST方法。
  • 编写Apex REST方法的测试方法,并在测试REST请求中设置属性。
  • 通过调用具有参数值的方法编写Apex REST方法的测试方法。

将Apex类公开为Web服务

您可以将Apex类方法公开为REST或SOAP Web服务操作。通过使您的方法可以通过Web进行调用,您的外部应用程序可以与Salesforce集成以执行各种漂亮的操作。
例如,假设贵公司的呼叫中心正在使用内部应用程序来管理本地资源。预计客户支持代表将使用相同的应用程序来执行其日常工作,包括管理Salesforce中的案例记录。通过使用一个接口,代表可以查看和更新​​案例记录并访问内部资源。该应用程序调用Apex Web服务类来管理Salesforce案例记录。

公开一个类作为REST服务

使您的Apex类可用作REST Web服务非常简单。定义您的类为全局,并将方法定义为全局静态。为类和方法添加注释。例如,此示例Apex REST类使用一种方法。 getRecord方法是一个自定义的REST API调用。它使用@HttpGet进行注释,并被GET请求调用。

@RestResource(urlMapping='/Account/*')
global with sharing class MyRestResource {
    @HttpGet
    global static Account getRecord() {
        // 添加你的代码
    }
}
正如你所看到的,这个类用@RestResource注解(urlMapping =’/ Account / *)。 Apex REST的基本端点是https://yourInstance.salesforce.com/services/apexrest/。 URL映射附加到基本端点以形成REST服务的端点。例如,在类示例中,REST端点是https://yourInstance.salesforce.com/services/apexrest/Account/。对于您的组织,它可能看起来像https://yourInstance.salesforce.com/services/apexrest/Account/。

URL映射区分大小写,可以包含通配符(*)。

将每个暴露的方法定义为全局静态,并添加注释以将其与HTTP方法相关联。以下注释可用。每个Apex类中只能使用一次每个注释。

注解 行动 细节
@HttpGet Read 读取或检索记录。
@HttpPost Create 创建记录。
@HttpDelete Delete 删除记录。
@HttpPut Upsert 通常用于更新现有记录或创建记录。
@HttpPatch Update 通常用于更新现有记录中的字段。

公开一个类作为SOAP服务

使您的Apex类作为SOAP Web服务可用,就像使用REST一样简单。将您的课程定义为全球课程。将webservice关键字和静态定义修饰符添加到您要公开的每个方法。 webservice关键字提供对其添加方法的全局访问权限。

例如,这里有一个方法的示例类。 getRecord方法是一个定制的SOAP API调用,它返回一个Account记录。

global with sharing class MySOAPWebService {
    webservice static Account getRecord(String id) {
        // 添加你的代码
    }
}

外部应用程序可以通过使用类WSDL文件来调用您的自定义Apex方法作为Web服务操作。从类详细信息页面为您的类生成此WSDL,可从“安装程序”中的“Apex类”页面进行访问。通常,您可以将WSDL文件发送给第三方开发人员(或者自己使用)来编写Web服务的集成。

由于平台安全性是一流的Salesforce公民,因此您的Web服务需要身份验证。除了Apex类WSDL外,外部应用程序还必须使用Enterprise WSDL或Partner WSDL作为登录功能。

Apex REST演练

现在有趣的东西。接下来的几个步骤将引导您完成构建Apex REST服务的过程。首先,创建作为REST服务公开的Apex类。然后你尝试从客户端调用几个方法,最后编写单元测试。有相当多的代码,但这将是值得的努力!
您的Apex课程管理案例记录。该类包含五个方法,每个方法对应一个HTTP方法。例如,当客户端应用程序调用GET HTTP方法的REST调用时,将调用getCaseById方法。

由于该类是使用/ Cases / *的URL映射定义的,因此用于调用此REST服务的端点是以https://yourInstance.salesforce.com/services/apexrest/Cases/开头的任何URI。

我们建议您也考虑对API端点进行版本控制,以便在不破坏现有代码的情况下提供功能升级。您可以创建两个指定 /Cases/v1/*/Cases/v2/* 的URL映射的类来实现此功能。

让我们开始创建一个Apex REST类。

  1. 从设置档(打开设备齿轮图标Setup gear icon)打开开发者控制台。
  2. 在开发者控制台中,选择 File | New | Apex Class.
  3. 对于课程名称,输入CaseManager,然后单击 OK.
  4. 将自动生成的代码替换为以下类定义。
    @RestResource(urlMapping='/Cases/*')
    global with sharing class CaseManager {
    
        @HttpGet
        global static Case getCaseById() {
            RestRequest request = RestContext.request;
            // 从URL的末尾抓取caseId
            String caseId = request.requestURI.substring(
              request.requestURI.lastIndexOf('/')+1);
            Case result =  [SELECT CaseNumber,Subject,Status,Origin,Priority
                            FROM Case
                            WHERE Id = :caseId];
            return result;
        }
    
        @HttpPost
        global static ID createCase(String subject, String status,
            String origin, String priority) {
            Case thisCase = new Case(
                Subject=subject,
                Status=status,
                Origin=origin,
                Priority=priority);
            insert thisCase;
            return thisCase.Id;
        }   
    
        @HttpDelete
        global static void deleteCase() {
            RestRequest request = RestContext.request;
            String caseId = request.requestURI.substring(
                request.requestURI.lastIndexOf('/')+1);
            Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
            delete thisCase;
        }     
    
        @HttpPut
        global static ID upsertCase(String subject, String status,
            String origin, String priority, String id) {
            Case thisCase = new Case(
                    Id=id,
                    Subject=subject,
                    Status=status,
                    Origin=origin,
                    Priority=priority);
            // 通过Case Id匹配,如果存在的话。
            // 否则,创建新的案例。
            upsert thisCase;
            // 返回案例ID。
            return thisCase.Id;
        }
    
        @HttpPatch
        global static ID updateCaseFields() {
            RestRequest request = RestContext.request;
            String caseId = request.requestURI.substring(
                request.requestURI.lastIndexOf('/')+1);
            Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
            // 将JSON字符串反序列化为名称 - 值对
            Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(request.requestbody.tostring());
            // 遍历每个参数字段和值
            for(String fieldName : params.keySet()) {
                // 设置Case sObject的字段和值
                thisCase.put(fieldName, params.get(fieldName));
            }
            update thisCase;
            return thisCase.Id;
        }    
    
    }
  5. 按下CTRL + S保存。

使用POST方法创建一个记录

让我们使用您刚刚创建的Apex REST类,并获得一些乐趣。首先,我们将调用POST方法来创建一个案例记录。
要调用您的REST服务,您需要使用… REST客户端!几乎可以使用任何REST客户端,例如您自己的API客户端,cURL命令行工具或PHP的curl库。我们将使用Workbench工具作为我们的REST客户端应用程序,但稍后我们将会看看cURL。

Apex REST支持两种格式来表示资源:JSON和XML。 JSON表示在请求或响应的主体中默认传递,格式由HTTP头中的Content-Type属性指示。由于JSON比XML更易于阅读和理解,因此该设备仅使用JSON。在这一步中,您将以JSON格式发送案例记录。

Apex REST支持OAuth 2.0和会话认证机制。简而言之,这意味着我们使用行业标准来保证您的应用程序和数据安全。幸运的是,您可以使用Workbench来简化测试。 Workbench是一个功能强大的基于Web的工具套件,供管理员和开发人员通过Force.com API与组织进行交互。使用Workbench,您在使用您的用户名和密码登录到Salesforce时使用会话身份验证。而您使用REST资源管理器来调用您的REST服务。

  1. 导航到 https://workbench.developerforce.com/login.php.
  2. 对于环境,请选择 Production.
  3. 从“API版本”下拉列表中选择最新的API版本。
  4. 接受服务条款,然后单击 Login with Salesforce.
  5. 要允许工作台访问您的信息,请单击 Allow.
  6. 输入您的登录凭据,然后单击 Log in to Salesforce.
  7. 登录后,选择 utilities | REST Explorer.
  8. 选择 POST.
  9. REST Explorer接受的URL路径相对于您的组织的实例URL。仅提供追加到实例URL的路径。在相对URI输入字段中,将缺省URI替换为/services/apexrest/Cases/.
  10. 对于请求主体,插入要插入的对象的以下JSON字符串表示形式。
    {
      "subject" : "Bigfoot Sighting!",
      "status" : "New",
      "origin" : "Phone",
      "priority" : "Low"
    }
  11. 点击 Execute.
    该调用调用与POST HTTP方法关联的方法,即createCase方法。
  12. 要查看返回的响应,请单击 Show Raw Response.
    返回的响应与此响应类似。该响应包含新案例记录的ID。您的ID值可能与50061000000t7kYAAQ不同。保存您的ID值以便在下一步中使用。

    HTTP/1.1 200 OK
    Date: Wed, 07 Oct 2015 14:18:20 GMT
    Set-Cookie: BrowserId=F1wxIhHPQHCXp6wrvqToXA;Path=/;Domain=.salesforce.com;Expires=Sun, 06-Dec-2015 14:18:20 GMT
    Expires: Thu, 01 Jan 1970 00:00:00 GMT
    Content-Type: application/json;charset=UTF-8
    Content-Encoding: gzip
    Transfer-Encoding: chunked
    
    "50061000000t7kYAAQ" 

使用自定义GET方法检索数据

按照以前类似的步骤,使用Workbench来调用GET HTTP方法。

  1. 在Workbench中,选择 GET.
  2. 输入URI /services/apexrest/Cases/<Record ID>,将 <Record ID> 替换为您在上一步中创建的记录的ID。
  3. 点击 Execute.

    该调用调用与GET HTTP方法关联的方法,即getCaseById方法。

  4. 要查看返回的响应,请单击 Show Raw Response.

    返回的响应与此响应类似。响应包含为新的案例记录查询方法的字段。

    HTTP/1.1 200 OK
    Date: Wed, 07 Oct 2015 14:28:20 GMT
    Set-Cookie: BrowserId=j5qAnPDdRxSu8eHGqaRVLQ;Path=/;Domain=.salesforce.com;Expires=Sun, 06-Dec-2015 14:28:20 GMT
    Expires: Thu, 01 Jan 1970 00:00:00 GMT
    Content-Type: application/json;charset=UTF-8
    Content-Encoding: gzip
    Transfer-Encoding: chunked
    
    {
      "attributes" : {
        "type" : "Case",
        "url" : "/services/data/v34.0/sobjects/Case/50061000000t7kYAAQ"
      },
      "CaseNumber" : "00001026",
      "Subject" : "Bigfoot Sighting!",
      "Status" : "New",
      "Origin" : "Phone",
      "Priority" : "Low",
      "Id" : "50061000000t7kYAAQ"
    }
    

使用cURL检索数据

每个好的开发者至少应该知道三件事:1)如何制作一个自己动画的GIF,吃自己喜欢的冰淇淋; 2)pi的值到小数点后25位; 3)如何使用cURL。前两个超出了本模块的范围,所以我们将专注于最后一个。

cURL是一个使用URL语法获取或发送文件的命令行工具。使用REST端点时,它非常方便。您可以使用cURL来调用GET HTTP方法,而不是使用Workbench作为Apex REST服务。每当您“ROCK”您的REST端点时,您传递会话ID进行授权。在Workbench中工作时,你被宠坏了,因为在你登录之后,它会在你的封面下传递会话ID。

要获取会话ID,您首先在Salesforce组织中创建一个连接的应用程序并启用OAuth。在这种情况下,您的客户端应用程序cURL使用连接的应用程序连接到Salesforce。按照这些说明创建一个连接的应用程序,为您提供消费者密钥和消费者密钥,您需要获得您的会话ID。为连接的应用程序选择OAuth范围时,请选择“访问和管理数据(api)”范围。连接的应用程序可能需要5到10分钟才能完成设置。准备就绪后,对凭证和连接的应用程序使用以下cURL命令。

curl -v https://login.salesforce.com/services/oauth2/token -d "grant_type=password" -d "client_id=<your_consumer_key>" -d "client_secret=<your_consumer_secret>" -d "username=<your_username>" -d "password=<your_password_and_security_token>" -H 'X-PrettyPrint:1'
如果全部成功,则结果包含access_token,这是您的组织的会话ID和instance_url。

cURL response with access token

现在输入您的cURL命令(将与以下内容类似),以调用您的Apex REST服务并返回案例信息。

curl https://yourInstance.salesforce.com/services/apexrest/Cases/<Record_ID> -H 'Authorization: Bearer <your_session_id>' -H 'X-PrettyPrint:1'
按Enter后,您会看到类似于以下内容的内容。现在你已经是一个命令行的主人,随心所欲地为你的内容添加cURL,jq,sed,awk和grep。有关cURL的更多信息,请参阅参考资料部分。

cURL and response from the command line

使用自定义PUT或PATCH方法更新数据

您可以使用PUT或PATCH HTTP方法更新记录。 PUT方法要么更新整个资源(如果存在的话),要么创建资源(如果它不存在)。 PUT本质上是一个upsert方法。 PATCH方法只更新现有资源的指定部分。在Apex中,更新操作只更新指定的字段,不覆盖整个记录。我们将编写一些Apex代码来确定我们的方法是更新还是上传。

使用PUT方法更新数据

您添加到CaseManager类的upsertCase方法实现了PUT操作。这里包括这个方法供您参考。该方法使用内置的upsert Apex DML方法通过匹配ID值来创建或覆盖大小写记录字段。如果在请求的主体中发送了一个ID,则会使用它填充案例sObject。否则,创建没有ID的案例sObject。 upsert方法用填充的case sObject调用,DML语句做其余部分。瞧!

@HttpPut
global static ID upsertCase(String subject, String status,
    String origin, String priority, String id) {
    Case thisCase = new Case(
        Id=id,
        Subject=subject,
        Status=status,
        Origin=origin,
        Priority=priority);
    // 通过Id匹配大小写,如果存在的话。
    // 否则,创建新的案例。
    upsert thisCase;
    // 返回案例ID。
    return thisCase.Id;
}
要调用PUT方法:

  1. 在Workbench REST Explorer中,选择 PUT.
  2. 对于URI,输入 /services/apexrest/Cases/.
  3. upsertCase方法期望字段值在请求主体中传递。为请求主体添加以下内容,然后将<Record ID>替换为您之前创建的案例记录的ID。
    {
      "id": "<Record_ID>",
      "status" : "Working",
      "subject" : "Bigfoot Sighting!",
      "priority" : "Medium"
    }

    注意

    ID字段是可选的。要创建案例记录,请忽略此字段。在我们的例子中,你传递这个字段是因为你想更新案例记录。

  4. 点击 Execute.

    该请求从您的REST服务调用upsertCase方法。状态,主题和优先级字段被更新。即使主题的值与旧主题相匹配,主题也会更新。此外,由于请求主体不包含“案例源”字段的值,因此upsertCase方法中的origin参数为null。因此,记录更新时,“原始”字段将被清除。

    要检查这些字段,请通过导航到 https://yourInstance.salesforce.com/<Record ID>中查看此记录。

使用PATCH方法更新数据

作为PUT方法的替代方法,使用PATCH方法更新记录字段。你可以用不同的方式实现PATCH方法。一种方法是在方法中指定参数以更新每个字段。例如,您可以创建一个方法来更新具有以下签名的案例的优先级:updateCasePriority(String priority)。要更新多个字段,可以列出所有需要的字段作为参数。

提供更多灵活性的另一种方法是将字段作为请求主体中的JSON名称/值对传递。这种方法可以接受任意数量的参数,并且参数在方法的签名中不固定。这种方法的另一个优点是没有字段被意外清除,因为是空的。您添加到CaseManager类的updateCaseFields方法使用第二种方法。此方法将请求正文中的JSON字符串反序列化为名称/值对映射,并使用sObject PUT方法设置字段。

@HttpPatch
global static ID updateCaseFields() {
    RestRequest request = RestContext.request;
    String caseId = request.requestURI.substring(
        request.requestURI.lastIndexOf('/')+1);
    Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
    // 将JSON字符串反序列化为名称 - 值对
    Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(request.requestbody.tostring());
    // 遍历每个参数字段和值
    for(String fieldName : params.keySet()) {
        // 设置Case sObject的字段和值
        thisCase.put(fieldName, params.get(fieldName));
    }
    update thisCase;
    return thisCase.Id;
}
要调用PATCH方法:

  1. 在Workbench REST Explorer中,单击 PATCH.
  2. 对于URI,输入 /services/apexrest/Cases/<Record ID>. 将<Record ID> 替换为先前创建的案例记录的ID。在请求正文中输入以下JSON。
    {
      "status" : "Escalated",
      "priority" : "High"
    }
    这个JSON有两个字段值:状态和优先级。 updateCaseFields方法从提交的JSON中检索这些值,并用于指定要在对象中更新的字段。
  3. 点击 Execute.

    该请求调用REST服务中的updateCaseFields方法。案例记录的状态和优先级字段被更新为新值。要检查这些字段,请通过导航到https://yourInstance.salesforce.com/<Record ID>在Salesforce中查看此记录。

测试您的Apex REST类

测试您的Apex REST类与测试其他Apex类相似,只需传入参数值即可调用类方法,然后验证结果。对于不接受参数或依赖REST请求中的信息的方法,创建一个测试REST请求。
一般来说,以下是测试Apex REST服务的方法。要模拟REST请求,请在测试方法中创建RestRequest,然后按如下方式在请求上设置属性。您也可以在请求中添加“通过”的参数来模拟URI参数。
// 设置一个测试请求
RestRequest request = new RestRequest();

// 设置请求属性
request.requestUri =
    'https://yourInstance.salesforce.com/services/apexrest/Cases/'
    + recordId;
request.httpMethod = 'GET';

// 设置其他属性,如参数
request.params.put('status', 'Working');

// 更令人敬畏的代码在这里...
// 最后,如果使用,将请求分配给RestContext
RestContext.request = request;
如果您正在测试的方法通过RestContext访问请求值,则将请求分配给RestContext以填充它(RestContext.request = request;)。

现在,让我们将整个课程保存在Developer Console中,然后运行结果。

  1. 在开发者控制台中,选择 File | New | Apex Class.
  2. 对于类名称,输入CaseManagerTest,然后单击 OK.
  3. 将自动生成的代码替换为以下类定义。
    @IsTest
    private class CaseManagerTest {
    
        @isTest static void testGetCaseById() {
            Id recordId = createTestRecord();
            // 设置一个测试请求
            RestRequest request = new RestRequest();
            request.requestUri =
                'https://yourInstance.salesforce.com/services/apexrest/Cases/'
                + recordId;
            request.httpMethod = 'GET';
            RestContext.request = request;
            // 调用测试方法
            Case thisCase = CaseManager.getCaseById();
            // 验证结果
            System.assert(thisCase != null);
            System.assertEquals('Test record', thisCase.Subject);
        }
    
        @isTest static void testCreateCase() {
            // 调用测试方法
            ID thisCaseId = CaseManager.createCase(
                'Ferocious chipmunk', 'New', 'Phone', 'Low');
            // 验证结果
            System.assert(thisCaseId != null);
            Case thisCase = [SELECT Id,Subject FROM Case WHERE Id=:thisCaseId];
            System.assert(thisCase != null);
            System.assertEquals(thisCase.Subject, 'Ferocious chipmunk');
        }
    
        @isTest static void testDeleteCase() {
            Id recordId = createTestRecord();
            // 设置一个测试请求
            RestRequest request = new RestRequest();
            request.requestUri =
                'https://yourInstance.salesforce.com/services/apexrest/Cases/'
                + recordId;
            request.httpMethod = 'GET';
            RestContext.request = request;
            // 调用测试方法
            CaseManager.deleteCase();
            // 验证记录被删除
            List<Case> cases = [SELECT Id FROM Case WHERE Id=:recordId];
            System.assert(cases.size() == 0);
        }
    
        @isTest static void testUpsertCase() {
            // 1.插入新记录
            ID case1Id = CaseManager.upsertCase(
                    'Ferocious chipmunk', 'New', 'Phone', 'Low', null);
            // 验证新记录是否已创建
            System.assert(Case1Id != null);
            Case case1 = [SELECT Id,Subject FROM Case WHERE Id=:case1Id];
            System.assert(case1 != null);
            System.assertEquals(case1.Subject, 'Ferocious chipmunk');
            // 2. 将现有记录的状态更新为Working
            ID case2Id = CaseManager.upsertCase(
                    'Ferocious chipmunk', 'Working', 'Phone', 'Low', case1Id);
            // 验证记录已更新
            System.assertEquals(case1Id, case2Id);
            Case case2 = [SELECT Id,Status FROM Case WHERE Id=:case2Id];
            System.assert(case2 != null);
            System.assertEquals(case2.Status, 'Working');
        }    
    
        @isTest static void testUpdateCaseFields() {
            Id recordId = createTestRecord();
            RestRequest request = new RestRequest();
            request.requestUri =
                'https://yourInstance.salesforce.com/services/apexrest/Cases/'
                + recordId;
            request.httpMethod = 'PATCH';
            request.addHeader('Content-Type', 'application/json');
            request.requestBody = Blob.valueOf('{"status": "Working"}');
            RestContext.request = request;
            // 将现有记录的状态更新为Working
            ID thisCaseId = CaseManager.updateCaseFields();
            // 验证记录已更新
            System.assert(thisCaseId != null);
            Case thisCase = [SELECT Id,Status FROM Case WHERE Id=:thisCaseId];
            System.assert(thisCase != null);
            System.assertEquals(thisCase.Status, 'Working');
        }  
    
        // Helper method
        static Id createTestRecord() {
            // Create test record
            Case caseTest = new Case(
                Subject='Test record',
                Status='New',
                Origin='Phone',
                Priority='Medium');
            insert caseTest;
            return caseTest.Id;
        }          
    
    }
  4. 按下CTRL + S保存。
  5. 通过选择测试|运行您的组织中的所有 Test | Run All.

测试结果显示在“测试”选项卡中。测试执行完成后,请检查“总体代码覆盖率”窗格中的CaseManager行。这是在100%的覆盖面。

告诉我更多…

了解Apex REST,Salesforce API和安全注意事项中支持的数据类型和名称空间。
Apex REST支持的数据类型
Apex REST支持这些数据类型的参数和返回值。

  • Apex基元(不包括sObject和Blob)。
  • sObjects
  • Apex基元或sObjects的列表或映射(仅支持使用String键的映射)。
  • 用户定义的类型包含上面列出的类型的成员变量。
Apex REST端点中的命名空间
Apex REST端点中的命名空间
Apex REST方法可用于托管和非托管包中。调用托管包中包含的Apex REST方法时,需要将托管包名称空间包含在REST调用URL中。例如,如果该类包含在名为packageNamespace的托管包名称空间中,并且Apex REST方法使用/ MyMethod / *的URL映射,则通过REST调用这些方法的URL将采用https:// instance形式。 salesforce.com/services/apexrest/packageNamespace/MyMethod/。
自定义Apex Web服务和Salesforce API
外部应用程序可以使用Salesforce的REST和SOAP API与Salesforce集成,而不是使用REST和SOAP服务的自定义Apex代码。这些API使您可以创建,更新和删除记录。但是,使用Apex Web服务的优势在于Apex方法可以封装复杂的逻辑。这个逻辑对消费应用程序是隐藏的。此外,Apex类操作可能比单独调用API更快,因为在客户端和Salesforce服务器之间执行的往返次数较少。通过Apex Web服务调用,仅发送一个请求,并且方法中的所有操作都在服务器上执行。
Apex Web服务的安全注意事项
Apex Web服务方法运行的安全上下文与Salesforce API的安全上下文不同。与Salesforce API不同,Apex Web服务方法以系统权限运行,并且不尊重用户的对象和字段权限。但是,Apex Web服务方法在使用with sharing关键字声明时会强制执行共享规则。

Salesforce API(8)SOAP外调

学习目标

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

  • 使用WSDL2Apex生成Apex类。
  • 执行标注以使用SOAP将数据发送到外部服务。
  • 使用模拟标注测试标注。

使用WSDL2Apex生成Apex代码

除了REST标注外,Apex还可以使用XML对SOAP Web服务进行标注。使用SOAP可能是一个痛苦的(但必要的)经验。幸运的是,我们有工具来简化这个过程。
WSDL2Apex从WSDL文档自动生成Apex类。您下载Web服务的WSDL文件,然后上传WSDL,WSDL2Apex为您生成Apex类。 Apex类构造SOAP XML,传输数据,并将响应XML解析到Apex对象中。而不是开发构建和分析Web服务消息的XML的逻辑,让由WSDL2Apex生成的Apex类在内部处理所有这些开销。如果您熟悉WSDL2Java或使用.NET作为Web引用导入WSDL,则此功能类似于WSDL2Apex。别客气。

注意

尽可能使用出站消息处理集成解决方案。仅在必要时才使用对第三方Web服务的标注。

对于这个例子,我们使用一个简单的计算器Web服务来添加两个数字。这是一个开创性的服务,是所有的愤怒!我们需要做的第一件事是下载WSDL文件来生成Apex类。点击此链接并将calculator.xml文件下载到您的计算机上。记住你保存这个文件的位置,因为你在下一步需要它。

从WSDL生成一个Apex类

  1. 从“设置”中,在“快速查找”框中输入“Apex类”,然后单击 Apex Classes.
  2. 点击 Generate from WSDL.
  3. 点击 Choose File 然后选择下载的calculator.xml文件。
  4. 点击 Parse WSDL. 应用程序为WSDL文档中的每个名称空间生成默认的类名称,并报告任何错误。

    对于这个例子,使用默认的类名称。但是,在现实生活中,强烈建议您更改默认名称,以使其更易于使用,并使代码更直观。

    现在该坦诚地谈谈WSDL解析器了。 WSDL2Apex解析是一个臭名昭着的善变。解析过程可能由于多种原因而失败,例如不支持的类型,多个绑定或未知元素。不幸的是,您可能会被迫手动编写调用Web服务的Apex类或使用HTTP。

  5. 点击 Generate Apex code. 向导的最后一页显示生成的类以及任何错误。该页面还提供链接以查看成功生成的代码。
    生成的Apex类包括用于调用由WSDL文档表示的第三方Web服务的存根类和类类。这些类允许您从Apex调用外部Web服务。对于每个生成的类,使用相同的名称和前缀Async创建第二个类。 calculatorServices类用于同步标注。 AsyncCalculatorServices类用于异步标注。

执行标注

先决条件

在运行此示例之前,请使用“授权端点地址”部分中的步骤来授权Web服务标注的端点URL https://th-apex-soap-service.herokuapp.com

现在你可以执行标注,看看它是否正确添加两个数字。有一个计算器,方便检查结果。

  1. 从设置档(打开设备齿轮图标Setup gear icon)打开开发者控制台。
  2. 在开发者控制台中,选择 Debug | Open Execute Anonymous Window.
  3. 删除所有现有的代码,并插入下面的代码片段。
    calculatorServices.CalculatorImplPort calculator = new  calculatorServices.CalculatorImplPort();
    Double x = 1.0;
    Double y = 2.0;
    Double result = calculator.doAdd(x,y);
    System.debug(result);
  4. 选择 Open Log, 然后单击 Execute.
  5. 调试日志打开后,单击调试只查看System.debug语句的输出。日志应该显示3.0。

测试Web服务标注

所有有经验的Apex开发人员都知道,要部署或打包Apex代码,至少有75%的代码必须具有测试覆盖率。这个覆盖范围包括我们由WSDL2Apex生成的类。您以前可能听说过这个,但测试方法不支持Web服务标注,执行Web服务标注的测试失败。所以,我们有一点工作要做。为了防止测试失败并增加代码覆盖率,Apex提供了一个内置的WebServiceMock接口和Test.setMock方法。您可以使用此接口在测试方法中接收假响应,从而提供必要的测试覆盖率。

为标注指定模拟响应

当您从WSDL创建Apex类时,自动生成的类中的方法调用WebServiceCallout.invoke,该方法执行对外部服务的调用。在测试这些方法时,只要调用WebServiceCallout.invoke,就可以指示Apex运行时生成一个假响应。为此,请实现WebServiceMock接口并指定测试运行时发送的假响应。

指示Apex运行时通过在您的测试方法中调用Test.setMock来发送此假响应。对于第一个参数,传递WebServiceMock.class。对于第二个参数,传递一个WebServiceMock接口实现的新实例。

Test.setMock(WebServiceMock.class, new MyWebServiceMockImpl());
这很重要,所以让我们来看一个完整的示例代码。在这个例子中,你创建了一个名为callout的类,一个用于测试的模拟实现和一个测试类本身。
  1. 在开发者控制台中,选择 File | New | Apex Class.
  2. 对于类名称,输入AwesomeCalculator,然后单击 OK.
  3. 用下面的类定义替换自动生成的代码。
    public class AwesomeCalculator {
        public static Double add(Double x, Double y) {
            calculatorServices.CalculatorImplPort calculator = 
                new calculatorServices.CalculatorImplPort();
            return calculator.doAdd(x,y);
        }
    }
  4. 按下CTRL + S保存。

    在测试过程中创建您的模拟实现来伪造标注。您的WebServiceMock实现调用doInvoke方法,该方法返回您为测试指定的响应。这些代码大部分是样板。这个练习中最难的部分就是搞清楚Web服务如何返回一个响应,这样你就可以伪造一个值。

  5. 在开发者控制台中,选择 File | New | Apex Class.
  6. 对于类名称,输入CalculatorCalloutMock,然后单击 OK.
  7. 将自动生成的代码替换为以下类定义。
    @isTest
    global class CalculatorCalloutMock implements WebServiceMock {
       global void doInvoke(
               Object stub,
               Object request,
               Map<String, Object> response,
               String endpoint,
               String soapAction,
               String requestName,
               String responseNS,
               String responseName,
               String responseType) {
            // 开始 - 指定你想发送的响应
            calculatorServices.doAddResponse response_x = 
                new calculatorServices.doAddResponse();
            response_x.return_x = 3.0;
            // 结束
            response.put('response_x', response_x); 
       }
    }
  8. 按下CTRL + S保存。

    最后,您的测试方法需要通过调用Test.setMock来指示Apex运行时发送假响应,然后在AwesomeCalculator类中进行标注。像任何其他测试方法一样,我们断言我们的模拟响应得到了正确的结果。

  9. 在开发者控制台中,选择 File | New | Apex Class.
  10. 对于类名称,输入AwesomeCalculatorTest,然后单击 OK.
  11. 将自动生成的代码替换为以下类定义。
    @isTest
    private class AwesomeCalculatorTest {
        @isTest static void testCallout() {              
            // 这将导致生成一个假的响应
            Test.setMock(WebServiceMock.class, new CalculatorCalloutMock());
            // 调用调用标注的方法
            Double x = 1.0;
            Double y = 2.0;
            Double result = AwesomeCalculator.add(x, y);
            // 验证是否返回假结果
            System.assertEquals(3.0, result); 
        }
    }
  12. 按下CTRL + S保存。
  13. 要运行测试,请选择 Test | Run All.

AwesomeCalculator类现在应该显示100%的代码覆盖率!

Salesforce API(7)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和标注基础”单元中的示例中,端点是https://th-apex-http-callout.herokuapp.com/animals。

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

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

注意

除了端点和HTTP方法之外,还可以为请求设置其他属性。例如,请求可以包含提供有关请求的更多信息的标题,例如内容类型。请求还可以包含要发送到服务的数据,例如POST请求。您将在稍后的步骤中看到POST请求的示例。 POST请求类似于单击网页上的按钮来提交表单数据。使用标注,您可以将数据作为请求正文的一部分发送,而不是在网页上手动输入数据。

从服务获取数据

现在是时候把你的新的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)专辑或评论您最喜欢的“鲨鱼服装中的猫在骑着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('The status code returned was not expected: ' +
            response.getStatusCode() + ' ' + response.getStatus());
    } else {
        System.debug(response.getBody());
    }
  4. 选择 Open Log, 然后单击 Execute.
  5. 调试日志打开时,请选择仅调试以查看System.debug语句的输出。动物列表中的最后一项是“mighty moose”.

测试标注

标注测试有好消息和坏消息。坏消息是Apex测试方法不支持标注,执行标注的测试失败。好消息是测试运行时允许你“嘲笑”标注。模拟标注允许您指定在测试中返回的响应,而不是实际调用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());
                // 将“动物”键中的值作为列表进行投射
                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('The status code returned was not expected: ' +
                    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类型,即使我们使用JSON,也请选择text / plain。
  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,
                'The callout returned a null response.');
            // 验证状态码
            System.assertEquals(200,result.getStatusCode(),
              'The status code is not 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(),
              'The array should only contain 3 items.');          
        }   
    
    }
  4. 按下CTRL + S保存。
  5. 选择 Test | Always Run Asynchronously.如果不选择“始终运行异步”,则测试运行只包含一个同步运行的类。您只能从“测试”选项卡打开日志,以进行同步测试运行。
  6. 要运行测试,请选择 Test | New Run.
  7. 从Test Classes列表中选择 AnimalsCalloutsTest.
  8. 点击 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();
        // Verify that the response received contains fake values
        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)进行注释以在单独的线程中运行。

Salesforce API(6)集成

学习目标

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

  • 描述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。在过去的几年中,所有“酷儿”都已经转向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/path1和 https://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 API(5)Streaming

学习目标

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

  • 描述推技术提供的过拉技术的主要好处。
  • 创建一个PushTopic并接收事件通知。
  • 指定重播选项以进行持续流式传输。
  • 用通用流播放消息。

流事件

为了总结我们对Salesforce数据API的调查,我们来看看一个完全不同用例的API。流式API使您可以根据您定义的条件将一系列通知从Salesforce推送到客户端应用程序。推送通知与我们其他API使用的拉式范例有什么不同,客户端应用程序向Salesforce请求还是从中拉取数据?我们从船长的角度来看问题。
想象一下,你正在航行公海,你要留意迎面而来的危险,其他船只和富有宝藏的岛屿。你把一个水手放在鱼窝里保持积极的了望。现在再次戴上你的开发者帽子。假设您正在使用REST或SOAP API编写应用程序,该应用程序会定期检查是否有帐户已更新。您可以使用类似的解决方案,并通过不断请求帐户数据并检查其是否与旧数据匹配来保持主动了望。

现在想象你再次在你的船上,但是这次你可以使用一个闪亮的新型雷达显示器。你不需要把水手留在鱼窝里,因为每当感兴趣的物体接近时,显示器会发出蜂鸣声。

流媒体API是你的雷达。它允许您定义事件并在事件发生时将通知推送到您的客户端应用程序。您不必积极监视数据更改 – 您不必经常轮询Salesforce并发出不必要的API请求。

Streaming API can be used like a radar to detect data changes and send notifications

在Salesforce外部系统中存储业务数据时,跟踪Salesforce中的数据更改尤其有用。您可以使用Streaming API将您的外部来源与Salesforce数据保持同步。此外,Streaming API还可以处理外部系统中的业务逻辑,以响应Salesforce中的数据更改。例如,您可以使用Streaming API在机会更新时通知履行中心。

除了数据更改之外,还可以使用Streaming API为Salesforce外部定义的事件广播通知。例如,您可以在系统维护时段即将开始时或向用户提供新的优惠时使您的应用程序显示消息。这个功能被称为通用流媒体,我们也会介绍它。

Streaming API使用Bayeux协议和CometD消息库。

PushTopics

我们通过PushTopics与Streaming API进行交互。 PushTopic是一个sObject,它包含您想要侦听的事件的标准,例如特定对象的数据更改。您可以在PushTopic中将条件定义为SOQL查询,并指定记录操作以通知(创建,更新,删除和取消删除)。除了事件标准之外,PushTopic表示客户端应用程序订阅的频道。
当我们创建我们自己的PushTopic时,我们会深入下去。

PushTopic查询中支持的对象

PushTopic查询支持所有自定义对象。 PushTopic查询支持以下标准对象。

  • Account
  • Campaign
  • Case
  • Contact
  • Lead
  • Opportunity
  • Task

通过试验程序在PushTopic查询中支持以下标准对象。

  • ContractLineItem
  • Entitlement
  • LiveChatTranscript
  • Quote
  • QuoteLineItem
  • ServiceContract

PushTopics和通知

PushTopic使您能够定义您感兴趣接收事件通知的对象,字段和条件。以下示例显示了在Apex中定义并插入的PushTopic。在创建PushTopic之后,您可以订阅此PushTopic频道以跟踪账单城市是旧金山的客户的变化。此PushTopic指定在每个事件通知中返回Id,Name,Phone字段。默认情况下,会发送通知来创建,更新,删除和取消删除与查询条件匹配的操作。

PushTopic pushTopic = new PushTopic();
pushTopic.Name = 'AccountUpdates';
pushTopic.Query = 'SELECT Id, Name, Phone FROM Account WHERE BillingCity=\'San Francisco\'';
pushTopic.ApiVersion = 37.0;

insert pushTopic;
至少定义PushTopic名称,查询和API版本。您可以使用其余属性的默认值。默认情况下,SELECT语句字段列表和WHERE子句中的字段是触发通知的字段。通知只发送给符合WHERE子句条件的记录。要更改哪些字段触发通知,请将pushTopic.NotifyForFields设置为其中一个值。
NotifyForFields值 描述
All 如果所评估的记录符合WHERE子句中指定的条件,则会为所有记录字段更改生成通知。
Referenced (default) 评估SELECT和WHERE子句中引用的字段的更改。只有在评估记录符合WHERE子句中指定的条件时,才会生成通知。
Select 计算SELECT子句中引用的字段的更改。只有在评估记录符合WHERE子句中指定的条件时,才会生成通知。
Where 对WHERE子句中引用的字段的更改进行评估。只有在评估记录符合WHERE子句中指定的条件时,才会生成通知。

要显式设置通知首选项,请将以下属性设置为true或false。默认情况下,所有值都设置为true。

pushTopic.NotifyForOperationCreate = true;
pushTopic.NotifyForOperationUpdate = true;
pushTopic.NotifyForOperationUndelete = true;
pushTopic.NotifyForOperationDelete = true;
如果您创建一个帐户,则会生成一个事件通知。通知以JSON形式提供,并包含我们在PushTopic查询中指定的字段:Id,Name和Phone。事件通知类似于以下内容。
{
  "clientId": "lxdl9o32njygi1gj47kgfaga4k", 
  "data": {
    "event": {
      "createdDate": "2016-09-16T19:45:27.454Z", 
      "replayId": 1, 
      "type": "created"
    }, 
    "sobject": {
      "Phone": "(415) 555-1212", 
      "Id": "001D000000KneakIAB", 
      "Name": "Blackbeard"
    }
  }, 
  "channel": "/topic/AccountUpdates"
}
通知消息包含PushTopic的通道,其名称格式为 /topic/PushTopicName. 当您创建一个PushTopic时,通道会自动创建。

推送专题查询

让我们花点时间深入一下我们为我们的PushTopic定义的查询。 PushTopic查询是常规的SOQL查询,所以如果您熟悉SOQL,则不需要学习新的格式。如果你不熟悉,不要担心。我们在这里介绍基本的查询格式。简而言之,查询包含一个带有可选WHERE子句的SELECT语句,如下所示。

SELECT <comma-separated list of fields> FROM <Salesforce object> WHERE <filter criteria>
默认情况下,逗号分隔的字段列表指定要检测其更改的字段。这些字段的值在通知消息中发送。 FROM子句指定我们感兴趣的数据更改的对象。WHERE子句根据条件将记录集合缩小为更具体的集合。如果WHERE子句中的条件得到满足,则对WHERE子句中的字段的更改也会生成通知。您可以通过修改PushTopic字段NotifyForFields来更改哪些字段生成通知。
WHERE子句支持SOQL支持的运算符,如LIKE,IN,OR和AND运算符。例如,要过滤帐单城市是旧金山或纽约的帐户,我们将使用此查询。
SELECT Id, Name, Phone FROM Account WHERE BillingCity='San Francisco' OR BillingCity='New York'
为了确保及时发送通知,以下要求适用于PushTopic查询。
  • SELECT语句的字段列表必须包含Id。
  • 每个查询只允许一个对象。
  • 该对象必须对指定的API版本有效。

某些查询不受支持,例如聚合查询或半连接。

使用持久数据流检索过去的通知

到目前为止,您已经了解了有关Salesforce记录更改的PushTopics和事件通知。如果在客户端订阅PushTopic之前创建或更新Salesforce记录,会发生什么情况?在API版本37.0之前,客户端错过了相应的通知。从API版本37.0起,即使没有人订阅PushTopic,Salesforce也会存储与PushTopic查询匹配的事件。这些事件被存储了24小时,您可以随时在窗口中找回它们。好极了!

从API版本37.0开始,每个事件通知都包含一个名为replayId的字段。与重放视频类似,Streaming API会重播使用replayId字段发送的事件通知。 replayId字段的值是标识流中事件的数字。重播ID对于组织和渠道是唯一的。当您重放一个事件时,您将从存储流中的位置检索存储的事件。您可以检索由重播ID指定的事件之后开始的事件流,也可以检索所有存储的事件。以下是订阅频道时可以指定的重播选项的摘要。

重播选项 描述
replayId 订阅者接收通过重播ID值指定的事件发生的事件的通知。
-1 订阅者接收订阅后发生的所有新事件的通知。
-2 订阅者接收订阅后发生的所有新事件的通知,以及在24小时保留窗口内的先前事件。

此图显示了事件使用者如何使用各种重播选项来读取事件流。

Streaming events with replay options

样品持久的流媒体应用程序

要了解如何订阅事件并使用重播选项,请参阅“Streaming API开发人员指南”中的持久流式码示例。提供了几个示例,包括使用JavaScript CometD扩展的Visualforce页面和使用CometD连接器的Java客户端。这些代码示例的链接包含在参考资料部分。

通用流媒体

在您自行启动之前,让我们花几分钟的时间来回顾一下通用流。 Streaming API支持使用不受Salesforce数据更改限制的通用有效内容发送通知。
您可以在任何想要发送自定义通知的情况下使用通用流式传输,例如:

  • 将消息广播给特定的团队或整个组织
  • 发送Salesforce外部事件的通知

要使用通用流式传输,您需要:

  • 定义频道的流媒体频道
  • 一个或多个客户订阅该频道
  • Streaming Channel Push资源监视和调用通道上的事件

您可以通过用户界面中的Streaming Channels应用程序或通过API为通用流式创建流式通道。流式通道由StreamingChannel sObject表示,因此您可以通过Apex,REST API或SOAP API来创建它。通用流的通道名称的格式是 /u/ChannelName. 例如,这个Apex片段创建一个名为广播的频道。

StreamingChannel ch = new StreamingChannel();
ch.Name = '/u/Broadcast';

insert ch;
或者,您可以选择让Salesforce动态地为您创建流式频道(如果不存在)。要在组织中启用动态流式传输渠道,请从“设置”中,在快速查找框中输入用户界面,然后选择 User Interface. 在“用户界面”页面上,选择 Enable Dynamic Streaming Channel Creation 选项。

您可以使用CometD客户端订阅频道。 (参考资料部分链接到Streaming API开发人员指南中的示例演练。)

要生成事件,请向以下REST资源发出POST请求。用您的频道的ID替换XX.0与API版本和 Streaming Channel ID

/services/data/vXX.0/sobjects/StreamingChannel/Streaming Channel ID/push

注意

要获得您的通道ID,请在StreamingChannel上运行SOQL查询,如: SELECT Id, Name FROM StreamingChannel

示例REST请求正文。

{ 
  "pushEvents": [
      { 
          "payload": "Broadcast message to all subscribers", 
          "userIds": [] 
      } 
   ] 
}

注意

而不是广播给所有用户,通过使用可选的userIds字段指定一个订阅用户列表发送通知。另外,您还可以使用Streaming Channel Push REST API资源的GET方法获取该频道的活动订阅者列表。

订阅的客户端收到的事件通知看起来类似于以下内容。

{
  "clientId": "1p145y6g3x3nmnlodd7v9nhi4k", 
  "data": {
    "payload": "Broadcast message to all subscribers", 
    "event": {
      "createdDate": "2016-09-16T20:43:39.392Z", 
      "replayId": 1
    }
  }, 
  "channel": "/u/Broadcast"
}

请注意,此事件通知包含replayId字段。与PushTopic流式传输一样,通用事件通知也存储了24小时,可以使用API​​版本37.0中的replayId值进行检索。

通过活动通知,您可以放心地在公海启航,前往富有宝藏的岛屿!