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 Triggers(2)

学习目标

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

  • 编写对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]) {
  
        // 做一些其他处理
    }
}
超越基础

触发器一次执行批次的200条记录。所以如果有400条记录触发一个触发器,那么触发器会触发两次,每个记录有一次。因为这个原因,在触发器中你没有得到SOQL的循环记录批处理的好处,因为触发器也记录了批处理记录。在这个例子中SOQL for循环被调用了两次,但是一个独立的SOQL查询也被调用了两次。但是,SOQL for循环看起来比迭代集合变量更优雅!

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

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

Salesforce Triggers(1)

学习目标

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

  • 编写一个Salesforce对象的触发器。
  • 使用触发器上下文变量
  • 从触发器调用类方法。
  • 在触发器中使用sObject addError()方法来限制保存操作。

编写 Apex触发器

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

使用触发器执行无法使用Salesforce用户界面中的指向点击工具完成的任务。例如,如果验证字段值或更新记录中的字段,请改为使用验证规则和工作流程规则。

可以为顶级标准对象定义触发器,例如帐户或联系人,自定义对象和一些标准子对象。触发器在创建时默认处于活动状态。当指定的数据库事件发生时,Salesforce会自动触发活动触发器。

Trigger 语法

触发器定义的语法不同于类定义的语法。触发器定义以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。点击 Submit.
  3. 用下面的代码替换默认的代码。
    trigger HelloWorldTrigger on Account (before insert) {
    	System.debug('Hello World!');
    }
  4. 要保存,请按Ctrl + S。
  5. 要测试触发器,请创建一个帐户。
    1. 单击 Debug | Open Execute Anonymous Window.
    2. 在新窗口中,添加以下内容,然后单击 Execute.
      Account a = new Account(Name='Test Trigger');
      insert a;
  6. 在调试日志中,找到Hello World!声明。日志还显示触发器已被执行。

触发器的类型

有两种类型的触发器。

  • 触发器在保存到数据库之前用于更新或验证记录值之前。
  • 使用触发器访问由系统设置的字段值(例如记录的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';
    }   
}

注意

系统在触发器执行完成后保存触发前触发器的记录。您可以修改触发器中的记录,而无需显式调用DML插入或更新操作。如果您在这些记录上执行DML语句,则会出现错误。

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

trigger ContextExampleTrigger on Account (before insert, after insert, after delete) {
    if (Trigger.isInsert) {
        if (Trigger.isBefore) {
            // Process before insert
        } else if (Trigger.isAfter) {
            // Process after insert
        }        
    }
    else if (Trigger.isDelete) {
        // Process after delete
    }
}

触发上下文变量

下表是可用于触发器的所有上下文变量的综合列表。

变量 用法
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()方法。此实用程序方法向指定的收件人发送电子邮件,并包含插入的联系人记录数。

注意

EmailManager类包含在“Apex单元入门”的示例中。您必须保存组织中的EmailManager类,并在保存此触发器之前将sendMail()方法更改为static。

  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('Your email address', 'Trailhead Trigger Tutorial', 
                        recordCount + ' contact(s) were inserted.');
        }
        else if (Trigger.isDelete) {
            // 删除后的处理
        }
    }
  4. 要保存,请按Ctrl + S。
  5. 要测试触发器,请创建一个联系人。
    1. 单击 Debug | Open Execute Anonymous Window.
    2. 在新窗口中,添加以下内容,然后单击 Execute.
      Contact c = new Contact(LastName='Test Contact');
      insert c;
  6. 在调试日志中,检查触发器是否被触发。在日志末尾,找到实用程序方法编写的调试消息: DEBUG|Email sent successfully
  7. 现在检查您是否收到一封电子邮件,其中正文 1 contact(s) were inserted.

    使用新的触发器,每次添加一个或多个联系人时都会收到一封电子邮件!

添加相关记录

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

如果没有机会与客户相关联,则此触发器为每个新的或更新的客户添加相关机会。触发器首先执行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. 在客户页面的机会相关列表中,找到新的机会。触发器自动添加了这个机会!

超越基础

你添加的触发器迭代所有属于触发器上下文的记录 – for循环遍历Trigger.New。但是,这个触发器中的循环可能更有效率。我们并不是真的需要访问这个触发器上下文中的每个客户,而是只有一个子集 – 没有机会的客户。下一个单元显示如何使这个触发器更有效率。在“批量触发器设计模式”单元中,了解如何修改SOQL查询以仅获取没有机会的帐户。然后,学习只遍历这些记录。

使用触发器异常

您有时需要对某些数据库操作添加限制,例如在满足某些条件时防止保存记录。为了防止在触发器中保存记录,在有问题的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帐户的页面,然后单击 Delete.
  3. 在确认弹出窗口中,单击 OK.

    使用自定义错误消息查找验证错误无法删除具有相关机会的帐户。

  4. 禁用AccountDeletion触发器。如果你保持这个触发器激活,你不能检查你的挑战。
    1. 从设置中搜索Apex触发器。
    2. 在Apex触发器页面上,单击AccountDeletion触发器旁边的编辑。
    3. 取消选择 Is Active.
    4. 点击 Save.

