Download as pdf or txt
Download as pdf or txt
You are on page 1of 49

36 trigger for practice .

pdf

Note:- Code is highlighted in a yellow background color.

27th June 2023

1. What is Trigger?

Trigger in Salesforce is essentially an Apex script used by developers before or after events
related to data manipulation language (DML). Apex triggers enable you to perform custom
actions before or after changes to Salesforce records, such as insertions, updates, or
deletions. Apex trigger gets executed when you perform a DML operation either from UI or
Apex.

2. How to create an Apex Trigger?

1st Way:-
In Developer Console → File → New → Apex Trigger → Write name of trigger
and select sObject.

2nd Way:-
Inside Object Manager → Select Object on which you want to create trigger
→ On the left pane select trigger → Click new button
3. Trigger Syntax

trigger TriggerName on ObjectAPIName (trigger_events) {


//code_block
}

4. Trigger events in salesforce?

A trigger is a set of statements that can be executed on the following events. In trigger
events, one or more of the below events can be used with comma-separated.
● before insert
● before update
● before delete
● after insert
● after update
● after delete
● after undelete

5. What are the different types of Triggers?

● Before triggers are used to perform a task before a record is inserted or updated or
deleted. These are used to update or validate record values before they are saved to
the database.
● After triggers are used if we want to use the information set by the Salesforce
system and to make changes in the other records. are used to access field values
that are set by the system (such as a record’s Id or LastModifiedDate field), and to
affect changes in other records. The records that fire the after the trigger is read-only.

6. What are context variables in triggers?

● isExecuting: Returns true if the current context for the Apex code is a trigger, not a
Visualforce page, a Web service, or an executeanonymous() API call.

● isInsert: Returns true if this trigger was fired due to an insert operation, from the
Salesforce user interface, Apex, or the API.

● isUpdate: Returns true if this trigger was fired due to an update operation, from the
Salesforce user interface, Apex, or the API.

● isDelete: Returns true if this trigger was fired due to a delete operation, from the
Salesforce user interface, Apex, or the API.
● isBefore: Returns true if this trigger was fired before any record was saved.

● isAfter: Returns true if this trigger was fired after all records were saved.

● isUndelete: Returns true if this trigger was fired after a record is recovered from the
Recycle Bin (that is, after an undelete operation from the Salesforce user interface,
Apex, or the API.)

● new: Returns a list of the new versions of the sObject records. This sObject list is
only available in insert, update, and undelete triggers, and the records can only be
modified in before triggers.

● newMap: A map of IDs to the new versions of the sObject records. This map is only
available in before update, after insert, after update, and after undelete triggers.

● old : Returns a list of the old versions of the sObject records. This sObject list is only
available in update and delete triggers.

● oldMap: A map of IDs to the old versions of the sObject records. This map is only
available in update and delete triggers.

● size: The total number of records in a trigger invocation, both old and new.

● operationType: Explained later.

7. What is Trigger.new vs Trigger.old vs Trigger.oldMap vs


Trigger.newMap?

Trigger.new always has new values of the record.


Trigger.old always has previous values of the record.

Both of the above have return type as List.


Look at the below code:-

trigger OpportunityTrigger on Opportunity (before update) {

List<Opportunity> newList = Trigger.new;


List<Opportunity> oldList = Trigger.old;

System.debug('newList : '+newList);
System.debug('oldList : '+oldList);
}
Trigger.newMap contains new values of record on value side and id on key side.
Trigger.oldMap contains old values of record on value side and id on key side.

Both of the above have return type of map. Please look at the below code:-

trigger OpportunityTrigger on Opportunity (before update) {

Map<Id, Opportunity> newMap = Trigger.newMap;


Map<Id, Opportunity> oldMAp = Trigger.oldMap;

System.debug('newMap : '+newMap);
System.debug('oldMAp : '+oldMAp);
}

8. Sizes:-

Trigger.new, Trigger,old, Trigger.oldMap and Trigger.newMap these collections have


a maximum of 200 records.

Assignment:-

1. Please practice everything including context variables deeply.


2. Practice on Trigger.new, Trigger.old, Trigger.newMap,
Trigger.oldMap by debugging their values in various trigger
events.
28th June 2023

1. Why there is no before undelete?

An update or Insert can have a Before functionality as the record exists.


Where as there is no Record before deleting the Actual record.
Hence only after deleting we can have the Undelete event, whereas every record merely
exists before deleting

2. What is Trigger.isExecuting?

If you are calling any apex class from trigger then Trigger.isExecuting returns true.
Suppose take a look at below class:-

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (before update) {

OpportunityTriggerHelper.myMethod();
}

OpportunityTriggerHelper.apxc

public class OpportunityTriggerHelper {

public static void myMethod() {

if(Trigger.isExecuting) {
System.debug('Trigger Code');
} else {
System.debug('Non Trigger Code');
}
}
}

If OpportunityTriggerHelper.myMethod() is getting called from trigger then


System.debug('Trigger Code'); this part will get executed.
If OpportunityTriggerHelper.myMethod() is getting called from other than trigger
then System.debug('Non Trigger Code'); this part will get executed.
3. How to use Switch-when in Apex?

public class SwitchClass {

public static void cities(String cityName) {

if(cityName == 'Pune')
{
System.debug('I am inside pune');
}
else if(cityName == 'Nasik')
{
System.debug('I am inside Nasik');
}
else if(cityName == 'Mumbai')
{
System.debug('I am inside Mumbai');
}
else
{
System.debug('I am inside other city');
}
}
}

The same above code can be written as below:-

