Salesforce Apex Queueable Interface
A queueable interface in Salesforce allows for the asynchronous execution of Apex jobs. It's similar to @Future annotated Apex classes except that a queueable interface can be monitored via the Salesforce user interface or by querying the AsyncApexJob table.
You can really appreciate the queueable interface if you've ever needed to kick off an asynchronous process from another asynchronous process. @Future annotated classes are prevented from being executed from something like a batch job but a queueable interface is completely okay when originating from a batch job.
You may be wondering why something like a queueable interface is useful to an everyday admin or developer. My opinion is that something like this is great for situations where you need to handle some additional processing but don't want to slow down someone that is actively navigating the Salesforce user interface.
For example, maybe you need to rollup some values on a record when related record(s) have been modified in some way. Like updating Community Contact records with changes made to a User record from within a Community. Or populating a territory value to all Contacts owned by a User when the territory value on the User record changes. Rather than execute the logic in the trigger or trigger handler you can kick off a queueable interface that will process the data as resource availability dictates and it won't slow down your User as they are navigating.
Some other examples that I've personally used:
In the past I provided an example of how reassign Contacts and Opportunities to an Account owner if the OwnerId was changed in some way other than using the user interface.
In that example I executed all of the logic within a trigger. Moving the primary business logic to a queueable interface could be accomplished through the creation of the following Apex Class:
/*
Created by: Greg Hacic
Last Update: 7 February 2019 by Greg Hacic
Questions?: greg@interactiveties.com
Notes:
- queueable interface enables the asynchronous execution of Apex jobs that can be monitored
- reassigns Contacts and Opportunities linked to an Account
*/
public class queueReassign implements queueable {
private final Map<Id, Id> previousOwnerIds = new Map<Id, Id>(); //map of Account.Id -> old Account.OwnerId
//constructor
public queueReassign(Map<Id, Id> passedMap) {
previousOwnerIds = passedMap; //assign the passed map
}
//executes the queueable logic
public void execute(QueueableContext qc) {
List<Contact> contactUpdates = new List<Contact>; //list of Contact objects to be updated
List<Opportunity> opportunityUpdates = new List<Opportunity>; //list of Opportunity objects to be updated
for (Account a : [SELECT Id, OwnerId, (SELECT Id, OwnerId FROM Contacts), (SELECT Id, OwnerId FROM Opportunities WHERE IsClosed = False) FROM Account WHERE Id in :previousOwnerIds.keySet()]) { //query for Contacts and Opportunities for specific Accounts
Id oldOwnerId = previousOwnerIds.get(a.Id); //grab the old OwnerId value for the Account from our map
for (Contact c : a.Contacts) { //for all related Contacts
if (c.OwnerId == oldOwnerId) { //if the Contact is assigned to the old Account Owner
contactUpdates.add(new Contact(Id = c.Id, OwnerId = a.OwnerId)); //construct a new Contact and add it to our list for updating
}
}
for (Opportunity o : a.Opportunities) { //for all related Opportunities
if (o.OwnerId == oldOwnerId) { //if the Opportunity is assigned to the old Account Owner
opportunityUpdates.add(new Opportunity(Id = o.Id, OwnerId = a.OwnerId)); //construct a new Opportunity and add it to our list for updating
}
}
}
//update the Contacts
if (!contactUpdates.isEmpty()) { //if the contactUpdates is not empty
List<Database.SaveResult> updateContactResults = Database.update(contactUpdates, false); //update the records and allow for some failures
}
//update the Opportunities
if (!opportunityUpdates.isEmpty()) { //if the opportunityUpdates is not empty
List<Database.SaveResult> updateOpportunityResults = Database.update(opportunityUpdates, false); //update the records and allow for some failures
}
}
}
Of course, using a queueable interface still requires that the interface be started in some way. A trigger is perfectly suitable and that code is below:
/*
Created by: Greg Hacic
Last Update: 7 February 2019 by Greg Hacic
Questions?: greg@interactiveties.com
Notes:
- Account object trigger - Trigger.isAfter && Trigger.isUpdate contexts
*/
trigger accountTrigger on Account (after update) {
Map<Id, Id> oldOwnerIds = new Map<Id, Id>(); //map of Account.Id -> pretrigger Account.OwnerId
for (Account a : Trigger.new) { //for all records
if (a.OwnerId != Trigger.oldMap.get(a.Id).OwnerId) { //if the OwnerId is different
oldOwnerIds.put(a.Id, Trigger.oldMap.get(a.Id).OwnerId); //populate the map using the Account Id as the key and the Old OwnerId as the value
}
}
if (!oldOwnerIds.isEmpty()) { //if the map is not empty
System.enqueueJob(new queueReassign(oldOwnerIds)); //queue up the process to reassign related Contacts and Opportunities
}
}
Notice that the trigger will still fire as Account records are updated. But, because we are executing the Contact and Opportunity updates from a queueable interface instead of the trigger itself, the time it takes to run the trigger logic is naturally quicker.
Another item to notice from the queueable interface is the constructor method. In the example I've coded we are using a constructor in order to grab the Account Ids that have been passed from the trigger. This was done because we wanted to ensure that we handle records related specifically to the Accounts that had ownership changes. But a constructor is not required for use within a queueable interface. We could have built a query into the execute method of the queueable interface to handle the identification of records that needed to be reassigned. Maybe a topic for sharing at another time.