Apex 异步(6)监视异步

学习目标

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

  • 如何监控不同类型的工作。
  • 如何使用弹性队列。

监视异步作业

异步工作的好处是他们在后台默默地工作。异步工作的难点在于他们在后台默默工作。幸运的是,有几种方法可以监视你的工作内容。

您可以监视Salesforce用户界面中所有作业的状态。从安装程序中,在快速查找框中输入作业,然后选择Apex作业。

Apex Jobs页面显示所有异步Apex作业,其中包含有关每个作业执行的信息。以下屏幕截图显示了同一个Batch Apex类的一个未来方法作业和两个已完成的批处理作业。

Apex Jobs

如果您有许多批处理作业,请使用“批处理作业”页面仅查看批处理作业。要打开“Apex批处理作业”页面,请单击“Apex作业”页面顶部的链接。使用“Apex批处理作业”页面中的滑块选择特定的日期范围,并缩小显示的批处理作业的列表。您可以查看过去尚未删除的作业。 “批处理作业”页面按批处理类别对作业进行分组。

Apex Batch Jobs

点击您感兴趣的课程ID旁边的“更多信息”,查找有关为该课程执行的作业的详细信息。此图像显示单击“更多信息”后显示的弹出窗口。这个批次类有两个成功执行的作业。

information about parent jobs

您还可以监视Apex Flex队列中Apex作业的状态,并对其进行重新排序以控制首先处理哪些作业。从“设置”中,在“快速查找”框中输入作业,然后选择 Apex Flex Queue.

监测Future Jobs

Future jobs就像任何其他工作一样出现在Apex工作页面上。但是,未来的工作目前不属于灵活队列的一部分。

你可以查询AsyncApexJob找到你未来的工作,但有一个警告。由于启动未来作业不会返回ID,因此您必须过滤其他字段(如MethodName或JobType)以查找您的作业。在这个Stack Exchange文章中有几个示例SOQL查询可能会有所帮助。

使用SOQL监视Queued Jobs

要查询有关您提交的作业的信息,请通过筛选System.enqueueJob方法返回的作业ID,在AsyncApexJob上执行SOQL查询。

AsyncApexJob jobInfo = [SELECT Status, NumberOfErrors
    FROM AsyncApexJob WHERE Id = :jobID];

使用Flex队列监视Queue Jobs

Apex Flex队列使您能够提交多达100个批处理作业执行。任何提交执行的作业都处于保持状态,并放置在Apex Flex队列中。最多可以有100个批处理作业处于保持状态。

按照先进先出的顺序处理作业。您可以查看当前的队列顺序并将其排列,以便将重要的工作移到前面,或者将重要的工作移到后面。

当系统资源可用时,系统从Apex Flex队列的顶部选取下一个作业,并将其移至批处理作业队列。系统可以为每个组织同时处理最多五个排队或活动作业。这些已移动作业的状态从“保持”更改为“已排队”。排队的作业在系统准备好处理新作业时被执行。像其他作业一样,您可以在Apex Jobs页面中监控排队的作业。

监视Scheduled Jobs

在计划完Apex作业之后,您可以通过在CronTrigger上运行SOQL查询来获得更多关于它的信息。以下示例查询作业运行的次数以及计划再次运行作业的日期和时间。它使用从System.schedule方法返回的jobID变量。

CronTrigger ct = [SELECT TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :jobID];
如果您在可调度类的execute方法内部执行此查询,则可以通过调用SchedulableContext参数变量上的getTriggerId来获取当前作业的ID。
global class DoAwesomeStuff implements Schedulable {

    global void execute(SchedulableContext sc) {
        // 一些很棒的代码
        CronTrigger ct = [SELECT TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :sc.getTriggerId()];
    }
    
}
您还可以从与CronTrigger记录关联的CronJobDetail记录中获取作业的名称和作业的类型。为此,请在CronTrigger上执行查询时使用CronJobDetail关系。本示例使用CronJobDetail中的作业名称和类型来检索最新的CronTrigger记录。
CronTrigger job = [SELECT Id, CronJobDetail.Id, CronJobDetail.Name, CronJobDetail.JobType FROM CronTrigger ORDER BY CreatedDate DESC LIMIT 1];
或者,您可以直接查询CronJobDetail以获取作业的名称和类型。 以下示例获取上例中查询的CronTrigger记录的作业名称和类型。 相应的CronJobDetail记录标识由CronTrigger记录上的CronJobDetail.Id表达式获取。
CronJobDetail ctd = [SELECT Id, Name, JobType FROM CronJobDetail WHERE Id = :job.CronJobDetail.Id];
最后,要获取所有Apex计划作业的总计数(不包括所有其他计划作业类型),请执行以下查询。 请注意,值“7”是为作业类型指定的,该作业类型对应于预定的Apex作业类型。 请参阅参考资料部分中的CronJobDetail,了解所有类型的列表。
SELECT COUNT() FROM CronTrigger WHERE CronJobDetail.JobType = '7

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小时内对呼叫数量有额外的限制。有关限制的更多信息,请参阅下面的链接。