public class SwitchClass {

public static void cities(String cityName) {

switch on cityName {

when 'Pune' {
System.debug('I am inside pune');
}

when 'Nasik' {
System.debug('I am inside Nasik');
}

when 'Mumbai' {
System.debug('I am inside Mumbai');
}
when else {
System.debug('I am inside other city');
}
}
}
}

4. Enum Apex Class

● An enum is an abstract data type with values that each take on exactly one of
a finite set of identifiers that you specify. Enums are typically used to define a
set of possible values that don’t otherwise have a numerical order, such as
the suit of a card, or a particular season of the year.
● The easy way to think of them are compile-time “constants” that can be
statically typed. They’re sort of like a list of static strings, in that sense, and
they obey some interesting rules in addition to the basics:
● All enums have access to two methods: name() and ordinal() - name simply
returns the string version of your enum instance (the equivalent of toString for
enums), ordinal returns its index in the “list” — FirstValue would have an index
of 0 in the above example; SecondValue would have an index of 1.

5. Enum Example:-

public class EnumClass {

public enum Cities {PUNE, MUMBAI, HYDERABAD, BANGLORE, DELHI}

public static void enumMethods() {

Cities city = Cities.PUNE;


System.debug('Ordinal : '+city.ordinal()); //Returns Integer.
System.debug('Name : '+city.name()); //Returns String.
System.debug('Values : '+Cities.values()); //Returns List<String>
}
}
6. Using Switch when with Trigger.operationType:-

trigger AccountTrigger on Account (before update, before insert, before delete, after
update, after insert, after delete, after undelete) {

switch on Trigger.operationType {

when BEFORE_UPDATE {

//Write before update logic here


}

when BEFORE_INSERT {

//Write before insert logic here


}

when BEFORE_DELETE {

//Write before delete logic here


}

when AFTER_UPDATE {
//Write after update logic here
}

when AFTER_INSERT{

//Write after insert logic here


}

when AFTER_DELETE{

//Write after delete logic here


}

when AFTER_UNDELETE {

//Write after undelete logic here


}
}
}

7. Points to Remember

● You cannot perform DML on Trigger.new or Trigger.old


● Trigger.old and after triggers are read only.
● Trigger.new is editable but only in before triggers.
● You cannot use SOSL in trigger.
● Before triggers are used to update the same record or for validation purposes.
● Always bulkify your trigger.
● Do not use SOQL or DML inside for loops.
● Add null check wherever necessary.
29th June 2023

1. Write a trigger such that when Account Rating is Cold then


type must be prospect.

AccountTrigger.apxt

