Developer 1 - Apex Language Basics

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 Strings to complex DateTimes
  • Apex does some datatype checking, so you cannot assign the string 'true' (instead of true) to a Boolean variable as it generates an “Illegal Assignment” error.
  • There are many interesting “helper methods” available by typing the datatype and ., so String., for example.
  • Blob - commonly used for Salesforce files
  • Object - generic Apex datatype, all primitives and more complex types are of type Object
  • 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
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
  • 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 and sObjects. Anytime we have an Id 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 Strings, Integers, and sObjects
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
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 statements
    • if(!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 it
      • throw ex; throws the exception
      • insert 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.
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 by Amount
      • Total_Price__c: Tax__c + Amount
    • We don’t want to run the calculations if Amount or Tax_Percentage__c are not populated (to avoid errors)
    • We don’t want to run the calculations on update if neither Amount nor Tax_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 and Trigger.oldMap to see changes between records. In an Opportunity trigger, both are of type Map<Id, Opportunity>. oldMap contains the Opportunity record prior to the update, and newMap contains the record after the update.
      • Trigger.new: List of records
      • Trigger.newMap, Trigger.oldMap: same records, but as a Map: Map<Id, Opportunity>
    • Be aware that in an insert trigger, you cannot reference Trigger.old or Trigger.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 possible
    • global method means anyone can call it
    • static method means it can be called directly from the handler
    • newMap.keySet() returns the IDs in the Map<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; or newRecord.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);
                }
            }
        }
    }
}