Triggers
Triggers
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.
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
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
● 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.
● 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.
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:-
System.debug('newMap : '+newMap);
System.debug('oldMAp : '+oldMAp);
}
8. Sizes:-
Assignment:-
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
OpportunityTriggerHelper.myMethod();
}
OpportunityTriggerHelper.apxc
if(Trigger.isExecuting) {
System.debug('Trigger Code');
} else {
System.debug('Non Trigger Code');
}
}
}
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');
}
}
}
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');
}
}
}
}
● 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:-
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 {
when BEFORE_INSERT {
when BEFORE_DELETE {
when AFTER_UPDATE {
//Write after update logic here
}
when AFTER_INSERT{
when AFTER_DELETE{
when AFTER_UNDELETE {
7. Points to Remember
AccountTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert) {
AccountTriggerHandler.typeProspect(Trigger.new);
}
}
AccountTriggerHandler.apxc
if(acc.Rating == 'Cold') {
acc.Type = 'Prospect';
}
}
}
}
AccountTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
AccountTriggerHandler.ratingValidation(Trigger.new);
}
}
}
AccountTriggerHandler.apxc
if(acc.Rating == null) {
acc.addError(Account.Rating, 'Rating cannot be empty');
}
}
}
}
AccountTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isUpdate) {
AccountTriggerHandler.isActiveCheck(Trigger.new, Trigger.old);
}
}
}
AccountTriggerHandler.apxc
if(accNew.Id == accOld.Id) {
if(accNew.Email__c != accOld.Email__c) {
accNew.isActive__c = true;
}
}
}
}
}
}
AccountTriggerHandler.apxc
if(accNew.Email__c != accOldMap.get(accNew.Id).Email__c) {
accNew.isActive__c = true;
}
}
}
}
Assignment:-
1. Write a trigger such that when student enters his mobile no,
then make sure that mobile no is of 10 digits.
StudentTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
StudentTriggerHandler.mobileValidation(Trigger.new);
}
}
}
StudentTriggerHandler.apxc
OpportunityTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isDelete) {
OpportunityTriggerHandler.preventDeletion(Trigger.old);
}
}
}
OpportunityTriggerHandler.apxc
AccountTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isUpdate) {
AccountTriggerHandler.preventUpdate(Trigger.new);
}
}
}
AccountTriggerHandler.apxc
Assignment:-
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.
1st Way:-
By using different methods inside handler one for insert and other one for update
OpportunityTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.setCloseDate_Insert(Trigger.new);
}
if(Trigger.isUpdate) {
OpportunityTriggerHandler.setCloseDate_Update(Trigger.new,
Trigger.oldMap);
}
}
}
OpportunityTriggerHandler.apxc
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
if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
OpportunityTriggerHandler.setCloseDate(Trigger.new, Trigger.oldMap);
}
}
}
OpportunityTriggerHandler.apxc
//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) {
2. Assignment 2 Solution:-
CalculatorTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
CalculatorTriggerHandler.calculation(Trigger.new);
}
}
}
CalculatorTriggerHandler.apxc
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
if(Trigger.isBefore) {
if(Trigger.isDelete) {
AccountTriggerHandler.preventAccountDeletion(Trigger.old);
}
}
}
AccountTriggerHandler.apxc
if(!accountOppMap.get(acc.Id).Opportunities.isEmpty()) {
acc.addError('This account has Closed Won Opportunities');
}
}
}
}
4. Assignment 4 Solution:-
LeadTrigger.apxt
LeadTrigger_Handler.apxc
for(Lead ld : leadNewList) {
if(ld.Email != null) {
emails.add(ld.Email);
}
}
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
6. Assignment 6 Solution:-
AccountTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
AccountTriggerHandler.ageValidation(Trigger.new);
}
}
}
AccountTriggerHandler.apxc
AccountTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isUpdate) {
AccountTriggerHandler.updateOppAmount(Trigger.new);
}
}
}
AccountTriggerHandler.apxc
//Contact + Opportunity
for(Account acc : accNewList) {
acc.Opportunity_Amount_Total__c = 0;
LeadTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isInsert || Trigger.isUpdate) {
LeadTriggerHandler.nameWithEr(Trigger.new);
}
}
}
LeadTriggerHandler.apxc
for(Lead ld : leadNewList) {
if(!ld.LastName.startsWith('Er. ')) {
ld.LastName = 'Er. ' + ld.LastName;
}
}
}
}
System.debug(accConMap);
}
}
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.
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)
14. Executes record-triggered flows that are configured to run after the record is saved.
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.
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
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
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.
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
if(Trigger.isBefore) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.setClosedDate(Trigger.new);
}
}
}
OpportunityTriggerHandler.apxc
Remember:- The below after trigger is just for understanding the difference between
before and after trigger. Never use after context for before
OpportunityTrigger.apxt
if(Trigger.isAfter) {
if(Trigger.isInsert) {
OpportunityTriggerHandler.setClosedDate(Trigger.new);
}
}
}
OpportunityTriggerHandler.apxc
oppToUpdate.add(oppNew);
}
if(!oppToUpdate.isEmpty()) {
update oppToUpdate;
}
}
}
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.
OpportunityTrigger.apxt
if(Trigger.isAfter) {
if(Trigger.isInsert || Trigger.isUpdate) {
OpportunityTriggerHandler.updateParent(Trigger.new);
}
}
}
OpportunityTriggerHandler.apxc
if(!accToUpdate.isEmpty()) {
update accToUpdate;
}
}
}
2nd Way:-
OpportunityTrigger.apxt
if(Trigger.isAfter) {
if(Trigger.isInsert || Trigger.isUpdate) {
OpportunityTriggerHandler.updateParent(Trigger.new);
}
}
}
OpportunityTriggerHandler.apxc
if(!accToUpdate.isEmpty()) {
update accToUpdate;
}
}
}
5th July 2023
1. Update all child contacts email with its parent Account email.
AccountTrigger.apxt
if(Trigger.isAfter) {
if(Trigger.isUpdate) {
AccountTriggerHandler.updateChildContacts(Trigger.new);
}
}
}
AccountTriggerHandler.apxc
if(!conToUpdate.isEmpty()) {
update conToUpdate;
}
}
}
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.
To avoid this scenario we should create a static variable and check the value of this
variable before we execute anything in the trigger.
LeadTrigger.apxt
if(Trigger.isAfter) {
if(Trigger.isInsert) {
if(LeadTriggerHandler.executeTrigger) {
LeadTriggerHandler.executeTrigger = false;
LeadTriggerHandler.insertDuplicate(Trigger.new);
}
}
}
}
LeadTriggerHandler.apxc
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.
● COUNT ()
● COUNT (FIELD_NAME)
● COUNT_DISTINCT ()
● SUM ()
● MIN ()
● MAX ()
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.
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.
● 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.
6. Aggregate Result:-
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.
Example 1:-
Example 2:-
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
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'));
}
}
}
insert accShare;
Assignment:-
OpportunityTrigger.apxt
if(Trigger.isBefore) {
if(Trigger.isUpdate) {
OpportunityTriggerHandler.avoidEdit(Trigger.new);
}
}
}
OpportunityTriggerHandler.apxc
2. Assignment 2 Solution:-
ContactTrigger.apxt
if(Trigger.isAfter) {
if(Trigger.isUpdate) {
ContactTriggerHandler.updateContacts(Trigger.old, Trigger.newMap);
}
}
}
ContactTriggerHandler.apxc
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;
}
}
}
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
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:-