超越基础

在触发器中调用addError()会导致整个操作集回滚,除非部分成功调用批量DML。

  • 如果Apex中的DML语句产生了触发器,则任何错误都会回滚整个操作。但是,运行时引擎仍会处理操作中的每个记录,以编译一个全面的错误列表。
  • 如果Force.com API中的批量DML调用产生了触发器,那么运行时引擎将把有害的记录放在一边。运行时引擎然后尝试部分保存没有产生错误的记录。

触发器和外调

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

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

注意

该示例仅为说明目的使用了假设的端点URL。除非您将端点更改为有效的URL并在Salesforce中为您的端点添加远程站点,否则无法运行此示例。

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调用标注。

Salesforce Apex (基础5)SOSL

学习目标

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

  • 描述SOSL和SOQL之间的区别。
  • 使用SOSL查询搜索跨多个对象的字段。
  • 使用开发者控制台中的查询编辑器执行SOSL查询。

编写SOSL查询

Salesforce对象搜索语言(SOSL)是一种Salesforce搜索语言,用于在记录中执行文本搜索。使用SOSL在Salesforce中跨多个标准和自定义对象记录搜索字段。 SOSL类似于Apache Lucene。
向Apex添加SOSL查询非常简单 – 您可以直接在您的Apex代码中嵌入SOSL查询。当SOSL嵌入在Apex中时,它被称为在线SOSL。

这是SOSL查询的一个例子,该查询搜索具有任何字段“SFDC”的客户和联系人。

List<List<SObject>> searchList = [FIND 'SFDC' IN ALL FIELDS 
                                      RETURNING Account(Name), Contact(FirstName,LastName)];

SOQL与SOSL的差异与相似性

与SOQL一样,SOSL允许您搜索组织的记录以获取特定信息。与一次只能查询一个标准或自定义对象的SOQL不同,单个SOSL查询可以搜索所有对象。

另一个区别是SOSL匹配基于单词匹配的字段,而默认情况下SOQL执行完全匹配(当不使用通配符时)。例如,在SOSL中搜索“Digital”会返回字段值为“Digital”或“The Digital Company”的记录,但SOQL只返回字段值为“Digital”的记录。

SOQL和SOSL是两种不同语法的独立语言。每种语言都有一个独特的用例:

  • 使用SOQL检索单个对象的记录。
  • 使用SOSL跨多个对象搜索字段。 SOSL查询可以搜索对象上的大多数文本字段。

先决条件

本单位的一些查询希望组织拥有账户和联系人。如果您还没有在SOQL单元中创建样本数据,请在本单元中创建样本数据。否则,您可以跳过在本节中创建示例数据。

  1. 在开发人员控制台中,从“调试”菜单中打开“执行匿名”窗口。
  2. 在窗口中插入下面的代码片段,然后点击执行。
// 添加客户和相关联系人
Account acct = new Account(
    Name='SFDC Computing',
    Phone='(415)555-1212',
    NumberOfEmployees=50,
    BillingCity='San Francisco');
insert acct;

// 一旦插入客户,sObject将填充一个ID。
// 获取这个ID
ID acctID = acct.ID;

// 添加一个联系人到这个客户。
Contact con = new Contact(
    FirstName='Carol',
    LastName='Ruiz',
    Phone='(415)555-1212',
    Department='Wingo',
    AccountId=acctID);
insert con;

// 添加没有联系人的客户
Account acct2 = new Account(
    Name='The SFDC Query Man',
    Phone='(310)555-1213',
    NumberOfEmployees=50,
    BillingCity='Los Angeles',
    Description='Expert in wing technologies.');
insert acct2;

使用查询编辑器

开发者控制台提供查询编辑器控制台,使您可以运行SOSL查询和查看结果。查询编辑器提供了一种快速检查数据库的方法。在将它们添加到您的Apex代码之前,测试您的SOSL查询是一个好方法。当您使用查询编辑器时,您只需提供没有围绕它的Apex代码的SOSL语句。

让我们尝试运行以下SOSL示例:

  1. 在开发者控制台中,点击 Query Editor 标签。
  2. 将以下内容复制并粘贴到查询编辑器下的第一个框中,然后单击 Execute.
FIND {Wingo} IN ALL FIELDS RETURNING Account(Name), Contact(FirstName,LastName,Department)
您组织中符合标准的所有客户和联系人记录都将显示在“查询结果”部分中作为包含字段的行。结果按每个对象(客户或联系人)的选项卡进行分组。 SOSL查询返回具有与Wingo匹配的字段的记录。根据我们的样本数据,只有联系人具有Wingo值的字段,因此此联系人将被返回。

注意

查询编辑器和API中的搜索查询必须用大括号({Wingo})括起来。相反,在Apex中,搜索查询包含在单引号(’Wingo’)内。

基本的SOSL语法

SOSL允许您指定以下搜索条件:

  • 文本表达(单个单词或短语)来搜索
  • 要搜索的字段的范围
  • 要检索的对象和字段的列表
  • 在源对象中选择行的条件

