Developer 1 - Apex Language Basics
These notes were taken while studying using Mike Wheeler's Salesforce Courses.
12 - Primitive Data Types
- Apex uses the same primitive data types as SOAP API, except for higher-precision Decimal type in certain cases.
- All primitive data types are passed by value.
- All Apex variables (both class member variables and method variables) are initialized to null. Initiatlize variabbles to appropriate values before using them. For example, initialize Boolean variables to false.
- Primitive Data Type developer doc
- Primitive data types are the most basic type of variable you can have in Apex.
- Range from simple
String
s to complexDateTime
s
- Range from simple
- Apex does some datatype checking, so you cannot assign the string
'true'
(instead oftrue
) to a Boolean variable as it generates an “Illegal Assignment” error. - There are many interesting “helper methods” available by typing the datatype and
.
, soString.
, for example. Blob
- commonly used for Salesforce filesObject
- generic Apex datatype, all primitives and more complex types are of typeObject
Long
- lets you have a higher-precision Integer
public class AccountTriggerHandler {
//String: Text value
//Note: some characters need to be "escaped"
public String stringVariable = 'This is a \'string\'';
//Can convert other datatypes to strings using the following
public String booleanAsString = String.valueOf(true);
//Boolean: True/False value
public Boolean booleanVariable = true;
//Integer: whole number value
public Integer integerVariable = 1;
//Can convert other datatypes to integers using the following
public Integer integerVariable = Integer.valueOf('2000');
//Decimal or Double: Number with decimal value
//Decimal and Double values, practically speaking, can be used almost interchangeably
public Decimal decimalVariable = 39.65;
public Double doubleVariable = 40.2;
//Id: Salesforce ids. Can accept and successfully compare 15 character and 18 character
//So, 15- and 18-char correspond to same record they will be equivalent in Apex
public Id idVariable = '0015f000003ar3EAAQ';
//Date: Contains year, month, and date. Can compare date values between two variables easily.
public Date dateVariable = Date.newInstance(2021, 09, 20);
//DateTime: Contains year, month, date, minute, hour and second. Can compare DateTime values between two variables easily.
//DateTime.newInstance(year, month, day, hour, minute, second)
public DateTime dateTimeVariable = DateTime.newInstance(2020, 01, 01, 23, 00, 00);
//Time: Contains minute, hour, second, and millisecond. Can compare Time values between two variables easily.
//Time.newInstance(hour, minute, second, millisecond)
public Time timeVariable = Time.newInstance(01, 01, 01, 01);
//Can construct a dateTimeVariable from an existing dateVariable and timeVariable
public DateTime dateTimeVariable2=DateTime.newInstance(dateVariable, timeVariable);
//Other primitives:
//Blob - commonly used for Salesforce files
//Object
//Long
}
13 - Complex Data Types
- The line
public Account testAccount = new Account(name = testString + '1');
just creates the account in Apex, it does not insert it into the database- Created by
new Account();
- If inside a method, do not need to include the access modifier (
public
), this is only needed at the top level class - Parameters that are passed in (
new Account(name = testString + '1')
) are sent to the “constructor” +
is concatenation, if used with strings
- Created by
public class AccountTriggerHandler {
public String testString = 'test String';
public Account testAccount = new Account(name = testString + '1');
}
trigger AccountTrigger on Account (before insert) {
AccountTriggerHandler handler = new AccountTriggerHandler();
System.debug(handler.testString);
System.debug(handler.testAccount.Name);
}
- Executing the trigger and handler above by inserting an account produces the following debug notes
14 - Arrays
- An Array is just a term for a collection of multiple variables
- Arrays: List, Set, Map
- List: ordered collection of items
- When instantiating, use
{}
- Indexes are zero-based (0-4, below)
- Overall size of the list is the count of items (5, below)
- To remove an item from a list, need to remove by index
- When instantiating, use
- Set: unordered collection of unique items
- If a set already includes a value, adding the same value again will not change the set
- An advantage of sets over lists is that sets can test whether the set
contains
a value - To remove an item from a set, need to remove the value itself
- Map: most complex type of array
- Collection of correspondences between values
- keys must be unique. Mapping a value already in the map to a new value will overwrite the existing value.
- Useful paradigm is a map of
Id
andsObject
s. Anytime we have anId
we can run the get method, passing in the Id, and then get out the Account record that matches the Id.
public class AccountTriggerHandler {
//Test Account
public Account testAccount1 = new Account();
// __________________________LISTS______________________________
//New Empty String Lists
public List<String> stringList = new List<String>();
public String[] stringList2 = new String[]{};
//Populated list of integers 0 1 2 3 4
public List<Integer> integerList = new List<Integer>{1,3,5,2,300};
//Populated list of Accounts 0 1
public Account[] accountList = new Account[]{testAccount1, new Account(name='Test1')};
// __________________________LISTS______________________________
// __________________________SETS_______________________________
//New Empty Set
public Set<Integer> integerSet = new Set<Integer>();
//New Populated set
public Set<String> stringSet = new Set<String>{'This','is','a','set'};
//Convert List to Set
public Set<Account> accountSet = new Set<Account>(accountList);
// __________________________SETS_______________________________
// __________________________MAPS_______________________________
//New empty map
public Map<String, String> stringMap = new Map< String, String>();
//New populated map
public Map<Integer, String> populatedMap = new Map<Integer, String>{1 => 'First', 3 => 'Third'};
//Record Id map
public Map<Id, Account> accountMap = new Map<Id, Account>(accountList);
//List Map
public Map<String, List<String>> listMap;
//Map Map
public Map<String, Map<String, Integer>> mapMap;
}
trigger AccountTrigger on Account (before insert, before update) {
AccountTriggerHandler handler = new AccountTriggerHandler();
//List Test
System.assertEquals(handler.testAccount1, handler.AccountList[0]);
System.assertEquals(handler.integerList.size(), 5);
handler.stringList.add('New String');
handler.stringList.remove(0);
//Set Test
System.assert(handler.stringSet.contains('This'));
handler.integerSet.add(35);
handler.integerSet.remove(35);
//Map Test
System.assert(handler.populatedMap.containsKey(1));
System.assertEquals(handler.populatedMap.get(3),'third');
handler.listMap.put('First List', new List<String){'first in list','second in list'};
}
15 - If Else
- If statements are commonly used to check which context a trigger is running in
- Recall the one-trigger per object best practice - this is because if the triggers are separate there is no way to check which trigger runs first if you are running multiple triggers on the same object.
trigger AccountTrigger on Account (before insert, before update) {
//First account
Account newAccount = Trigger.new[0];
//Greater than statement
if(newAccount.NumberOfEmployees > 100){
newAccount.NumberOfEmployees++;
}
//Check boolean variable on Account record
if(newAccount.IsDeleted){
//Results
}
//Boolean variable example
Boolean greaterThan100 = newAccount.NumberOfEmployees > 100;
if(greaterThan100){
//Results
}
if(Trigger.isBefore){
if(Trigger.isInsert){
//Some before insert logic
}else if(Trigger.isUpdate){
//Some before update logic
}else if(Trigger.isDelete){
//Some before delete logic
}
}else{
if(Trigger.isInsert){
//Some after insert logic
}else if(Trigger.isUpdate){
//Some after update logic
}else if(Trigger.isDelete){
//Some after delete logic
}
}
}
16 - Comparison Operators
==
- comparison operator=
- assignment operator!=
- not equal&&
,||
- and, or
trigger AccountTrigger on Account (before insert, before update) {
Account newAccount = Trigger.new()[0];
if(Trigger.new.size() == 0){
//Logic if size equal to 0
}
if(Trigger.new.size() != 0){
//Logic if size no equal to 0
}
if(Trigger.new.size() > 0){
//Logic if size greater than 0
}
if(Trigger.new.size() >= 1){
//Logic if size greater than or equal to 1
}
if(newAccount.NumberOfEmployees < 1000){
//Logic if NumberOfEmployees less than 1000
}
if(newAccount.NumberOfEmployees > 1 &&
newAccount.NumberOfEmployees < 1000){
//Logic if NumberOfEmployees is less than 1000 AND
//greater than 1
}
if(newAccount.Name == 'Test Account 1' ||
newAccount.NumberOfEmployees > 1000){
//Logic if Name is equalt to 'Test Account 1' OR
//NumberOfEmployees is greater than 1000
}
if((newAccount.Name == 'Test Account 1' &&
newAccount.NumberOfEmployees < 1000) ||
newAccount.NumberOfEmployees > 1000){
//Logic if NumberOfEmployees is greater than 1000 OR
//BOTH
//NumberOfEmployees is less than 1000 AND
//Name is equal to 'Test Account 1'
}
}
17 - Ternary Operators
- Ternary operators are a way to simplify code and reduce the number of lines needed to write simple logic.
- Ternary operators combine an if statement and an assignment into a single line
some_value = if_condition ? assignment_if_true : assignment_if_false
Without ternary operator
Account acct = Trigger.new[0];
if(acct.NumberofEmployees > 50){
acct.AccountNumber = acct.AccountNumber + 1000;
}
With ternary operator
Account acct = Trigger.new[0];
// Condition // If True // If False
acct.AccountNumber = acct.AccountNumber > 50 ? acct.AccountNumber + 1000 : acct.AccountNumber;
18 - Switch Statements
- Switch statements are way of simplify code from a standard set of if-statements
- Note: can only run switch statements on
String
s,Integer
s, andsObject
s
Account newAccount = Trigger.new[0];
//If statements - repetitive, takes more cpu time to process
if(newAccount.Name == 'Test Account 1'){
//Logic if Test Account 1
}else if(newAccount.Name == 'Test Account 2'){
//Logic if Test Account 2
}else if(newAccount.Name == 'Test Account 3'){
//Logic if Test Account 3
}else{
//Logic if none of the above results were true
}
//Switch statement - better than if
switch on newAccount.Name {
when 'Test Account 1' {
//Logic if Test Account 1
}
when 'Test Account 2' {
//Logic if Test Account 2
}
when 'Test Account 3' {
//Logic if Test Account 3
}
when else {
//Logic if non of the above results were true
}
}
19 - For Loops
Standard For Loop
for integer_instantiation ; continuation_condition ; increment
- for loop continues until
continuation_condition
evaluates to false
- for loop continues until
Account[] accountList = Trigger.new;
for(Integer i = 0 ; i < accountList.size() ; i++){
//Get Account at index 1
Account a = accountList[i];
System.debug(a.Name);
}
For Each Loop
- “For Each” loops are similar, just more brief. They are used to loop through a list of items.
Account[] accountList = Trigger.new;
for (Account a : Trigger.new){ // Could replace Trigger.new with accountList
a.AccountNumber += 1;
}
20 - While Loops
- While loop may not execute at all
Integer i = 1;
while (i < 100){
//While loop logic
i++;
}
- Do While loop is similar - always executes at least once
do{
//do while logic
i--;
}while(i > 0);
21 - try/catch
- Code below fails if you try to insert a single account, since the Apex tries to access the second new account in the
Trigger.new
array.List index is out of bounds
trigger AccountTrigger on Account (before insert, before update) {
Trigger.new[1].Name = 'Test name';
}
- Situations like this where code may fail in certain situations can be handled with
try-catch
statementsif(!ex.getMessage().contains('index'))
lets you catch just certain types of exceptions that do not include ‘index’ in the exception message.getMessage()
returns a string, so all string methods work on itthrow ex;
throws the exceptioninsert errorRecord
could be used to create an error record log in the database that could be reviewed later
trigger AccountTrigger on Account (before insert, before update) {
try{
Trigger.new[1].Name = 'Test name';
}catch(Exception ex){
if(!ex.getMessage().contains('index')){
throw ex;
}else{
//insert errorRecord - to review later
System.debug(ex.getMessage());
}
}
}
22 - Custom Exceptions
- There are situations where we would like to be able to throw our own exceptions and Salesforce would not normally throw them for us - we can define Custom Exceptions.
- Create by creating a new Apex class. If the name of the class includes “Exception” then the boilerplate class code will include
extends Exception
.
- Create by creating a new Apex class. If the name of the class includes “Exception” then the boilerplate class code will include
public class AccountTriggerException extends Exception {}
public class AccountTriggerHandler {
public static void throwException(String message){
System.debug(message);
throw new AccountTriggerException(message);
}
}
trigger AccountTrigger on Account (before insert, before update) {
try{
Trigger.new[1].Name = 'Test name';
}catch(Exception ex){
AccountTriggerHandler.throwException(ex.getMessage());
}
}
23 - Challenge 1 - Basic Trigger Setup
- The setup from Mike Wheeler’s content includes the following custom fields on Opportunity (included in this package):
Tax_Percentage__c
(percentage field)Tax__c
(Currency field)Total_Price__c
(Currency field)- Also includes the
Challenge 1
opportunity page layout
- Requirements:
- Calculate tax any time an Opportunity is inserted or updated
- Fields should be calculated as follows:
Tax__c
:Tax_Percentrage__c
*Amount
- Need to convert
Tax_Percentrage__c
to a decimal value to multiply byAmount
- Need to convert
Total_Price__c
:Tax__c
+Amount
- We don’t want to run the calculations if
Amount
orTax_Percentage__c
are not populated (to avoid errors) - We don’t want to run the calculations on update if neither
Amount
norTax_Percentage__c
are changed in the update
- Pointers:
- Keep triggers logic-less and call logic in a handler
- Ensure class access is set up correctly. Methods only called from the same class should be kept private. Others should be public or global.
- In an update trigger, you can call
Trigger.newMap
andTrigger.oldMap
to see changes between records. In an Opportunity trigger, both are of typeMap<Id, Opportunity>
.oldMap
contains the Opportunity record prior to the update, andnewMap
contains the record after the update.Trigger.new
: List of recordsTrigger.newMap
,Trigger.oldMap
: same records, but as a Map: Map<Id, Opportunity>
- Be aware that in an insert trigger, you cannot reference
Trigger.old
orTrigger.oldMap
, so be sure to only reference old records from the update context.
- My First Solution:
trigger OpportunityTrigger on Opportunity (before insert, before update) {
OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
handler.calculateAmount(Trigger.new);
}
public class OpportunityTriggerHandler {
public void calculateAmount(Opportunity[] opportunityList) {
for (Opportunity o : opportunityList){
if(o.Amount != NULL && o.Tax_Percentage__c != NULL) {
o.Tax__c = o.Tax_Percentage__c * .01 * o.Amount;
o.Total_Price__c = o.Amount + o.Tax__c;
}
}
}
}
24 - Challenge 1 - Work Check
- Opportunity Trigger Solution:
- Comments as shown below are a best practice
/*
* Name: OpportunityTrigger
* Descriptions: Runs before every Opportunity DML action
* Author: Ryan Wingate (copying from Anthony Wheeler)
*/
trigger OpportunityTrigger on Opportunity (before insert, before update) {
if(Trigger.isBefore){
if(Trigger.isInsert){
OpportunityTriggerHandler.beforeInsert(Trigger.new);
}
if(Trigger.isUpdate){
OpportunityTriggerHandler.beforeUpdate(Trigger.newMap, Trigger.oldMap);
}
}
}
- Opportunity Trigger Handler solution:
global
class definition so it is as accessible as possibleglobal
method means anyone can call itstatic
method means it can be called directly from the handlernewMap.keySet()
returns the IDs in theMap<Id, Opportunity>
newMap.get(oppId)
returns the new record- Whenever there is a sequence of calculations like
newRecord.Tax__c = (newRecord.Tax_Percentage__c/100) * newRecord.Amount;
ornewRecord.Total_Price__c = newRecord.Tax__c + newRecord.Amount;
its a good idea to abstract the logic into its own method
global class OpportunityTriggerHandler {
//Method to calculate Tax__c on Opportunity
private static void calculateTax(Opportunity opp){
opp.Tax__c = (opp.Tax_Percentage__c/100) * opp.Amount;
opp.Total_Price__c = opp.Tax__c + opp.Amount;
}
// Runs on Opportunity before insert
global static void beforeInsert(List<Opportunity> newRecords){
//Loop through all new records
for(Opportunity newRecord : newRecords){
//Verify calculation fields are populated
if(newRecord.Tax_Percentage__c != null && newRecord.Amount != null){
calculateTax(newRecord);
}
}
}
//Runs on Opportunity before update
global static void beforeUpdate(Map<Id, Opportunity> newMap, Map<Id, Opportunity> oldMap){
//Loop through new records
for(Id oppId : newMap.keySet()){
Opportunity newRecord = newMap.get(oppId);
Opportunity oldRecord = oldMap.get(oppId);
//Verify calculation fields are populated
if(newRecord.Tax_Percentage__c != null && newRecord.Amount != null){
//Verify that Amount or Tax_Percentage__c changed on record
if(newRecord.Amount != oldRecord.Amount ||
newRecord.Tax_Percentage__c != oldRecord.Tax_Percentage__c){
calculateTax(newRecord);
}
}
}
}
}