Salesforce: Asynchronous Apex
Asynchronous Apes is similar to other asynchronous technologies in other languages. It is used to run processes in a separate thread, at a later time.
High level overview
Benefits
User Efficiency, Scalability, Higher Limits
Future Methods
- Methods with the future annotation must be static methods, and can only return a void type.
- The specified parameters must be primitive data types, arrays of primitive data types, or collections of primitive data types; future methods can’t take objects as arguments.
- Future methods won’t necessarily execute in the same order they are called. In addition, it’s possible that two future methods could run concurrently, which could result in record locking if the two methods were updating the same record.
- Future methods can’t be used in Visualforce controllers in getMethodName(), setMethodName(), nor in the constructor.
- You can’t call a future method from a future method. Nor can you invoke a trigger that calls a future method while running a future method. See the link in the Resources for preventing recursive future method calls.
- The getContent() and getContentAsPDF() methods can’t be used in methods with the future annotation.
- You’re limited to 50 future calls per Apex invocation, and there’s an additional limit on the number of calls in a 24-hour period.
Syntax
global class HelpUtility {
// The specified parameters must be primitive data types,
// arrays of primitive data types
@future
public satic void someFutureMethod(List<Id> recordIds) {
list<Account> accounts =
[Select Id, Name from Account Where Id IN :recordIds];
// Process with the records Ids
}
}
Example
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 = send(fromNbr, toNbr, m);
insert new SMS_Log__c(to__c=toNbr, from__c=fromNbr, msg__c=results);
return results;
}
public static String send(String fromNbr, String toNbr, String m) {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://someservice');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
request.setBody('{"from":"' + fromNbr + '", "to": "' + toNbr + '", "message": "' + m +'"}');
HttpResponse response = http.send(request);
if (response.getStatusCode() != 201) {
System.debug('The status code returned was not expected: ' +
response.getStatusCode() + ' ' + response.getStatus());
} else {
System.debug(response.getBody());
}
return response.getBody();
}
}
Unit Test
@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 testSMSUtils {
@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('{"status":"Success"}', logs[0].msg__c);
}
}
Batch Apex
Syntax
public class MybatchClass implements Database.Batchable<sObject> {
public (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
// Used to collect the records or objects to be passed
// to the interface method execute for processing.
}
public void execute(Database.BatchableContext bc, List<P> records) {
// Performs the actual processing for each chunk or “batch”
// of data passed to the method
}
public void finish(Database.BatchableContext bc) {
// Used to execute post-processing operations (for example,
// sending an email) and is called once after all batches are processed
}
}
Invoking
MyBatchClass myBatchObject = new MyBatchClass(); Id batchId = Database.executeBatch(myBatchObject); // You can also optionally pass a second scope parameter to specify the // number of records that should be passed into the execute method for each batch
Example
public class UpdateAccountName implements Database.Batchable<sObject> {
public Integer recordsProcessed = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
for(Account account : scope) {
account.Name = 'Updated Name';
recordsProcessed = recordsProcessed + 1;
}
update scope;
}
public void finish(Database.BatchableContext bc) {
System.debug(recordsProcessed + ' records processed!!');
}
}
Testing
@isTest
private class UpdateAccountNameTest {
@testSetup
static void setup() {
List<Account> accounts = new List<Account>();
for(integer i=0;i<10;i++){
accounts.add(new Account(name='Account ' + i));
}
insert accounts;
}
@isTest static void test() {
Test.startTest();
UpdateAccountName uca = new UpdateAccountName();
Id batchId = Database.executeBatch(uca);
Test.stopTest();
System.assertEquals(10,
[Select count() from account where Name = 'Updated Name']);
}
}
Queueable Apex
Syntax
public class SomeClass implements Queueable {
public void execute(QueueableContext context) {
// awesome code here
}
}
Example
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: this.accounts) {
account.ParentId = this.parent;
}
update accounts;
}
}
Invoking
// Add this class to a job List<Account> accounts = [select id from account where billingstate = ‘NY’]; Id parentId = [select id from account where name = 'ACME Corp'][0].Id; UpdateParentAccount updateJob = new UpdateParentAccount(accounts, parentId); ID jobID = System.enqueueJob(updateJob);
SELECT Id, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobID
Test
@isTest
private class UpdateParentAccountTest {
@testSetup
static void setup() {
List<Account> accounts = new List<Account>();
accounts.add(new Account(name='Parent'));
for(Integer i=0;i<100;i++) {
accounts.add(new Account(name = 'Test Account ' + i));
}
insert accounts;
}
@isTest
static 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%'];
UpdateParentAccount updater = new UpdateParentAccount(accounts, parentId);
Test.startTest();
System.enqueueJob(updater);
Test.stopTest();
System.assertEquals(100,
[Select count() from Account where ParentId = :parentId]);
}
}
Scheduled Apex
Syntax
global class SomeClass implements Schedulable {
global void execute(SchedulableContext ctx) {
// awesome code here
}
}
Example
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];
// Some operations with opportunities
System.debug(opptys);
List<Task> tasks = new List<Task>();
for(Opportunity opp: opptys) {
Task t = new Task(
Status = 'New',
Type = 'Call'
);
tasks.add(t);
}
insert tasks;
}
}
Invoking
// How to use it RemindOpptyOwners reminder = new RemindOpptyOwners(); String sch = '20 30 8 10 2 ?';
String jobID = System.schedule('Remind Opp Owners', sch, reminder);
Test
@isTest
private class RemindOpptyOwnersTest {
public static String CRON_EXP = '0 0 0 15 3 ? 2022';
@isTest
static 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;
Map<Id, Opportunity> opptyMap = new Map<Id, Opportunity>(opptys);
List<Id> opptyIds = new List<Id>(opptyMap.keySet());
Test.startTest();
String jobId = System.schedule('SchedulableApexTest', CRON_EXP,
new RemindOpptyOwners());
List<Task> lt = [SELECT Id From Task Where WhatId in :opptyIds];
System.assertEquals(0, lt.size(), 'Task exist before has run');
Test.stopTest();
lt = [Select Id From Task];
System.assertEquals(opptyIds.size(), lt.size(), 'Task were not created');
}
}