这是一个基本的SOSL查询语法:

FIND 'SearchQuery' [IN SearchGroup] [RETURNING ObjectsAndFields]

SearchQuery是要搜索的文本(单个单词或短语)。搜索项可以与逻辑运算符(AND,OR)和括号组合。此外,搜索字词可以包含通配符(*,?)。 *通配符在搜索项的中间或末尾匹配零个或多个字符。这个?通配符只匹配搜索项中间或末尾的一个字符。

文本搜索不区分大小写。例如,搜索客户,客户或CUSTOMER都会返回相同的结果。

SearchGroup是可选的。这是搜索领域的范围。如果未指定,则默认搜索范围是所有字段。 SearchGroup可以采取以下值之一。

  • ALL FIELDS
  • NAME FIELDS
  • EMAIL FIELDS
  • PHONE FIELDS
  • SIDEBAR FIELDS

ObjectsAndFields是可选的。它是在搜索结果中返回的信息 – 一个或多个sObjects的列表,并且在每个sObject中,包含一个或多个字段的列表,并带有可筛选的值。如果未指定,则搜索结果包含找到的所有对象的ID。

单词和短语

SearchQuery包含两种类型的文本:

  • Single Word— 单个单词,如test或hello。 SearchQuery中的单词由空格,标点符号和从字母到数字的变化(反之亦然)分隔。单词总是不区分大小写的。
  • Phrase— 由双引号包围的单词和空格的集合,如“约翰史密斯”。多个单词可以与逻辑和分组操作符组合在一起,形成一个更复杂的查询。

搜索示例

要了解SOSL搜索的工作原理,让我们使用不同的搜索字符串来查看输出是基于我们的样本数据。此表列出了各种示例搜索字符串和SOSL搜索结果。

在所有领域搜索: 搜索说明 匹配的记录和字段
查询 此搜索将返回其所有字段都包含文本的所有记录:The和Query。搜索字词中的单词顺序无关紧要。 客户:SFDC查询人(名称字段匹配)
Wingo OR Man 该搜索使用OR逻辑运算符。它返回包含Wingo单词的字段的记录或包含SFDC单词的字段的记录。 联系人:Carol Ruiz,部门:’Wingo’
客户:SFDC查询人(名称字段匹配)
1212 此搜索将返回其所有字段包含1212字的记录。以-1212结尾的电话字段被匹配,因为1212被短划线分隔时被认为是单词。客户:SFDC查询人,电话:’(415)555-1212′ 联系人:Carol Ruiz,电话:’(415)555-1212′
wing* 这是一个通配符搜索。此搜索将返回具有以wing开头的字段值的所有记录。

账号:SFDC查询员,描述: ‘Expert in wing technologies.’

SOSL Apex示例

此示例显示如何在Apex中运行SOSL查询。这个SOSL查询通过使用OR逻辑运算符来组合两个搜索项 – 它在任何字段中搜索Wingo或SFDC。此示例返回所有示例客户,因为它们每个都有一个包含其中一个单词的字段。 SOSL搜索结果以列表形式返回。每个列表包含返回记录的数组。在这种情况下,列表有两个元素。在索引0处,列表包含客户数组。在索引1处,列表包含联系人数组。

在开发者控制台的“执行匿名”窗口中执行此代码段。接下来,检查调试日志以验证是否返回所有记录。

List<List<sObject>> searchList = [FIND 'Wingo OR SFDC' IN ALL FIELDS 
                   RETURNING Account(Name),Contact(FirstName,LastName,Department)];
Account[] searchAccounts = (Account[])searchList[0];
Contact[] searchContacts = (Contact[])searchList[1];

System.debug('Found the following accounts.');
for (Account a : searchAccounts) {
    System.debug(a.Name);
}

System.debug('Found the following contacts.');
for (Contact c : searchContacts) {
    System.debug(c.LastName + ', ' + c.FirstName);
}

告诉我更多…

您可以筛选,重新排序和限制SOSL查询的返回结果。因为SOSL查询可以返回多个sObjects,所以这些过滤器应用在RETURNING子句中的每个sObject中。

您可以通过在对象的WHERE子句中添加条件来过滤SOSL结果。例如,这只会导致其行业服装被退回的客户:退货客户(名称,行业)行业=’服装’)

同样,通过为一个对象添加ORDER BY来支持一个sObject的排序结果。例如,这会导致返回的客户按名称字段排序:RETURNING客户(名称,行业ORDER BY名称)。

返回记录的数量可以限制为记录的一个子集。此示例将返回的客户限制为10个:RETURNING Account(Name,Industry LIMIT 10)

Salesforce Apex (基础4)SOQL

学习目标

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

  • 在Apex中编写SOQL查询。
  • 通过在开发者控制台中使用查询编辑器执行SOQL查询。
  • 通过使用匿名Apex执行嵌入在Apex中的SOQL查询。
  • 查询相关记录。

编写SOQL查询