trigger AccountTrigger on Account (before insert) {

if(Trigger.isBefore) {
if(Trigger.isInsert) {
AccountTriggerHandler.typeProspect(Trigger.new);
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void typeProspect(List<Account> accNewList) {

for(Account acc : accNewList) {

if(acc.Rating == 'Cold') {
acc.Type = 'Prospect';
}
}
}
}

2. Write a trigger such that Rating is required field.

AccountTrigger.apxt

trigger AccountTrigger on Account (before update, before insert) {

if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
AccountTriggerHandler.ratingValidation(Trigger.new);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void ratingValidation(List<Account> accNewList) {

for(Account acc : accNewList) {

if(acc.Rating == null) {
acc.addError(Account.Rating, 'Rating cannot be empty');
}
}
}
}

3. Write a trigger such that when Account Email is changed then


isActive must be true.

AccountTrigger.apxt

trigger AccountTrigger on Account (before update) {

if(Trigger.isBefore) {
if(Trigger.isUpdate) {
AccountTriggerHandler.isActiveCheck(Trigger.new, Trigger.old);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void isActiveCheck(List<Account> accNewList, List<Account>


accOldList) {

for(Account accNew : accNewList) {

for(Account accOld : accOldList) {

if(accNew.Id == accOld.Id) {
if(accNew.Email__c != accOld.Email__c) {
accNew.isActive__c = true;
}
}
}
}
}
}

Do not try this above way use the below way:-

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void isActiveCheck(List<Account> accNewList, Map<Id, Account>


accOldMap) {

for(Account accNew : accNewList) {

//Account accOldRecord = accOldMap.get(accNew.Id);

if(accNew.Email__c != accOldMap.get(accNew.Id).Email__c) {
accNew.isActive__c = true;
}
}
}
}
Assignment:-

1. Write update code for #1 trigger above.


2. Write a trigger such that if Account Rating is ‘Cold’ then
Account Number must be a required field.
30th June 2023

1. Write a trigger such that when student enters his mobile no,
then make sure that mobile no is of 10 digits.

StudentTrigger.apxt

trigger StudentTrigger on Student__c (before insert, before update) {

if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
StudentTriggerHandler.mobileValidation(Trigger.new);
}
}
}

StudentTriggerHandler.apxc

public class StudentTriggerHandler {

public static void mobileValidation(List<Student__c> stdNewList) {

for(Student__c std : stdNewList) {


String mobileNo = std.Mobile_No__c;

if(std.Mobile_No__c != null && mobileNo.length() != 10) {


std.addError('Mobile no should be of 10 digits');
}
}
}
}

2. Write a trigger such that if Opportunity is Closed Won then


the record should be prevented from getting deleted.

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (before delete) {

if(Trigger.isBefore) {
if(Trigger.isDelete) {
OpportunityTriggerHandler.preventDeletion(Trigger.old);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void preventDeletion(List<Opportunity> oppOldList) {

for(Opportunity opp : oppOldList) {


if(opp.StageName == 'Closed Won') {
opp.addError('Closed Won Opp cannot be deleted');
}
}
}
}

3. Write a trigger such that if account has atleast one contact


then account must be non editable.

AccountTrigger.apxt

trigger AccountTrigger on Account (before update) {

if(Trigger.isBefore) {
if(Trigger.isUpdate) {
AccountTriggerHandler.preventUpdate(Trigger.new);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void preventUpdate(List<Account> accNewList) {

Map<Id, Account> parentChildMap = new Map<Id, Account>([SELECT Id,


(SELECT Id FROM Contacts LIMIT 1) FROM Account WHERE Id = :accNewList]);

for(Account acc : accNewList) {


//Account mapAccount = parentChildMap.get(acc.Id);
if(!parentChildMap.get(acc.Id).Contacts.isEmpty()) {
acc.addError('You cannot edit this record.');
}
}
}
}

Assignment:-

1. Write a trigger such that if the Opportunity Stage Name is


Closed Won or Closed Lost then the Close Date must be
populated with today's date.

Note:- In this trigger when you write before update run the
trigger only when the stage name will be changed and it must
be Closed Won or Closed Lost. Write two separate methods
one method before insert and another method before update
and call these methods using the appropriate way from the
trigger.
2. Create a custom object Calculator. Create below fields on the
object:-

● Number 1
● Number 2
● Addition
● Subtraction
● Multiplication
● Division

All the above fields must have a number data type when
creating these fields. (Do not create formula fields here)
Write a trigger such that Number 1 and Number 2 will be
required fields. If Number 1 and Number 2 are not blank then
Addition, Subtraction, Multiplication and Division fields must
gets calculated based on Number 1 and Number 2.
If you have any questions regarding this please ping me once.

3. Write a trigger such that if an Account has any Child


Opportunity having StageName as ‘Closed Won’ then prevent
deletion of that Account.
4. Write a trigger such that when you create a new Lead with
Email as not null. Check if an existing lead is present with the
same email. If present throw an error message that “Duplicate
lead found with same email”.
5. Write a trigger on Account, when an account is inserted or
updated, automatically account billing address should
populate into the account shipping address.
6. Write a trigger such that age must be single or double digit.
3rd July 2023
1. Assignment 1 Solution:-

1st Way:-
By using different methods inside handler one for insert and other one for update

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (before insert, before update) {

if(Trigger.isBefore) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.setCloseDate_Insert(Trigger.new);
}

if(Trigger.isUpdate) {
OpportunityTriggerHandler.setCloseDate_Update(Trigger.new,
Trigger.oldMap);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void setCloseDate_Insert(List<Opportunity> oppNewList) {

for(Opportunity opp : oppNewList) {

if(opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost') {


opp.CloseDate = System.today();
}
}
}

public static void setCloseDate_Update(List<Opportunity> oppNewList, Map<Id,


Opportunity> oppOldMap) {

for(Opportunity opp : oppNewList) {

if(opp.StageName != oppOldMap.get(opp.Id).StageName) {
if(opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost') {
opp.CloseDate = System.today();
}
}
}
}
}

2nd Way:-
By using single method inside handler for insert and as well as for update

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (before insert, before update) {

if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
OpportunityTriggerHandler.setCloseDate(Trigger.new, Trigger.oldMap);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void setCloseDate(List<Opportunity> oppNewList, Map<Id,


Opportunity> oppOldMap) {

//Update
if(oppOldMap != null) {
for(Opportunity opp : oppNewList) {

if(opp.StageName != oppOldMap.get(opp.Id).StageName) {
if(opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost')
{
opp.CloseDate = System.today();
}
}
}
}
//Insert
else {
for(Opportunity opp : oppNewList) {

if(opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost') {


opp.CloseDate = System.today();
}
}
}
}
}

2. Assignment 2 Solution:-

CalculatorTrigger.apxt

trigger CalculatorTrigger on Calculator__c (before insert, before update) {

if(Trigger.isBefore) {

if(Trigger.isInsert || Trigger.isUpdate) {
CalculatorTriggerHandler.calculation(Trigger.new);
}
}
}

CalculatorTriggerHandler.apxc

public class CalculatorTriggerHandler {

public static void calculation(List<Calculator__c> calculatorNewList) {

for(Calculator__c calc : calculatorNewList) {

if(calc.Number_1__c == null || calc.Number_2__c == null) {


calc.addError('Number 1 and Number 2 cannot be empty');
}
else {
calc.Addition__c = calc.Number_1__c + calc.Number_2__c;
calc.Subtraction__c = calc.Number_1__c - calc.Number_2__c;
calc.Multiplication__c = calc.Number_1__c * calc.Number_2__c;

if(calc.Number_2__c != 0)
calc.Division__c = calc.Number_1__c / calc.Number_2__c;
else
calc.Division__c = 0;
}
}
}
}
3. Assignment 3 Solution:-

AccountTrigger.apxt

trigger AccountTrigger on Account (before delete) {

if(Trigger.isBefore) {
if(Trigger.isDelete) {
AccountTriggerHandler.preventAccountDeletion(Trigger.old);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void preventAccountDeletion(List<Account> accOldList) {

Map<Id, Account> accountOppMap = new Map<Id, Account>([SELECT Id,


(SELECT Id FROM Opportunities WHERE StageName = 'Closed Won' LIMIT 1)
FROM Account WHERE Id = :accOldList]);

for(Account acc : accOldList) {

if(!accountOppMap.get(acc.Id).Opportunities.isEmpty()) {
acc.addError('This account has Closed Won Opportunities');
}
}
}
}

4. Assignment 4 Solution:-

LeadTrigger.apxt

trigger LeadTrigger on Lead (before insert) {


if(Trigger.isBefore) {
if(Trigger.isInsert) {
LeadTrigger_Handler.duplicateLeads(Trigger.new);
}
}
}

LeadTrigger_Handler.apxc

public class LeadTrigger_Handler {

public static void duplicateLeads(List<Lead> leadNewList) {

Set<String> emails = new Set<String>();

for(Lead ld : leadNewList) {
if(ld.Email != null) {
emails.add(ld.Email);
}
}

List<Lead> leadEmailList = [SELECT Id, Email FROM Lead WHERE Email IN :


emails];

emails.clear();

for(Lead ld : leadEmailList) {
emails.add(ld.Email);
}

for(Lead ld : leadNewList) {
if(emails.contains(ld.Email)) {
ld.addError('Duplicate lead found with same email');
}
}
}
}

5. Assignment 5 Solution:-

AccountTrigger.apxt
trigger AccountTrigger on Account (before insert, before update) {

if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
AccountTriggerHandler.shippingAddress(Trigger.new);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void shippingAddress(List<Account> accNewList) {

for(Account acc : accNewList) {


acc.ShippingCity = acc.BillingCity;
acc.ShippingCountry = acc.BillingCountry;
acc.ShippingGeocodeAccuracy = acc.BillingGeocodeAccuracy;
acc.ShippingLatitude = acc.BillingLatitude;
acc.ShippingLongitude = acc.BillingLongitude;
acc.ShippingPostalCode = acc.BillingPostalCode;
acc.ShippingState = acc.BillingState;
acc.ShippingStreet = acc.BillingStreet;
}
}
}

6. Assignment 6 Solution:-

AccountTrigger.apxt

trigger AccountTrigger on Account (before update, before insert) {

if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
AccountTriggerHandler.ageValidation(Trigger.new);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {


public static void ageValidation(List<Account> accNewList) {

for(Account acc : accNewList) {

String age = String.valueOf(acc.Age__c);


if(acc.Age__c != null && age.length() >= 3) {
acc.addError('Age must be single or double digit');
}
}
}
}

7. Once an Account will update then that Account will update


with the total amount from All its Opportunities on the
Account Level. The account field name would be "Total
Opportunity Amount".

AccountTrigger.apxt

trigger AccountTrigger on Account (before update) {

if(Trigger.isBefore) {
if(Trigger.isUpdate) {
AccountTriggerHandler.updateOppAmount(Trigger.new);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void updateOppAmount(List<Account> accNewList) {

Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id, (SELECT


Amount FROM Opportunities) FROM Account WHERE Id = :accNewList]);

//Contact + Opportunity
for(Account acc : accNewList) {
acc.Opportunity_Amount_Total__c = 0;

//List<Opportunity> oppList = accMap.get(acc.Id).Opportunities;


for(Opportunity opp : accMap.get(acc.Id).Opportunities) {
acc.Opportunity_Amount_Total__c = acc.Opportunity_Amount_Total__c +
opp.Amount;
}
}
}
}

8. Prefix first name with Er. whenever lead is created or updated.

LeadTrigger.apxt

trigger LeadTrigger on Lead (before insert, before update) {

if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
LeadTriggerHandler.nameWithEr(Trigger.new);
}
}
}

LeadTriggerHandler.apxc

public class LeadTriggerHandler {

public static void nameWithEr(List<Lead> leadNewList) {

for(Lead ld : leadNewList) {
if(!ld.LastName.startsWith('Er. ')) {
ld.LastName = 'Er. ' + ld.LastName;
}
}
}
}

9. Apex class which stores Account Id as key and value as its


related child contacts in a map
public class myApexClass {

public static void myMethod() {

Map<String, List<Contact>> accConMap = new Map<String, List<Contact>>();


List<Contact> conList = [SELECT Id, Name, AccountId FROM Contact WHERE
AccountId != null];

for(Contact con : conList) {


if(!accConMap.containsKey(con.AccountId)) {
accConMap.put(con.AccountId, new List<Contact>{con});
} else {
accConMap.get(con.AccountId).add(con);
}
}

System.debug(accConMap);
}
}

a) When iterating on Contact List if we simply use


accConMap.put(con.AccountId, new List<Contact>{con}); to add values
inside map. This will work only when one Account has only one Contact.
b) But in real-time scenarios one Account can have multiple Contacts. So using
the above-highlighted code will erase the previous value from the Map and
add the new value in the next iteration.
c) We need to keep the previous value as it is in the map. Hence, first, check
whether the current iteration of Contact’s AccountId is already present in the
map or not? If present get that particular value using
accConMap.get(con.AccountId) this will return you the List of Contact, so
we will add the current iteration Contact record to this list using
accConMap.get(con.AccountId).add(con);
10. Order of Execution:-

1. Loads the original record from the database.

2. Loads the new record field values from the request and overwrites the old values.

3. Executes record-triggered flows that are configured to run before the record is saved.

4. Executes all before triggers.

5. Runs most system validation steps again, such as verifying that all required fields
have a non-null value, and runs any custom validation rules.

6. Executes duplicate rules. (If the duplicate rule identifies the record as a duplicate and
uses the block action, the record isn’t saved and no further steps, such as after
triggers and workflow rules, are taken)

7. Saves the record to the database, but doesn't commit yet.

8. Executes all after triggers.

9. Executes assignment rules.

10. Executes auto-response rules.

11. Executes workflow rules.

12. Executes escalation rules.


13. Executes these Salesforce Flow automations, but not in a guaranteed order.
a. Processes
b. Flows launched by processes
c. Flows launched by workflow rules (flow trigger workflow actions pilot)
d. When a process or flow executes a DML operation, the affected record goes
through the save procedure.

14. Executes record-triggered flows that are configured to run after the record is saved.

15. Executes entitlement rules.

16. If the record contains a roll-up summary field or is part of a cross-object workflow,
performs calculations and updates the roll-up summary field in the parent record.
Parent record goes through save procedure.

17. If the parent record is updated, and a grandparent record contains a roll-up summary
field or is part of a cross-object workflow, performs calculations and updates the
roll-up summary field in the grandparent record. Grandparent record goes through
save procedure.

18. Executes Criteria Based Sharing evaluation.

19. Commits all DML operations to the database.

20. After the changes are committed to the database, executes post-commit logic are
executed. Examples of post-commit logic (in no particular order) include:
a. Sending email
b. Enqueued asynchronous Apex jobs, including queueable jobs and future
methods
c. Asynchronous paths in record-triggered flows

11. Apex Trigger Best Practices:-

1) One Trigger Per Object


A single Apex Trigger is all you need for one particular object. If you develop multiple
Triggers for a single object, you have no way of controlling the order of execution if
those Triggers can run in the same contexts

2) Context-Specific Handler Methods


Create context-specific handler methods in Trigger handlers

3) Bulkify your Code


Bulkifying Apex code refers to the concept of making sure the code properly handles
more than one record at a time.
4) Avoid SOQL Queries or DML statements inside FOR Loops
An individual Apex request gets a maximum of 100 SOQL queries before exceeding
that governor limit. So if this trigger is invoked by a batch of more than 100 Account
records, the governor limit will throw a runtime exception

