Salesforce Apex for Programmers
This is Salesforce Apex introduction for programmers. It can also serve as a quick reference that no one ever seems to need.
Why? #
Because I was tired of seeing the same “what is a constant and variable” type posts that goes through everything from defining how beautiful SFDC is, how Apex can make your dreams come true, and finally get to expanding “OOP”. So, I decided to do something sensible - create yet another “what’s a constant” type post.
Also, because it can become tiring to click through Trailhead and sit through hours of videos that describe how beautiful SFDC is, and blah.
This post is mostly pointers to practical information and examples for Apex beginners who are not conversant in other programming languages.
How to get started? #
- Create an org if you don’t already have one
- Download VSCode, install Salesforce Extensions
- Install SFDX
- Create a project and connect to Salesforce
You will find it convenient to use the below mechanism to test/execute code-
- Create files in
script
folder, write code, select code, and hitSFDX: Execute Anonymous Apex with Editor Contents
in VSCode command pallette - Create Apex class with
SFDX: Create Apex Class
and execute that Apex class withMyClass.foo()
similar to point (1)
Where to Apex? #
Apex is an object oriented, strongly typed programming language provided by Salesforce. Apex enables us to customize our org by writing validation rules, automation routines (‘update this when I update that’), call external services, or expose custom logic in salesforce to external world. It is a key player in the “controller” layer of the MVC representation of salesforce architecture. Visualforce and Lightning UI invoke Apex to execute backend business logic and carry out data operations.
You can write Apex in three places in salesforce application -
- Triggers
- Classes
- Anonymous
Triggers #
Perform custom actions before or after data operations. Triggers are database-equivalents of “do this before or after some operation happens”. The “operation” can be of -
before
orafter
Insert, Update, Upsert, Delete operationsafter
an Undelete operation
Here’s a sample trigger -
trigger beforeAccountCreateHello on Account (before update) {
System.debug("hello");
}
With this simple trigger, you instruct salesforce to execute trigger beforeAccountCreateHello
code before
any update
operation on Account
object.
Triggers can be -
before
: Validate or update field values before record is saved to databaseafter
: Trigger any other operations after the record is saved to database. The current record will be read-only at this stage
You can write as many triggers as you like on an object, but it is recommended to follow a “one trigger per object” pattern.
Also see -
Classes #
Classes are reusable blocks of code which can be instantiated and used in triggers, other classes, Lightning Web Components (LWCs), and other such components in salesforce. Classes are also called user data types, which can be used to create objects.
Here’s a sample class -
public Class SayHello {
public void hello(String msg) {
System.debug(msg);
}
}
Execute the class method with the below code -
SayHello say = new SayHello();
say.hello('hello world');
We are just creating an object say
from the class and executing the method.
Classes can be -
public
: A public access modifier will make the class available to other classes, triggers, etc.private
: Available only for other local classes. Typically used with inner classes or for test classesglobal
: Class can be accessed from any Apex. Typically used with webservices
In addition:
with sharing
orwithout sharing
keywords specify the “sharing mode” - whether the class runs under the sharing rules of current user- classes can also be
virtual
orabstract
- your beloved OOP principles apply - class can implement an interface
Classes can have public / private / virtual / etc. member variables and member functions. Classes can also have static variables and methods (as opposed to the “normal”(?) instance variables and methods).
You can also have a static method -
public Class SayHello {
public static void hi(String msg) {
System.debug(msg);
}
}
.. in which case you do not instantiate the object (but directly use the class method).
SayHello.hi();
Static variables/ methods are available once the class is loaded and there is a single copy available to all instances of the class. Instance variables (& methods) are created new for every instance of the class.
Also see -
Anonymous Apex #
You can also write quick code for testing/debugging. We do that either in-
- Script files as described in previous section
- Click the
Setup
icon in your Salesforce org >Developer Console
> Click onDebug
in the popup window > ChooseOpen Execute Anonymous Window
All the Basics #
No ‘here be dragons` here - everything’s cool and in the right place.
Debugging #
- Include
System.debug('I am here
) message in Apex code - Execute Apex
- Go through logs to locate the statement and see what goes before/after that statement. In VSCode you will see this in
Output
panel (just make sureSalesforce CLI
is selected)
Data Types #
Apex has types and is similar to Java. I absolutely love discussion on the below types supported by Apex -
- Blob
- Boolean
- Date
- Datetime
- Decimal
- Double
- ID - 18 character representation of record Id. E.g
ID id='00300000003T2PGAA0'
; - Integer
- Long
- Object
- String
- Time
You define / initiate a variable like so -
Integer a;
Integer a = 1;
Integer a = 1;
Integer b;
b = 2;
Integer sum = a + b;
System.debug('sum: ' + sum);
Mix types if you can use some excitement in your life.
Control Statements #
There are multiple ways to control flow - no surprise there.
if
/else
Boolean talk = true; if (talk) { System.debug('yes'); } else { System.debug('no'); }
switch
Integer ct = 1; switch on ct { when 0 { System.debug('zero'); } when 1 { System.debug('one'); } when else { System.debug('other'); } }
while
Integer ct = 0; while (ct < 3) { System.debug('hello ' + ct); ct = ct + 1 ; }
do
/while
Integer ct = 0; do { System.debug('hello ' + ct); ct = ct + 1 ; } while (ct < 3);
for
for (Integer ct = 0; ct < 3; ct++) { System.debug('hello ' + ct); }
Operators #
- Arithmetic:
+
,-
,/
,*
- Comparison:
=
,>
,<
. Mix comparison operators for max effect ->=
,!=
,<=
etc. - Logical:
&&
,||
- Unary: any arithmetic / logical operators,
!
not operator - Bitwise operators
- Ternary operator
See operators in Apex for more.
Collections #
There are three types of collections - list, set and maps.
List #
- ordered
- allows duplicates
// initiate
List<String> employees = new List<String>();
// add values
employees.add('Siva');
employees.add('John');
System.debug('employees: ' + employees);
// (Siva, John)
// initialize list and assign values
List<Integer> num = new List<Integer>{1,2,3};
System.debug('num: ' + num);
// (1, 2, 3)
System.debug('num [0]: ' + num.get(0));
// 1
// iterate through list - `for`
for (Integer i = 0; i < num.size(); i++) {
System.debug('i is : ' + i);
}
// iterate through list - `for` alternative
List<Integer> num = new List<Integer>{1,2,3};
for (Integer i:num) {
System.debug('i am : ' + i);
}
Since SObject (or Salesforce object) is a valid type in Apex, we can do -
List<Contact> contactList = new List<Contact>();
We also have a short notation -
String[] names = new String{'Siva', 'Rama'};
List can go 4 levels deep - List<List<List<List<String>>>>
.
Set #
- order not important
- element is unique
- faster to retrieve elements
Set<String> names = new Set<String>();
names.add('Siva');
names.add('Rama');
System.debug(names);
// {Rama, Siva}
// initialize + add
Set<Integer> nums = new Set<Integer>{1,2,3};
System.debug('nums: ' + nums);
System.debug('Is 1 present? ' + nums.contains(1));
// true
Map #
- key-value pairs
- key is unique
Map<Integer, String> names = new Map<Integer, String>();
names.put(1, 'Siva');
names.put(2, 'Rama');
System.debug('names: ' + names);
// {1=Siva, 2=Rama}
System.debug('name 1: ' + names.get(1));
// Siva
You can mix and match collections too.
Map<Integer, List<String>> names = new Map<Integer, List<String>>();
names.put(1, new List<String>{'Siva', 'John'});
names.put(2, new List<String>{'Jane', 'Mat'});
System.debug('names: ' + names);
// {1=(Siva, John), 2=(Jane, Mat)}
All collections are limited by only your heap size limits.
Error Handling #
Every problem can be solved by try/catch
-
try {
System.debug('this never fails.');
}
catch (Exception e) {
// Handle this exception here
}
Built in exceptions-
- DmlException
- ListException
- NullPointerException
- QueryException
- SObjectException
Limits #
Apex coding and discussion is not complete without “limits”.
Salesforce has a beautiful, scaleable system - but that system is built on a shared infrastructure. The most effective way to ensure that the infrastructure is available for everyone is to make sure every org on the system uses resources sensibly - ergo, limits. Limits are hard, or soft, constraints on what you can execute, how many resources can be dedicated to execution of the transaction, and how often can you execute stuff.
Salesforce enforces limits for Apex creation and execution on a per transaction and time-limited basis.
Key Limits: Per Transaction #
Description | Sync Limit | Async Limit (same as sync if blank) |
---|---|---|
Total SOQL queries issued | 100 | 200 |
Total records retrieved by SOQL queries | 50,000 | |
Total SOSL queries issues | 20 | |
# DML statements issues | 150 | |
# Callouts | 100 | |
# Future calls | 50 | |
Heap size | 6 MB | 12 MB |
CPU time | 10 seconds | 60 seconds |
Max execution time for each Apex transaction | 10 min |
Key Limits: Overall #
Description | Limit |
---|---|
# async Apex method executions (greater of the two) | 250K/24 hours (or) number_licenses * 200 |
Max. Apex class scheduled concurrently | 100 |
Batch Apex jobs queued or active concurrently | 5 |
Maximum size of callout request or response | 6 MB (sync) / 12 MB (async) |
For loop list batch size | 200 |
Max size of Apex class or trigger | 1 million characters |
Max size of code used by all Apex code in org | 6 MB |
# push notifications for mobile app | 50K (SFDC app) 35K (Custom) 5K (AppExchange) |
See more limits on Apex Governor Limits documentation.
Working with Data #
You can perform data operations in Apex using SOQL and SOSL.
SOQL #
Salesforce Object Query Language is similar to SQL, but acts on the Salesforce data model. You can use SOQL in Apex, or directly fire queries using the developer console or sfdx.
Here’s some Apex code using a simple SOQL -
List<Account> acctList = [SELECT name from Account LIMIT 10];
System.debug('account list: ' + acctList);
// (Account:{Name=Acme Inc, Id=001o0000006wAaJAAU}, Account:{Name=GenePoint, Id=001o0000006wA0vAAE}, ...)
You have to use the API name of the field in SOQL. You can find that in Setup
> Object Manager
> Select the object of interest > Click on Fields and Relationships
.
You can also use relationships (implicitly - since these are already defined in object relationships against look-up fields et. al.) -
List<Contact> conList = [
SELECT Name, Email, Account.Name, Account.AnnualRevenue
FROM Contact
WHERE
Name LIKE 'A%'
LIMIT 10
];
for (Contact con: conList) {
System.debug(con.Name + '; ' + con.Email + '; ' + con.Account.Name + '; ' + con.Account.AnnualRevenue);
}
The Account
value is the relationship name in Account
look up field in Contact
.
While the above query works for selecting the parent Account
with the child Contact
record, we need to build a slightly different query for selecting child records along with parent records -
List<Account> accList = ;
for (Account a: [SELECT Name, AnnualRevenue, (SELECT Id, Name, CloseDate from Opportunities)
FROM Account
WHERE
Name LIKE 'A%'
LIMIT 10
]) {
System.debug('acct: ' + a.Name + '; ' + a.Opportunities);
}
This way of using the list directly in the for
query is preferred for better performance.
You can perform aggregates in SOQL -
List<AggregateResult> aggr = [
SELECT Type, SUM(AnnualRevenue)
FROM Account
WHERE
Rating != 'Cold'
GROUP BY Type
LIMIT 10
];
for (AggregateResult r: aggr) {
System.debug(r);
}
Following operators are valid in SOQL:
=
,>
,>=
,<
,<=
,!=
- do exactly as they appear on the tinINCLUDES
,EXCLUDES
- SpecifyWHERE
criteria for multi-select picklistsLIKE
- Use wildcardsIN
,NOT IN
You can do aggregate of SUM
, COUNT
, AVERAGE
, MIN
, MAX
.
SOSL #
SOSL is another way to access data from Salesforce.
- perform search on multiple objects in one go
- works only on text, email and phone fields
Here’s a simple SOSL statement used in Apex -
List<List<SObject>> results = [FIND 'Acme*' in ALL FIELDS RETURNING Account(Name), Contact];
Account[] accList = results[0];
Contact[] conList = results[1];
System.debug('accList: ' + accList);
System.debug('conList: ' + conList);
DML Operations #
SOQL and SOSL help you retrieve results from database. Apex has handy functions to insert, update or delete records.
You can do -
<op> <SObject> List<SObject>
… where -
op
is eitherinsert
,update
,upsert
, ordelete
SObject
is a valid Salesforce object
You can also do two more operations -
merge
: Merge upto three records of sameSObject
merge con1 con2
undelete
undelete con undelete conList
Let’s see a few examples.
Start with a simple insert -
Account a = new Account();
a.Name = 'Acme 1 Inc';
insert a;
Or..
Contact con = new Contact(Name='John Doe', Birthdate=Date.valueof('2010-01-01'), AccountId='0011J00001mZTK5QAO');
insert con;
update
and delete
are equally simple -
Contact con = [SELECT Id FROM Contact WHERE FirstName='John' AND LastName='Doe' AND AccountId='0011J00001mZTK5QAO'];
con.LastName = 'Doe Jr.';
update con;
If you have multiple DML operations in code and any of the DML operations fail, the previous DML operations will be rolled back.
DML Operations: Database #
We have an alternate mechanism to perform DML operations in Apex using Database
object.
Instead of direct SObject operations, we do -
Database.insert(List<Contact>, true);
Database.update(List<Contact>, true);
- … and so on
The advantage here is the last parameter that signifies allOrNone
to allow partial operations.
- If true,
Database
operations are very similar to the previous SObject operations. - If false, Salesforce commits partial transactions that have been executed before encountering error.
Check out this example -
Account acc1 = new Account(Name='Acme 30 Inc');
Account acc2 = new Account(Name='', AnnualRevenue=1000);
List<Account> acc = new List<Account> {acc1, acc2};
insert acc;
The transaction will fail since Account.Name
is required and the second account does not have a name. The first account, though valid, will not be created.
Let’s change the code to use Database
operations -
Account acc1 = new Account(Name='Acme 30 Inc');
Account acc2 = new Account(Name='', AnnualRevenue=1000);
List<Account> acc = new List<Account> {acc1, acc2};
Database.SaveResult[] res = Database.insert(acc, false);
System.debug('res: ' + res);
You can now see that the transaction does not throw any error. The res
debug output will show that one of the accounts failed due to required field not being present.
Order of Execution #
Salesforce has more than one way to execute business logic. Ergo, it may be useful for you to know what gets executed when.
Here’s a high-level diagram of the execution flow.
Order of execution is not guaranteed across multiple triggers - yet another reason to use a standard trigger pattern and control execution in code.
See order of execution in docs to know more.
Bulk up #
Salesforce can (and will) commit more than a single record in a single transaction. We write Apex to make use of the feature - this is what is referred to as “bulkification of Apex”.
Consider this code that we have seen previously -
Contact con = [SELECT Id FROM Contact WHERE LastName='Anderson' LIMIT 1];
con.Title = 'Neo';
update con;
You can do the update in a loop for multiple records -
Contact[] conList = [SELECT Id FROM Contact WHERE LastName='Anderson'];
for (Contact con: conList) {
con.Title = 'Neo';
update con;
}
This is when things get tricky. When you run database transactions like the above example in a loop, it works, but has to perform commits for every Anderson
contact record out there. Salesforce limits the no. of commits, no. of SOQL queries, CPU time etc. allowed in a single transaction - we’ve touched on this topic before and too many records may fail the execution.
So, it is a good idea to treat all transactions as bulk transactions. The above code can be easily re-written as -
Contact[] conList = [SELECT Id FROM Contact WHERE LastName='Anderson'];
for (Contact con:ConList) {
con.Title = 'Neo 1';
}
update conList;
Unit Tests #
Salesforce provides a unit testing framework for your Apex code. It is mandatory to have unit tests that cover 75% of Apex code. Unit tests must run successfully for (almost) every deployment. Unit tests are also executed at the time of release upgrades.
Test classes do not count against the 6 MB Apex code limit.
Let’s see an example. Consider this simple Apex class.
public class TemperatureConverter {
// Takes a Fahrenheit temperature and returns the Celsius equivalent.
public static Decimal FahrenheitToCelsius(Decimal fh) {
Decimal cs = (fh - 32) * 5/9;
return cs.setScale(2);
}
}
We write test class for the above Apex and cover all run paths -
@isTest
private class TemperatureConverterTest {
@isTest static void testWarmTemp() {
Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70);
System.assertEquals(21.11,celsius);
}
@isTest static void testFreezingPoint() {
Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32);
System.assertEquals(0,celsius);
}
@isTest static void testBoilingPoint() {
Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212);
System.assertEquals(100,celsius,'Boiling point temperature is not expected.');
}
@isTest static void testNegativeTemp() {
Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10);
System.assertEquals(-23.33,celsius);
}
}
Run tests in VSCode - Sidebar > Test
> Select Apex Tests
to run. Or - Ctrl+Shift+P
> Run Apex Tests
.
Alternatively, run tests manually in the Developer Console.
Here’s another Apex test example for a trigger.
trigger AccountDeletion on Account (before delete) {
// Prevent the deletion of accounts if they have related contacts.
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(
'Cannot delete account with related opportunities.');
}
}
Test class for the trigger is simple enough -
@isTest
private class TestAccountDeletion {
@isTest
static void TestDeleteAccountWithOneOpportunity() {
// Test data setup
// Create an account with an opportunity, and then try to delete it
Account acct = new Account(Name='Test Account');
insert acct;
Opportunity opp = new Opportunity(Name=acct.Name + ' Opportunity',
StageName='Prospecting',
CloseDate=System.today().addMonths(1),
AccountId=acct.Id);
insert opp;
// Perform test
Test.startTest();
Database.DeleteResult result = Database.delete(acct, false);
Test.stopTest();
// Verify
// In this case the deletion should have been stopped by the trigger,
// so verify that we got back an error.
System.assert(!result.isSuccess());
System.assert(result.getErrors().size() > 0);
System.assertEquals('Cannot delete account with related opportunities.',
result.getErrors()[0].getMessage());
}
}
You may note that -
- We are creating test data for our tests. This data is not persisted
- If you want to live life on the edge (or absolutely need your tests to “see” org data), you should use
@isTest(SeeAllData=true)
annotation against your test methods
See Apex testing docs to know more.
Revisiting Triggers #
Let us deep-dive into a couple of concepts now that we know better.
Collections in Triggers #
Triggers provide useful context variables to validate or update field values.
This may range from variables like isInsert
, isBefore
that specify the operation type, to collections like newMap
, old
(list) that are automatically populated with the new or old values being inserted, updated or deleted.
We can do this to set a default Rating
of Warm
for all accounts -
trigger AccountTrigger on Account (before insert, after insert, before update, after update) {
if (trigger.isInsert) {
System.debug('Inserting record');
if (trigger.isBefore) {
System.debug('Before insert');
for (Account a: Trigger.new) {
a.Rating = 'Warm';
}
}
}
else if (trigger.isUpdate) {
System.debug('Updating record');
if (trigger.isAfter) {}
else {
System.debug('before update: ' + Trigger.oldMap);
}
}
}
Collections available in triggers are below.
Value | Description | Can be used in |
---|---|---|
new | List of new values of SObject records | insert, update, undelete triggers; can be modified only in before trigger |
newMap | Returns a map - <Id, SObject> of new values of SObject records | after insert, before/after update, after undelete triggers |
old | List of old values of SObject | update, delete |
oldMap | Map of old values of <Id, SObject> | update, delete |
See all context variables available to triggers.
Bulkify Triggers #
Trigger code, like the rest of Apex, should be “bulkified” as well.
Let’s consider an example. We have to set Contact’s Title
to Contact’s Account’s Rating. (well, what can I say - scenarios can be wierd like that).
Normally, your will write some trigger code -
trigger ContactTitleFromRating on Contact (before insert) {
for (Contact con : Trigger.new) {
if (con.AccountId != null)
con.Title = [Select Rating from Account Where Id = :con.AccountId].Rating;
}
}
While this works and is perfectly acceptable for single records, remember that we can always deal with more than one record in Apex. The above code will fire off select rating query for every record being updated. This is the quickest way to the much loved “too many SOQLs” error - thanks to the Salesforce limits.
We can rewrite the code to something more sane -
trigger ContactTitleFromRating on Contact (before insert) {
Set<Id> accountIds = new Set<Id>();
// get all account ids from trigger
// .. add them to a set (to filter out duplicates)
for (Contact con : Trigger.new)
accountIds.add(con.AccountId);
// query accounts for Rating
Map<id, Account> accList = new Map<id, Account>([Select Rating from Account Where Id in :accountIds]);
// re-iterate trigger.new
// fetch Rating from the set and populate title
for (Contact con : Trigger.new) {
if (con.AccountId != null)
con.Title = accList.get(con.AccountId).Rating;
}
// the actual update happens due to trigger functionality
}
Trigger Patterns #
One moment you know what triggers are capable of, and the next moment you are writing code like this -
// example similar to that provided by salesforce.com
trigger myAccountTrigger on Account(before delete, before insert, before update,
after delete, after insert, after update) {
if (Trigger.isBefore) {
if (Trigger.isDelete) {
// In a before delete trigger, the trigger accesses the records that will be
// deleted with the Trigger.old list.
for (Account a: Trigger.old) {
if (a.name != 'okToDelete') {
a.addError('You can\'t delete this record!');
}
}
} else {
// In before insert or before update triggers, the trigger accesses the new records
// with the Trigger.new list.
for (Account a: Trigger.new) {
if (a.name == 'bad') {
a.name.addError('Bad name');
}
}
if (Trigger.isInsert) {
for (Account a: Trigger.new) {
System.assertEquals('xxx', a.accountNumber);
System.assertEquals('industry', a.industry);
System.assertEquals(100, a.numberofemployees);
System.assertEquals(100.0, a.annualrevenue);
a.accountNumber = 'yyy';
}
}
}
}
} else {
// If the trigger is not a before trigger, it must be an after trigger.
if (Trigger.isInsert) {
List < Contact > contacts = new List <Contact> ();
for (Account a: Trigger.new) {
if (a.Name == 'makeContact') {
contacts.add(new Contact(LastName = a.Name,
AccountId = a.Id));
}
3
insert contacts;
}
}
}
}
While this is doing a lot of good, there are ways to make it better. Enter trigger patterns.
There is no “one official trigger pattern” to follow. What we see below is an amalgamation of best practices -
- encourage a modular approach to writing triggers and associated logic
- write reusable code
- more maintainable code that your support guy never complains about
A simple approach was outlined by salesforce in one of the developer blog posts, and I can get behind it -
- create an interface
ITrigger
that can be used across our salesforce instance. This will provide access to all possible trigger methods - create a reusable class called
TriggerFactory
that gets called from all triggers. This is called byITrigger
- create a trigger handler class for individual objects that will implement our
ITrigger
interface. For e.g.AccountHandler
,ContactHandler
. - finally, write minimal code in the trigger itself - see example
AccountTrigger
below. This will be the only trigger defined for the object
Let’s see the code.
ITrigger Interface #
Here’s the interface -
public interface ITrigger {
/**
* bulkBefore
*
* This method is called prior to execution of a BEFORE trigger. Use this to cache
* any data required into maps prior execution of the trigger.
*/
void bulkBefore();
/**
* bulkAfter
*
* This method is called prior to execution of an AFTER trigger. Use this to cache
* any data required into maps prior execution of the trigger.
*/
void bulkAfter();
/**
* beforeInsert
* This method is called iteratively for each record to be inserted during a BEFORE
* trigger. Never execute any SOQL/SOSL etc in this and other iterative methods.
*/
void beforeInsert(SObject so);
/**
* beforeUpdate
*
* This method is called iteratively for each record to be updated during a BEFORE
* trigger.
*/
void beforeUpdate(SObject oldSo, SObject so);
/**
* beforeDelete
*
*/
void beforeDelete(SObject so);
// More code to implement everything incl. kitchensink
}
Trigger Factory #
Common resuable code that gets called by all triggers.
{
// example by salesforce
/**
* Class TriggerFactory
* Used to instantiate and execute Trigger Handlers associated with sObjects.
*/
public with sharing class Triggerfactory {
/**
* Public static method to create and execute a trigger handler
* Arguments: Schema. sObjectType soType - Object type to process (SObject.sObjectType)
* Throws a TriggerException if no handler has been coded.
*/
public static void createHandler(Schema.sObjectType soType) {
// Get a handler appropriate to the object being processed
ITrigger handler = getHandler(soType);
// Make sure we have a handler registered, new handlers must be registered in the getHandler method.
if (handler == null) {
throw new TriggerException('No Trigger Handler registered for Object Type: ' + soType);
}
// Execute the handler to fulfill the trigger
execute(handler);
}
/**
* private static method to control the execution of the handier
* Arguments: ITrigger handler - A Trigger Handler to execute
*/
private static void execute(ITrigger handler) {
// Before Trigger
if (Trigger.isBefore) {
// Call the bulk before to handle any caching of data and enable bulkification
handler.bulkBefore();
// ..rest of code
}
// ..rest of operations/actions
}
}
Trigger Handler #
Write trigger handler classes that are called by actual trigger code against objects.
public with sharing class AccountHandler
implements ITrigger {
// Constructor
public AccountHandler() {}
/**
* bulkBefore
=
* This method is called prior to execution of a BEFORE trigger. Use this to cache
* any data required into maps prior execution of the trigger.
*/
public void bulkBefore() {
// If this a delete trigger, cache a list of Account Ids that are 'in use'
}
public void bulkAfter() {
}
public void beforeInsert(SObject so) {
// code code code
// code the trigger
}
}
Object Trigger #
Finally, the minimal code in trigger itself to call the trigger handler class.
trigger AccountTrigger on Account (after delete, after insert, after update){
TriggerFactory.createHandler(Account, sObjectType)
}
By using a trigger pattern, you can -
- Control order of execution of code
- Write classes to handle business logic and reuse them in triggers or other classes
- Easier control of recursion - just use flags to control which code can be executed recursively
Asynchronous Apex #
So far we have seen how Apex works -
- You call Apex through triggers, flow or wherever
- Apex executes
This happens in real-time and the transaction waits for Apex to complete. But, there are many scenarios where transactions are required to be processed in the background, and not make the user wait until they complete. For e.g. -
- An integration call to send SFDC updates to external system
- Automation that does not need user intervention. For e.g. create a feedback activity when a service request is closed
We can write code in Apex to do stuff asynchronously. Async processing also has more generous limits as compared to their sync counterparts (e.g. timeout is 12s instead of 6s in sync mode). There are other advantages too -
- Run pseudo parallel jobs. Run tasks asynchronously while the main thread is busy on some other tasks
- Mixed DML operations. Apex does not support both user data and setup data operations in the same transaction. Using async Apex you can hand-over one of those operations to another thread
- Long running tasks are best run async. For e.g. process all contacts that turned inactive.
- Previously discussed scenario of integration call-out
There is more than one way of writing async Apex.
Future Method #
The easiest of them all is future apex. Just annotate the method that needs to run async with @future
.
Create a class -
public Class SayHello {
@future
public static void hinext(String msg) {
System.debug(msg);
}
}
Execute this anonymous Apex -
System.debug('start..');
SayHello.hinext('hello world');
System.debug('end.');
If you executed the Apex in VSCode, you will see two debug statements -
start..
end.
You can see that the future method is invoked from the limits summary in log, but do not see the debug statement. If you execute Apex in Developer Console, you will see only the future method output -
hello world
In fact, there are two logs - one for the anon Apex execution and one for the future handler. Also, you can see the future method execution task entry in Setup
> Apex
> Apex Jobs
.
The two common use cases for future method are -
- Mixed DML operations
- Call out
Let’s see a practical example. We will write Apex to -
- Create user, assign to profile
- Create a contact
Create a class CreateUserContact
-
public with sharing class CreateUserContact {
public void CreateUserContactRec(String email, String lastname, String alias) {
Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
User usr = new User(alias = alias, email=email, lastname=lastname,
languagelocalekey='en_US', localesidkey='en_US',
profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
EmailEncodingKey='UTF-8',
username=email);
insert usr;
Contact con = new Contact(FirstName='John', LastName='Doe');
insert con;
}
}
Try to run this class with anonymous Apex -
CreateUserContact cuc = new CreateUserContact();
cuc.CreateUserContactRec('joe.user1@example.com', 'User', 'joe1');
You should see an error -
23:38:11.195 (1196433779)|FATAL_ERROR|System.DmlException: Insert failed. First exception on row 0; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): Contact, original object: User: []
See more on which SObjects cannot be used together.
Let’s use the future method to create user. Modify CreateUserContact
-
public with sharing class CreateUserContact {
public void CreateUserContactRec(String email, String lastname, String alias) {
CreateUserRec(email, lastname, alias);
Contact con = new Contact(FirstName='John', LastName='Doe');
insert con;
}
@future
public static void CreateUserRec(String email, String lastname, String alias) {
Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
User usr = new User(alias = alias, email=email, lastname=lastname,
languagelocalekey='en_US', localesidkey='en_US',
profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
EmailEncodingKey='UTF-8',
username=email);
insert usr;
}
}
Your code should run without problems now.
Note that future methods -
- should be static
- allow parameters of primitive or primitive collection types (no SObject or anything)
- do not guarantee sequence of execution
- a future method cannot call another future method (no chaining)
Queueable Apex #
Queueable remediates limitations of future method. Using queueable Apex, you can -
- Pass any parameter types incl. SObject
- Control order of execution
- Call other queueable Apex and chain jobs
- More easy to monitor since you have a definite job id :)
Implement queueable Apex by creating a new class CreateUserQue
-
public class CreateUserQue implements Queueable{
public final Map<String, String> data;
public CreateUserQue(Map<String, String> input)
{
this.data = input;
}
public void execute(QueueableContext ctx) {
CreateUserRec();
}
public void CreateUserRec() {
Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
User usr = new User(alias = data.get('alias'), email=data.get('email'), lastname=data.get('lastname'),
languagelocalekey='en_US', localesidkey='en_US',
profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
EmailEncodingKey='UTF-8',
username=data.get('email'));
insert usr;
System.debug('created user.');
}
}
You may observe that we created a constructor to receive the variables passed by caller.
We will create a second class for Contact
operation -
public class CreateContactQue implements Queueable{
public final Map<String, String> data;
public CreateContactQue(Map<String, String> input)
{
this.data = input;
}
public void execute(QueueableContext ctx) {
CreateContactRec();
}
public void CreateContactRec() {
Contact con = new Contact(FirstName='John', LastName='Doe');
insert con;
System.debug('created contact.');
}
}
All we need to do now is call the queueable Apex -
Map<string, string> inData= new Map<string, string>{'email'=>'joe.user2@example.com', 'lastname' => 'User', 'alias' => 'joe2'};
CreateUserQue cuq = new CreateUserQue(inData);
Id jobId1 = System.enqueueJob(cuq);
CreateContactQue cuc = new CreateContactQue(inData);
Id jobId2 = System.enqueueJob(cuc);
While the above code works, it also enqueues two seemingly independent jobs. We can make them execute in sequence. Change CreateUserQue
to enqueue CreateContactQue
-
public class CreateUserQue implements Queueable{
public final Map<String, String> data;
public CreateUserQue(Map<String, String> input)
{
this.data = input;
}
public void execute(QueueableContext ctx) {
CreateUserRec();
System.enqueueJob(new CreateContactQue(data));
}
public void CreateUserRec() {
Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
User usr = new User(alias = data.get('alias'), email=data.get('email'), lastname=data.get('lastname'),
languagelocalekey='en_US', localesidkey='en_US',
profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
EmailEncodingKey='UTF-8',
username=data.get('email'));
insert usr;
System.debug('created user.');
}
}
You can now call CreateUserQueue
to execute jobs one after the other -
Map<string, string> inData= new Map<string, string>{'email'=>'joe.user2@example.com', 'lastname' => 'User', 'alias' => 'joe2'};
CreateUserQue cuq = new CreateUserQue(inData);
Id jobId1 = System.enqueueJob(cuq);
Batch #
Schedule Apex #
Integration #
See More #
Not satisfied with the above end-to-end, “deep-dive”, “complete guide” on Apex? Do you feel there is much more than simply writing code and saving the world? Fear not - head over to the below links -
- Get Started on Apex unit on Trailhead
- Apex developer guide
- See more Apex beginner content in PD1 certification guide
- Get Apex Specialist and Advanced Apex Specialist super badges