要从Salesforce中读取记录,您需要编写一个查询。 Salesforce提供Salesforce对象查询语言(简称SOQL),您可以使用它来读取保存的记录。 SOQL类似于标准的SQL语言,但是为Force.com平台定制。
由于Apex可直接访问存储在数据库中的Salesforce记录,因此可以将SOQL查询嵌入到Apex代码中,并以简单明了的方式获取结果。当SOQL嵌入到Apex中时,它被称为内联SOQL。

要在您的Apex代码中包含SOQL查询,请将SOQL语句包装在方括号中,并将返回值分配给一个sObjects数组。例如,以下内容将检索具有两个字段(名称和电话号码)的所有客户记录,并返回一个Account sObjects数组。

Account[] accts = [SELECT Name,Phone FROM Account];

先决条件

本单位的一些查询希望组织拥有客户和联系人。在运行查询之前,请创建一些示例数据。

  1. 在开发人员控制台中,从“调试”菜单中打开“执行匿名”窗口。
  2. 在窗口中插入下面的代码片段,然后点击执行。
// 添加客户和相关联系人
Account acct = new Account(
    Name='SFDC Computing',
    Phone='(415)555-1212',
    NumberOfEmployees=50,
    BillingCity='San Francisco');
insert acct;

// 一旦插入客户,sObject将填充一个ID。
// 获取这个ID。
ID acctID = acct.ID;

// 添加一个联系人到这个客户。
Contact con = new Contact(
    FirstName='Carol',
    LastName='Ruiz',
    Phone='(415)555-1212',
    Department='Wingo',
    AccountId=acctID);
insert con;

// 添加没有联系人的客户
Account acct2 = new Account(
    Name='The SFDC Query Man',
    Phone='(310)555-1213',
    NumberOfEmployees=50,
    BillingCity='Los Angeles',
    Description='Expert in wing technologies.');
insert acct2;

使用查询编辑器

开发者控制台提供查询编辑器控制台,使您可以运行您的SOQL查询和查看结果。查询编辑器提供了一种快速检查数据库的方法。在将它们添加到您的Apex代码之前,测试您的SOQL查询是一个好方法。当您使用查询编辑器时,您只需提供SOQL语句,而不包含围绕它的Apex代码。

让我们尝试运行以下SOQL示例:

  1. 在开发者控制台中,点击 Query Editor 标签。
  2. 将以下内容复制并粘贴到查询编辑器下的第一个框中,然后单击 Execute.
SELECT Name,Phone FROM Account

您的组织中的所有客户记录都以查询结果部分显示为包含字段的行。

基本的SOQL语法

这是一个基本的SOQL查询语法:

SELECT fields FROM ObjectName [WHERE Condition]
WHERE子句是可选的。我们从一个非常简单的查询开始。例如,以下查询检索客户并为每个客户获取两个字段:ID和电话。
SELECT Name,Phone FROM Account

查询有两部分:

  1. 选择名称,电话:这部分列出您想要检索的字段。这些字段在逗号分隔列表中的SELECT关键字之后指定。或者您只能指定一个字段,在这种情况下不需要逗号(例如SELECT Phone)。
  2. FROM客户:这部分指定您要检索的标准或自定义对象。在这个例子中,它是客户。对于名为Invoice_Statement的自定义对象,它是Invoice_Statement__c。

超越基础

与其他SQL语言不同,您不能为所有字段指定*。你必须指定你想明确得到的每个字段。如果您尝试访问您在SELECT子句中未指定的字段,则会出现错误,因为该字段尚未被检索到。

您不需要在查询中指定Id字段,因为它始终在Apex查询中返回,无论是在查询中指定还是不在查询中指定。例如:SELECT Id,Phone FROM Account和SELECT Phone FROM Account是等价的语句。唯一一次你可能想要指定Id字段,如果它是唯一的字段,你正在检索,因为你必须列出至少一个字段:SELECT ID FROM Account。在查询编辑器中运行查询时,您可能还需要指定Id字段,因为除非指定,否则ID字段将不会显示。

使用条件过滤查询结果

如果您在组织中拥有多个客户,则将全部退回。如果要限制返回给满足特定条件的客户的客户,则可以在WHERE子句中添加此条件。以下示例仅检索名称为SFDC Computing的客户。请注意,字符串比较不区分大小写。

SELECT Name,Phone FROM Account WHERE Name='SFDC Computing'
WHERE子句可以包含多个使用逻辑运算符(AND,OR)和括号分组的条件。例如,此查询将返回名称为SFDC Computing的拥有超过25名员工的所有客户:
SELECT Name,Phone FROM Account WHERE (Name='SFDC Computing' AND NumberOfEmployees>25)
这是一个更复杂条件的例子。此查询将返回所有SFDC计算客户,或者拥有超过25名员工的帐单城市为洛杉矶的所有客户。
SELECT Name,Phone FROM Account WHERE (Name='SFDC Computing' OR (NumberOfEmployees>25 AND BillingCity='Los Angeles'))

超越基础

您可以使用LIKE运算符执行模糊匹配,而不是使用相等运算符(=)进行比较。例如,您可以使用以下条件检索名称以SFDC开头的所有客户:WHERE Name LIKE’SFDC%’。 %通配符匹配任何字符或不匹配。相反的_字符可以用来匹配一个字符。