5) Using Collections, Streamlining Queries, and Efficient For Loops


It is important to use Apex Collections to efficiently query data and store the data in
memory. A combination of using collections and streamlining SOQL queries can
substantially help writing efficient Apex code and avoid governor limits

6) Querying Large Data Sets


The total number of records that can be returned by SOQL queries in a request is
50,000. If returning a large set of queries causes you to exceed your heap limit, then
a SOQL query for loop must be used instead. It can process multiple batches of
records through the use of internal calls to query and queryMore

7) Use @future Appropriately


It is critical to write your Apex code to efficiently handle bulk or many records at a
time. This is also true for asynchronous Apex methods (those annotated with the
@future keyword). The differences between synchronous and asynchronous Apex
can be found

8) Avoid Hardcoding IDs


When deploying Apex code between sandbox and production environments, or
installing Force.com AppExchange packages, it is essential to avoid hardcoding IDs
in the Apex code. By doing so, if the record IDs change between environments, the
logic can dynamically identify the proper data to operate against and not fail.

Few more Best Practices for Triggers


● There should only be one trigger for each object.
● Avoid complex logic in triggers. To simplify testing and resuse, triggers should
delegate to Apex classes which contain the actual execution logic. See Mike
Leach's excellent trigger template for more info.
● Bulkify any "helper" classes and/or methods.
● Triggers should be "bulkified" and be able to process up to 200 records for
each call.
● Execute DML statements using collections instead of individual records per
DML statement.
● Use Collections in SOQL "WHERE" clauses to retrieve all records back in
single query
● Use a consistent naming convention including the object name (e.g.,
AccountTrigger)