订购查询结果

执行查询时,它将从Salesforce返回记录,但不会按特定顺序进行,因此每次运行查询时都不能依赖返回数组中记录的顺序。但是,您可以选择通过添加ORDER BY子句并指定记录集应该排序的字段来对返回的记录集进行排序。本示例根据“名称”字段对所有检索到的客户进行排序。

SELECT Name,Phone FROM Account ORDER BY Name
默认排序顺序按照字母顺序排列,指定为ASC。以前的声明相当于:
SELECT Name,Phone FROM Account ORDER BY Name ASC
要反转订单,请按降序使用DESC关键字:
SELECT Name,Phone FROM Account ORDER BY Name DESC

您可以对大多数字段进行排序,包括数字和文本字段。你不能在丰富的文本和多选择选项列表上排序。

在“查询编辑器”中尝试使用这些SOQL语句,并根据“名称”字段查看返回记录的顺序如何变化。

限制返回的记录数

您可以通过添加LIMIT n子句来限制返回到任意数字的记录数,其中n是要返回的记录数。当你不关心哪些记录被返回时,限制结果集很方便,但是你只想使用记录的一个子集。例如,此查询检索返回的第一个客户。请注意,使用LIMIT 1时,返回的值是一个客户,而不是数组。

Account oneAccountOnly = [SELECT Name,Phone FROM Account LIMIT 1];

把所有的东西结合在一起

您可以按照以下顺序在一个查询中组合可选子句

SELECT Name,Phone FROM Account 
                   WHERE (Name = 'SFDC Computing' AND NumberOfEmployees>25)
                   ORDER BY Name
                   LIMIT 10
在开发人员控制台中使用“执行匿名”窗口在Apex中执行以下SOQL查询。然后检查调试日志中的调试语句。应该返回一个样本客户。
Account[] accts = [SELECT Name,Phone FROM Account 
                   WHERE (Name='SFDC Computing' AND NumberOfEmployees>25)
                   ORDER BY Name
                   LIMIT 10];
System.debug(accts.size() + ' account(s) returned.');
// 写入所有客户数组信息
System.debug(accts);

在SOQL查询中访问变量

Apex中的SOQL语句可以引用Apex代码变量和表达式,前提是冒号(:)。在SOQL语句中使用局部变量称为绑定。

这个例子展示了如何在WHERE子句中使用targetDepartment变量。

String targetDepartment = 'Wingo';
Contact[] techContacts = [SELECT FirstName,LastName 
                          FROM Contact WHERE Department=:targetDepartment];

查询相关记录

Salesforce中的记录可以通过关系相互关联:查找关系或主 – 细节关系。例如,联系人与“客户”具有查找关系。当您创建或更新联系人时,您可以将其与一个客户相关联。与相同客户关联的联系人显示在客户页面的相关列表中。以同样的方式,您可以查看Salesforce用户界面中的相关记录,您可以在SOQL中查询相关记录。

要获取与父记录相关的子记录,请为子记录添加一个内部查询。内部查询的FROM子句针对关系名称而不是Salesforce对象名称。此示例包含一个内部查询以获取与每个返回的客户关联的所有联系人。 FROM子句指定联系人关系,这是客户与客户和联系人链接的默认关系。

SELECT Name, (SELECT LastName FROM Contacts) FROM Account WHERE Name = 'SFDC Computing'

下一个示例在Apex中嵌入示例SOQL查询,并演示如何使用sObject上的Contacts关系名称从SOQL结果中获取子记录。

Account[] acctsWithContacts = [SELECT Name, (SELECT FirstName,LastName FROM Contacts)
                               FROM Account 
                               WHERE Name = 'SFDC Computing'];
// 获取子记录
Contact[] cts = acctsWithContacts[0].Contacts;
System.debug('Name of first associated contact: ' 
             + cts[0].FirstName + ', ' + cts[0].LastName);
您可以使用点符号从子对象(联系人)到其父级(Account.Name)上的字段进行遍历。例如,以下Apex片段查询联系名字为Carol的记录,并且能够通过遍历客户和联系人之间的关系来检索Carol的相关客户的名称。
Contact[] cts = [SELECT Account.Name FROM Contact 
                 WHERE FirstName = 'Carol' AND LastName='Ruiz'];
Contact carol = cts[0];
String acctName = carol.Account.Name;
System.debug('Carol\'s account name is ' + acctName);

注意

本节中的示例基于标准对象。自定义对象也可以通过使用自定义关系链接在一起。自定义关系名称以__r后缀结尾。例如,发票声明通过Invoice_Statement__c自定义对象上的Line_Items__r关系链接到订单项。 (这些自定义对象是Force.com工作簿中使用的Warehouse模式的一部分。)

通过使用SOQL for循环查询批次中的记录

使用SOQL for循环,可以在for循环中包含SOQL查询。 SOQL查询的结果可以在循环内迭代。 SOQL for循环使用不同的方法来检索记录 – 通过调用SOAP API的查询和queryMore方法,使用高效分块来检索记录。通过使用SOQL for循环,可以避免碰到堆大小的限制。

SOQL for循环遍历由SOQL查询返回的所有sObject记录。 SOQL for循环的语法是:

for (variable : [soql_query]) {
    code_block
}
要么
for (variable_list : [soql_query]) {
    code_block
}

变量和variable_list必须与soql_query返回的sObjects类型相同。

最好使用SOQL for循环的sObject列表格式,因为循环为每个200 sObjects批次执行一次。这样做使您可以批量处理记录并批量执行DML操作,这有助于避免达到管理员限制。

insert new Account[]{new Account(Name = 'for loop 1'), 
                     new Account(Name = 'for loop 2'), 
                     new Account(Name = 'for loop 3')};

// sObject列表格式为每个返回的批记录执行一次for循环
Integer i=0;
Integer j=0;
for (Account[] tmp : [SELECT Id FROM Account WHERE Name LIKE 'for loop _']) {
    j = tmp.size();
    i++;
}
System.assertEquals(3, j); // 列表中应该包含三个名为“yyy”的客户
System.assertEquals(1, i); // 由于一个批次最多可以容纳200条记录,只有三条记录应该被返回,所以循环只能执行一次

Salesforce Apex (基础3)操作记录

学习目标

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

  • 使用DML插入,更新和删除记录。
  • 批量执行DML语句。
  • 使用upsert来插入或更新记录。
  • 捕获一个DML异常。
  • 使用数据库方法插入具有部分成功选项的新记录并处理结果。
  • 知道何时使用DML语句以及何时使用数据库方法。
  • 对相关记录执行DML操作

用DML操作记录

使用数据操作语言(简称DML)在Salesforce中创建和修改记录。 DML通过提供简单的语句来插入,更新,合并,删除和恢复记录,提供了一种直接管理记录的方法。
由于Apex是一种以数据为中心的语言,并保存在Force.com平台上,因此可直接访问Salesforce中的数据。与其他需要额外设置以连接数据源的编程语言不同,使用Apex DML,管理记录变得非常简单!通过调用DML语句,您可以快速对Salesforce记录执行操作。

此示例将Acme客户添加到Salesforce。首先创建一个客户sObject,然后将其作为参数传递给insert语句,该语句在Salesforce中保留该记录。

// 创建客户sObject
Account acct = new Account(Name='Acme', Phone='(415)555-1212', NumberOfEmployees=100);
// 通过使用DML插入客户
insert acct;

DML语句

以下DML语句可用。

  • insert
  • update
  • upsert
  • delete
  • undelete
  • merge

每个DML语句接受一个单独的sObject或一个sObject的列表(或数组)。在sObjects列表上操作是处理记录的更有效的方法。

除了一对,所有这些陈述都是熟悉的数据库操作。 upsert和merge语句是Salesforce特有的,可以非常方便。

upsert DML操作创建新记录,并在单个语句中更新sObject记录,使用指定的字段确定现有对象的存在,或者在没有指定字段的情况下使用ID字段。

合并语句将最多三个相同sObject类型的记录合并到其中一个记录中,删除其他记录,并将所有相关记录重新归类。

ID字段自动分配到新记录

插入记录时,系统为每个记录分配一个ID。除了在数据库中保存ID值之外,还可以在用作DML调用中的参数的sObject变量上自动填充ID值。

这个例子展示了如何获得对应于插入客户的sObject的ID。

// 创建客户sObject
Account acct = new Account(Name='Acme', Phone='(415)555-1212', NumberOfEmployees=100);
// 通过使用DML插入客户
insert acct;

// 获取插入的sObject参数的新ID
ID acctID = acct.Id;
// 在调试日志中显示此ID
System.debug('ID = ' + acctID);

// 调试日志结果(在您的情况下ID将不同)
// DEBUG|ID = 001D000000JmKkeIAF

超越基础

由于示例中的sObject变量包含DML调用后的ID,因此您可以重新使用此sObject变量来执行进一步的DML操作(如更新),因为系统可以通过匹配ID来将sObject变量映射到其对应的记录。

您可以从数据库检索记录以获取其字段,包括ID字段,但这不能用DML完成。您需要使用SOQL编写查询。您将在另一个单元学习SOQL。

批量 DML

您可以在单个sObject上执行DML操作,也可以在sObjects列表上批量执行DML操作。执行批量DML操作是推荐的方法,因为它有助于避免触及限额,例如每个Apex交易的DML限制为150条语句。这个限制是为了确保公平地访问Force.com多租户平台中的共享资源。在sObjects列表上执行DML操作会计算为列表中所有sObjects的一个DML语句,而不是每个sObject的一个语句。