Assignment:-
1. Update Parent Account rating to 'Hot' when opportunity stage
name is 'closed won'.
2. Whenever the phone field is updated in the account then the
name field should also get updated with the name and phone
number.
Ex:- if name is “Test” and phone is 123456 then name must
get changed to “Test - 123456”
3. Prevent account from deleting, if it has 2 or more contacts.
4. While creating lead or updating, check if Email already exist in
contacts or not, if exists throw an error.
4th July 2023

1. When to use after and before triggers?

Before triggers can be used to update or validate record values before they are
saved to the database.

After triggers can be used to access field values that are set by the database (such
as a record's Id or lastUpdated field) and to affect changes in other records, such as
logging into an audit table or firing asynchronous events with a queue.

Use Before Trigger:


● In the case of validation check in the same object.
● Insert or update the same record.

Use After Trigger:


● Insert/Update related object, not the same object.
● Notification email.
● We cannot use After trigger if we want to update a record because it causes
read only error. This is because, after inserting or updating, we cannot update
a record.

2. Write a trigger such that when opportunity is inserted Closed


Date must be set to today date:-

This code has to be done using before trigger only, because we are updating the
same record which fired the trigger. See the below before trigger:-

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (before insert) {

if(Trigger.isBefore) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.setClosedDate(Trigger.new);
}
}
}
OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void setClosedDate(List<Opportunity> oppNewList) {

for(Opportunity opp : oppNewList) {


opp.CloseDate = System.today();
}
}
}

Trying the same above code using after trigger:-

Remember:- The below after trigger is just for understanding the difference between
before and after trigger. Never use after context for before

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (after insert) {

if(Trigger.isAfter) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.setClosedDate(Trigger.new);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void setClosedDate(List<Opportunity> oppNewList) {

List<Opportunity> oppToUpdate = new List<Opportunity>();

for(Opportunity opp : oppNewList) {

Opportunity oppNew = new Opportunity();


oppNew.Id = opp.Id;
oppNew.CloseDate = System.today();

oppToUpdate.add(oppNew);
}
if(!oppToUpdate.isEmpty()) {
update oppToUpdate;
}
}
}

3. Context Variable Considerations

Can update Can delete


Can change fields original object original object
Trigger Event
using trigger.new using an update using a delete
DML operation DML operation

before insert Allowed. Not applicable. The Not applicable. The


original object has original object has
not been created; not been created;
nothing can nothing can
reference it, so reference it, so
nothing can update nothing can update
it. it.

after insert Not allowed. A Allowed. Allowed, but


runtime error is unnecessary. The
thrown, as object is deleted
trigger.new is immediately after
already saved. being inserted.

before update Allowed. Not allowed. A Not allowed. A