此示例通过在一次调用中插入联系人列表来批量插入联系人。该示例然后批量更新这些联系人。

  1. 在开发者控制台中使用Anonymous Apex执行此代码段。
    // 创建联系人列表
    List<Contact> conList = new List<Contact> {
        new Contact(FirstName='Joe',LastName='Smith',Department='Finance'),
            new Contact(FirstName='Kathy',LastName='Smith',Department='Technology'),
            new Contact(FirstName='Caroline',LastName='Roth',Department='Finance'),
            new Contact(FirstName='Kim',LastName='Shain',Department='Education')};
                
    // 用一个DML调用批量插入所有联系人
    insert conList;
    
    // List来保存新的联系人进行更新
    List<Contact> listToUpdate = new List<Contact>();
    
    // 遍历列表并仅添加标题
    //  如果部门是财务部门
    for(Contact con : conList) {
        if (con.Department == 'Finance') {
            con.Title = 'Financial analyst';
            // Add updated contact sObject to the list.
            listToUpdate.add(con);
        }
    }
    
    // 将更新的联系人sObject添加到列表中。
    update listToUpdate;
    
  2. 检查最近在您的组织中创建的联系人。
    两位在财务部门的联系人应该有他们的头衔,财务分析师。

插入记录

如果您有一个包含新记录和现有记录混合的列表,则可以使用upsert语句来处理对列表中所有记录的插入和更新。 Upsert有助于避免重复记录的创建,并且可以节省时间,因为您不必先确定哪些记录存在。

upsert语句通过比较一个字段的值来将sObjects与现有记录进行匹配。如果调用此语句时未指定字段,则upsert语句将使用sObject的ID将sObject与Salesforce中的现有记录进行匹配。或者,您可以指定一个字段用于匹配。对于自定义对象,请指定标记为外部标识的自定义字段。对于标准对象,可以指定任何idLookup属性设置为true的字段。例如,联系人或用户的电子邮件字段设置了idLookup属性。要检查字段的属性,请参阅Salesforce和Force.com的对象参考。

Upsert语法

upsert sObject | sObject[]

upsert sObject | sObject[]​​ field

可选字段是字段标记。例如,要指定MyExternalID字段,该语句是:

upsert sObjectList Account.Fields.MyExternalId;

Upsert使用sObject记录的主键(ID),idLookup字段或外部ID字段来确定是否应该创建新记录或更新现有记录:

  • 如果密钥不匹配,则创建一个新的对象记录。
  • 如果密钥匹配一次,则更新现有对象记录。
  • 如果密钥匹配多次,则会生成错误,并且不会插入或更新对象记录。

这个例子显示了upsert如何更新一个现有的联系人记录,并在一个呼叫中插入一个新的联系人。这个upsert调用更新现有的Josh联系人并插入一个新的联系人Kathy。

注意

upsert调用使用该ID来匹配第一个联系人。 josh变量正被重新用于upsert调用。这个变量已经被填充了上一次插入调用的记录ID,所以在这个例子中不需要显式地设置ID。

  1. 在开发者控制台的“执行匿名”窗口中执行此代码段。
    //插入Josh联系人
    Contact josh = new Contact(FirstName='Josh',LastName='Kaplan',Department='Finance');       
    insert josh;
    
    // Josh's 的记录已被插入
    //   所以变量josh现在有一个ID
    //   将用于通过upsert匹配记录
    josh.Description = 'Josh\'s record has been updated by the upsert operation.';
    
    // 创建Kathy联系人,但不要将其保存在数据库中
    Contact kathy = new Contact(FirstName='Kathy',LastName='Brown',Department='Technology');
    
    // 列举以保持新联系人upsert
    List<Contact> contacts = new List<Contact> { josh, kathy };
    
    // 调用upsert
    upsert contacts;
    
    // 结果:Josh被更新,Kathy被创建。
  2. 检查组织中的所有联系人。
    您的组织只有一个Josh Kaplan记录,而不是两个,因为upsert操作找到了现有记录并进行了更新,而不是创建新的联系人记录。凯西·布朗的一个联系记录也将在那里。或者,您可以指定一个字段用于匹配记录。此示例使用Contact上的电子邮件字段,因为它具有idLookup属性集。该示例插入Jane Smith联系人,并创建第二个Contact sObject,使用相同的电子邮件填充它,然后使用电子邮件字段进行匹配以调用upsert以更新联系人。

注意

如果在这个例子中使用了插入而不是upsert,那么将会插入一个重复的Jane Smith联系人。

  1. 在开发者控制台的“执行匿名”窗口中执行此代码段。
    Contact jane = new Contact(FirstName='Jane',
                             LastName='Smith',
                             Email='jane.smith@example.com',
                             Description='Contact of the day');
    insert jane;
    
    // 1.使用idLookup字段的Upsert
    // 创建第二个sObject变量。
    // 这个变量没有任何ID设置。
    Contact jane2 = new Contact(FirstName='Jane',
                             LastName='Smith',  
                             Email='jane.smith@example.com',
                             Description='Prefers to be contacted by email.');
    // 通过使用idLookup字段进行匹配来提升联系人。
    upsert jane2 Contact.fields.Email;
    
    // 确认联系人已更新
    System.assertEquals('Prefers to be contacted by email.',
                       [SELECT Description FROM Contact WHERE Id=:jane.Id].Description);
    
  2. 检查组织中的所有联系人。

    您的组织将只有一个Jane Smith联系更新后的描述。

删除记录

您可以使用delete语句删除持久记录。删除的记录不会从Force.com永久删除,但是它们可以从恢复位置放置在回收站中15天。

此示例显示如何删除姓氏为Smith的所有联系人。如果您已经为批量DML运行了样本,那么您的组织应该与Smith的姓氏有两个联系。在开发人员控制台中使用匿名Apex执行此代码段,然后验证是否没有与姓氏Smith的联系人。

Contact[] contactsDel = [SELECT Id FROM Contact WHERE LastName='Smith']; 
delete contactsDel;

注意

这个片段包含一个查询来检索联系人(一个SOQL查询)。你会在另一个单元学到更多关于SOQL的知识。

DML语句异常

如果DML操作失败,则返回DmlException类型的异常。您可以在代码中捕获异常以处理错误情况。

这个例子产生了一个DmlException,因为它试图插入一个没有必需的Name字段的客户。 catch块中捕获到异常。

try {
    // 这会导致异常
    //   不提供必需的名称字段。
    Account acct = new Account();
    // 插入客户
    insert acct;
} catch (DmlException e) {
    System.debug('A DML exception has occurred: ' +
                e.getMessage());
}

数据库方法

Apex包含内置的Database类,它提供执行DML操作和镜像DML语句对应的方法。
这些数据库方法是静态的,并在类名称上调用。
  • Database.insert()
  • Database.update()
  • Database.upsert()
  • Database.delete()
  • Database.undelete()
  • Database.merge()

与DML语句不同,数据库方法有一个可选的allOrNone参数,允许您指定操作是否应该部分成功。当此参数设置为false时,如果在部分记录集上发生错误,则成功的记录将被提交,并且将为失败的记录返回错误。另外,部分成功选项也不会引发异常。

这是你如何调用allOrNone设置为false的插入方法。

Database.insert(recordList, false);
Database方法返回包含每条记录的成功或失败信息的结果对象。例如,insert和update操作每个都返回一个Database.SaveResult对象的数组。
Database.SaveResult[] results = Database.insert(recordList, false);

注意

Upsert返回Database.UpsertResult对象,并删除返回的Database.DeleteResult对象。

默认情况下,allOrNone参数为true,这意味着Database方法的行为与其DML语句相对应,如果遇到故障,则会引发异常。

以下两条语句相当于insert recordList;声明。

Database.insert(recordList);
和:
Database.insert(recordList, true);

超越基础

除了这些方法外,Database类还包含不作为DML语句提供的方法。例如,用于事务控制和回滚的方法,用于清空回收站以及与SOQL查询相关的方法。您将在另一个单元学习SOQL。

示例:以部分成功插入记录

我们来看看使用数据库方法的例子。此示例基于批量DML示例,但是用Database方法替换了DML语句。用部分成功选项调用Database.insert()方法。列表中的一个联系人没有任何有意使用的字段,并且会导致错误,因为如果没有所需的姓氏字段,则无法保存联系人。三个联系人被提交,没有任何字段的联系人会产生错误。本示例的最后一部分遍历返回的结果并将调试消息写入调试日志。

  1. 在开发者控制台的“执行匿名”窗口中执行此示例。
    // 创建联系人列表
    List<Contact> conList = new List<Contact> {
            new Contact(FirstName='Joe',LastName='Smith',Department='Finance'),
            new Contact(FirstName='Kathy',LastName='Smith',Department='Technology'),
            new Contact(FirstName='Caroline',LastName='Roth',Department='Finance'),
            new Contact()};
                
    // 用一个DML调用批量插入所有联系人
    Database.SaveResult[] srList = Database.insert(conList, false);
    
    // 遍历每个返回的结果
    for (Database.SaveResult sr : srList) {
        if (sr.isSuccess()) {
            // 操作成功,所以得到处理记录的ID
            System.debug('Successfully inserted contact. Contact ID: ' + sr.getId());
        } else {
            // 操作失败,所以得到所有的错误
            for(Database.Error err : sr.getErrors()) {
                System.debug('The following error has occurred.');
                System.debug(err.getStatusCode() + ': ' + err.getMessage());
                System.debug('Contact fields that affected this error: ' + err.getFields());
    	 }
        }
    }
    
  2. 验证调试消息(使用过滤器的DEBUG关键字)。
    应该报告一个故障,应该插入三个联系人。

你应该使用DML语句还是数据库方法?

  • 如果希望在批量DML处理期间发生的任何错误作为立即中断控制流的Apex异常(通过使用try。。catch块)引发,请使用DML语句。这种行为类似于大多数数据库过程语言中处理异常的方式。
  • 如果要允许批量DML操作的部分成功,请使用Database类方法 – 如果记录失败,则DML操作的其余部分仍可能成功。然后您的应用程序可以检查被拒绝的记录,并可能重试该操作。使用此表单时,您可以编写不会抛出DML异常错误的代码。相反,您的代码可以使用适当的结果数组来判断成功或失败。请注意,数据库方法还包括一个支持抛出异常的语法,类似于DML语句。