runtime error is runtime error is
thrown. thrown.

after update Not allowed. A Allowed. Even Allowed. The


runtime error is though bad code updates are saved
thrown, as could cause an before the object is
trigger.new is infinite recursion deleted, so if the
already saved. doing this object is undeleted,
incorrectly, the the updates
error would be become visible.
found by the
governor limits.

before delete Not allowed. A Allowed. The Not allowed. A


runtime error is updates are saved runtime error is
thrown. trigger.new before the object is thrown. The
is not available in deleted, so if the deletion is already
before delete object is undeleted, in progress.
triggers. the updates
become visible.

after delete Not allowed. A Not applicable. The Not applicable. The
runtime error is object has already object has already
thrown. trigger.new been deleted. been deleted.
is not available in
after delete
triggers.

after undelete Not allowed. A Allowed. Allowed, but


runtime error is unnecessary. The
thrown. object is deleted
immediately after
being inserted.

4. Update Account rating to 'Hot' when opportunity stage name


is 'closed won'.
1st Way:- (Do not try this method)

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (after insert, after update) {

if(Trigger.isAfter) {
if(Trigger.isInsert || Trigger.isUpdate) {
OpportunityTriggerHandler.updateParent(Trigger.new);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void updateParent(List<Opportunity> oppNewList) {

List<Account> accToUpdate = new List<Account>();

for(Opportunity opp : oppNewList) {


if(opp.StageName == 'Closed Won' && opp.AccountId != null) {
Account acc = new Account();
acc.Id = opp.AccountId;
acc.Rating = 'Hot';
accToUpdate.add(acc);
}
}

if(!accToUpdate.isEmpty()) {
update accToUpdate;
}
}
}

2nd Way:-

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (after insert, after update) {

if(Trigger.isAfter) {
if(Trigger.isInsert || Trigger.isUpdate) {
OpportunityTriggerHandler.updateParent(Trigger.new);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void updateParent(List<Opportunity> oppNewList) {

Set<Id> accIds = new Set<Id>();


List<Account> accToUpdate = new List<Account>();

for(Opportunity opp : oppNewList) {

if(opp.StageName == 'Closed Won' && opp.AccountId != null) {


accIds.add(opp.AccountId);
}
}

for(Id accId : accIds) {

Account acc = new Account();


acc.Id = accId;
acc.Rating = 'Hot';
accToUpdate.add(acc);
}

if(!accToUpdate.isEmpty()) {
update accToUpdate;
}
}
}
5th July 2023

1. Update all child contacts email with its parent Account email.

AccountTrigger.apxt

trigger AccountTrigger on Account (after update) {

if(Trigger.isAfter) {
if(Trigger.isUpdate) {
AccountTriggerHandler.updateChildContacts(Trigger.new);
}
}
}

AccountTriggerHandler.apxc

public class AccountTriggerHandler {

public static void updateChildContacts(List<Account> accNewList) {

List<Account> accList = [SELECT Id, Email__c, (SELECT Id, Email FROM


Contacts)
FROM Account WHERE Id = :accNewList AND Email__c !=
null];
List<Contact> conToUpdate = new List<Contact>();

for(Account acc : accList) {


for(Contact con : acc.Contacts) {
con.Email = acc.Email__c;
conToUpdate.add(con);
}
}

if(!conToUpdate.isEmpty()) {
update conToUpdate;
}
}
}

2. Define Recursive Trigger


A recursive trigger is one that performs an action, such as an update or insert, which
invokes itself owing to, say something like an update it performs.

Recursion is the process of executing the same task multiple times. There may be
chances to hit the Governor Limit with Recursive Trigger.

Trigger recursion happens 16 times. I.e., 15 times it gets called itself and fails on the
16th time.

3. Avoid Trigger Recursion

To avoid this scenario we should create a static variable and check the value of this
variable before we execute anything in the trigger.

Below is an example of the recursive trigger and how to avoid it

4. Create a duplicate lead when a lead is inserted.

LeadTrigger.apxt

trigger LeadTrigger on Lead (after insert) {

if(Trigger.isAfter) {
if(Trigger.isInsert) {
if(LeadTriggerHandler.executeTrigger) {
LeadTriggerHandler.executeTrigger = false;
LeadTriggerHandler.insertDuplicate(Trigger.new);
}
}
}
}

LeadTriggerHandler.apxc

public class LeadTriggerHandler {

public static Boolean executeTrigger = true;

public static void insertDuplicate(List<Lead> leadNewList) {

List<Lead> leadToInsert = new List<Lead>();


for(Lead ld : leadNewList) {
Lead ldDuplicate = new Lead();
ldDuplicate.LastName = ld.LastName + ' Duplicate';
ldDuplicate.Company = ld.Company;
ldDuplicate.Status = ld.Status;
leadToInsert.add(ldDuplicate);
}

if(!leadToInsert.isEmpty()) {
insert leadToInsert;
}
}
}

5. GROUP BY Clause:-

You can use the GROUP BY option in a SOQL query to avoid iterating through
individual query results. That is, you specify a group of records instead of processing
many individual records.

GROUP BY clause is used in SOQL query to group set of records by the values
specified in the field. We can perform aggregate functions using GROUP BY clause.

Aggregated functions for GROUP BY clause:

● COUNT ()
● COUNT (FIELD_NAME)
● COUNT_DISTINCT ()
● SUM ()
● MIN ()
● MAX ()

1. SELECT Address__c, count(id) FROM Student__c GROUP BY


Address__c

The above query will return count of students as per their address. Like from
pune there are 6 students, from mumbai there are 3 students, etc.

2. SELECT Address__c, count(id), sum(Fees_Paid__c) FROM Student__c


GROUP BY Address__c
The above query will return count of students and sum of their Fees paid as
per their address. (Address is a text field)

3. SELECT Name__c, Address__c, count(id), sum(Fees_Paid__c) FROM


Student__c GROUP BY Address__c, Name__c

The above query will return count of students and sum of their Fees paid as
per their address and name. (Address is a text field)

Some object fields have a field type that does not support grouping. You can’t include
fields with these field types in a GROUP BY clause.

Following are the list of Groupable & Non-Groupable field types:

Groupable Field Types:

● Id (Id)
● Lookup (Id)
● Checkbox (Boolean)
● Phone (String)
● Picklist (String)
● Email (String)
● Text (String)
● Text Area (String)
● URL (String)
● Number (Int). Does not include custom fields, only standard Number fields
with SOAP type int, like Account.NumberOfEmployees.
● Date (date)
● Direct cross-object references to groupable fields, up to 5 levels from the root
object (SOQL limit), as in SELECT count(Id) FROM Contact GROUP BY
Account.Parent.Parent.Parent.Parent.Name. Both custom and standard
references are groupable.
● Formulas of type Checkbox and Date, including cross-object formulas across
standard and custom relationships.

Non-Groupable Field Types:

● Auto Number (string)


● Address Compound Fields
● Number (double), including custom Number fields with or without decimal and
regardless of scale.
● Percent (double), including custom Percent fields with or without decimal and
regardless of scale.
● Currency (double), including custom Currency fields with or without decimal
and regardless of scale.
● Components of Address compound fields are groupable if their types
otherwise allow it.
● Geolocations, both custom and standard, and whether or not defined as
having decimal places, including the compound field and components
(location/double)
● Long Text (string)
● Rich Text (string)
● Multi-Select Picklist (string)
● Roll-Up Summary Fields (double), including COUNT rollups.
● Encrypted Text Fields (Classic Encryption; string)
● Date/Time (dateTime)
● Time (time)
● Formulas of types other than Checkbox and Date, including the
otherwise-groupable String type.

6. Aggregate Result:-

Aggregate functions in salesforce include AVG(), COUNT(), MIN(), MAX(), SUM().The


functions like SUM() and MAX() in SOQL allow to roll up and summarize the data in
a query.

The GROUP BY clause in a SOQL query is to avoid iterating through individual query
results and used to specify a group of records instead of processing many individual
records.

● AVG() – Returns the average value of a numeric field


● COUNT() – Returns the number of rows matching the query criteria
● MIN() – Returns the minimum value of a field
● MAX() – Returns the maximum value of a field
● SUM() – Returns the total sum of a numeric field

A query that includes an aggregate function returns its results in an array of


AggregateResult objects. AggregateResult is a read-only sObject and is only used
for query results. The values in the AggregateResult object can be accessed much
like a map calling a “get” method with the name of the column.

See below some of the examples:-

Example 1:-

Just debugging the AggregateResult List


public class AggregateClass {

public static void aggregateMethod() {

List<AggregateResult> oppList = [SELECT count(id), sum(amount),


min(amount), max(amount) FROM Opportunity];

System.debug('Aggregate List : '+oppList);


}
}

Example 2:-

Accessing values inside for loop.

public class AggregateClass {

public static void aggregateMethod() {

List<AggregateResult> oppList = [SELECT count(id), min(amount),


sum(amount), max(amount), avg(amount) FROM Opportunity];

for(AggregateResult ar : oppList) {
System.debug('Count is : '+ar.get('expr0'));
System.debug('Sum Amount is : '+ar.get('expr1'));
System.debug('Min Amount is : '+ar.get('expr2'));
System.debug('Max Amount is : '+ar.get('expr3'));
System.debug('Avg Amount is : '+ar.get('expr4'));
}
}
}

Example 3:-

Giving unique name for the aggregate function inside the query like countOfOpp,
sumAmount, minAmount, maxAmount in the below example

public class AggregateClass {

public static void aggregateMethod() {


List<AggregateResult> aggResult = [SELECT count(id) countOfOpp,
min(amount) minAmount, sum(amount) sumAmount, max(amount) maxAmount,
avg(amount) avgAmount FROM Opportunity];

for(AggregateResult ar : aggResult) {
System.debug('Count is : '+ar.get('countOfOpp'));
System.debug('Min is : '+ar.get('minAmount'));
System.debug('sum is : '+ar.get('sumAmount'));
System.debug('max is : '+ar.get('maxAmount'));
System.debug('avg is : '+ar.get('avgAmount'));
}
}
}

7. Standard Object Apex Sharing:-

AccountShare accShare = new AccountShare();

//Account access (Read, Edit, All)


accShare.AccountAccessLevel = 'Read';

//Which record to be shared


accShare.AccountId = '0015g00001EnRkTAAV';

//Child opportunities access (None, Read, Edit)


accShare.OpportunityAccessLevel = 'Read';

//To whom record need to be shared


accShare.UserOrGroupId = '0055g000008ajOgAAI';

insert accShare;

8. Custom Object Apex Sharing:-

Student__Share stdShare = new Student__Share();

//Which record to be shared


stdShare.ParentId = 'a0g5g000000udPPAAY';

//To whom record need to be shared


stdShare.UserOrGroupId = '0055g000008ajOgAAI';

//Record access (Read, Edit, All)


stdShare.AccessLevel = 'Read';
insert stdShare;

Assignment:-

1. Write a Trigger on Opportunity such that if the current logged


in user is not having a System Administrator profile then that
record must not be editable.
2. Write a trigger on Contact such that if Contact email is
updated then all the contacts having the same previous email
(before updating the email) must also get updated with the
new email value.
For Ex:- Contact 1 and Contact 2 Email = ‘sf@sf.com’. I have
updated Contact 2 Email to ‘salesforce@velocity.com’ then
Contact 1 must get updated with this email.
6th July 2023
1. Assignment 1 Solution:-

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (before update) {

if(Trigger.isBefore) {
if(Trigger.isUpdate) {
OpportunityTriggerHandler.avoidEdit(Trigger.new);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void avoidEdit(List<Opportunity> oppNewList) {


//String ProfileId = UserInfo.getProfileId();
Profile prof = [SELECT Id, Name FROM Profile WHERE Id =
:UserInfo.getProfileId()];

for(Opportunity opp : oppNewList) {


if(prof.Name != 'System Administrator') {
opp.addError('You are a non system admin user');
}
}
}
}

2. Assignment 2 Solution:-

ContactTrigger.apxt

trigger ContactTrigger on Contact (after update) {

if(Trigger.isAfter) {
if(Trigger.isUpdate) {
ContactTriggerHandler.updateContacts(Trigger.old, Trigger.newMap);
}
}
}

ContactTriggerHandler.apxc

public class ContactTriggerHandler {

public static void updateContacts(List<Contact> conOldList, Map<Id, Contact>


conNewMap) {

Set<String> emailSet = new Set<String>();


List<Contact> conToUpdate = new List<Contact>();

for(Contact con : conOldList) {

if(con.Email != null && con.Email != conNewMap.get(con.Id).Email) {


emailSet.add(con.Email);
}
}

List<Contact> conList = [SELECT Id, Email FROM Contact WHERE Email IN :


emailSet];
Map<String, List<Contact>> emailConMap = new Map<String, List<Contact>>();

for(Contact con : conList) {


if(!emailConMap.containsKey(con.Email)) {
emailConMap.put(con.Email, new List<Contact>{con});
} else {
emailConMap.get(con.Email).add(con);
}
}

for(Contact con : conOldList) {

if(emailConMap.containsKey(con.Email)) {
for(Contact sameEmail : emailConMap.get(con.Email)) {
sameEmail.Email = conNewMap.get(con.Id).Email;
conToUpdate.add(sameEmail);
}
}
}

if(!conToUpdate.isEmpty()) {
update conToUpdate;
}
}
}

3. Write a rollup summary trigger which will store Opportunity


count and Opportunity amount sum, min, max, avg on Parent
Account:-

OpportunityTrigger.apxt

trigger OpportunityTrigger on Opportunity (after insert, after update, after delete, after
undelete) {

if(Trigger.isAfter) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.handleInsert(Trigger.new);
}

if(Trigger.isUpdate) {
OpportunityTriggerHandler.handleUpdate(Trigger.new, Trigger.oldMap);
}

if(Trigger.isDelete) {
OpportunityTriggerHandler.handleDelete(Trigger.old);
}

if(Trigger.isUndelete) {
OpportunityTriggerHandler.handleUndelete(Trigger.new);
}
}
}

OpportunityTriggerHandler.apxc

public class OpportunityTriggerHandler {

public static void handleInsert(List<Opportunity> oppNewList) {


Set<Id> accountIds = new Set<Id>();

for(Opportunity opp : oppNewList) {


if(opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
calculateRollUps(accountIds);
}

public static void handleUpdate(List<Opportunity> oppNewList, Map<Id,


Opportunity> oppOldMap) {
Set<Id> accountIds = new Set<Id>();
List<Account> accToUpdate = new List<Account>();

for(Opportunity opp : oppNewList) {


if(opp.AccountId != null && opp.Amount != oppOldMap.get(opp.Id).Amount) {
accountIds.add(opp.AccountId);
}
}
calculateRollUps(accountIds);
}

public static void handleDelete(List<Opportunity> oppOldList) {


Set<Id> accountIds = new Set<Id>();

for(Opportunity opp : oppOldList) {


if(opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
calculateRollUps(accountIds);
}

public static void handleUndelete(List<Opportunity> oppNewList) {


Set<Id> accountIds = new Set<Id>();

for(Opportunity opp : oppNewList) {


if(opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
calculateRollUps(accountIds);
}

public static void calculateRollUps(Set<Id> accountIds) {

List<AggregateResult> oppAggregateResult = [SELECT AccountId, count(id)


countOfOpp, sum(amount) sumAmount, min(amount) minAmount, max(amount)
maxAmount, avg(amount) avgAmount FROM Opportunity WHERE AccountId IN :
accountIds GROUP BY AccountId];
List<Account> accToUpdate = new List<Account>();

for(AggregateResult ar : oppAggregateResult) {
Account acc = new Account();
acc.Id = (Id)ar.get('AccountId');
acc.Opportunity_Count__c = (Decimal)ar.get('countOfOpp');
acc.Opportunity_Max_Amount__c = (Decimal)ar.get('maxAmount');
acc.Opportunity_Min_Amount__c = (Decimal)ar.get('minAmount');
acc.Opportunity_Avg_Amount__c = (Decimal)ar.get('avgAmount');
acc.Opportunity_Amount_Total__c = (Decimal)ar.get('sumAmount');
accToUpdate.add(acc);
}

if(!accToUpdate.isEmpty()) {
update accToUpdate;
}
}
}

Assignment:-

1. Write Rollup summary trigger on Account and Contact. (Use


any field for rollup).
2. Whenever an Account is created Contact should be created
automatically.
3. When an opportunity is inserted or updated, if the stage name
is ‘Closed won’, then add the task.
4. Write a trigger that divides the AnnualRevenue of the Account
and stores it equally on the child opportunities amount field.

You might also like