function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Mike DeMilleMike DeMille 

I need help Bulkifying this Trigger!

This is my first Apex Trigger and I realize it will make most of you cringe your teeth!  It is a simple trigger that will update the lead 'status' field to 'contacted' when the following criteria are met:

1- its 'lead rating' is either D1,D2,D3, or D4
2- the current lead 'status' is "open"
3- the lead has a related task containing "Message Sent:" in the subject 

Can anyone please help me bulkify this?  




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



    for (Lead l : Trigger.new) {
    if((l.Eloqua_Lead_Rating__c =='D1' || l.Eloqua_Lead_Rating__c =='D2'|| l.Eloqua_Lead_Rating__c =='D3'|| l.Eloqua_Lead_Rating__c =='D4')&& l.Status =='Open') {
    List<Task> sentemails = [SELECT Id FROM Task WHERE Subject LIKE '%Message Sent:%'];
    
    if(sentemails.size() > 0) {
    l.Status = 'Contacted';}
    else {l.Status = 'Open';}}}

}
Best Answer chosen by Mike DeMille
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student

Hi Mike,

Nice name mate. I have reformed your trigger. There may be some issues as I was not 100% sure on what you were trying to do or how your org is structured.

However, this is what I have done: I have filled it to the brim with comments, this should help you with understanding bulkifying - 

 

Important thing to remember, is that you dont perform any DML actions or SOQL within a for loop and when you query, you query the whole group of records and then sort through it with a for loop and if statements.

NOTE: if you need to update, insert, query, delete, etc always add to a list and then perform one dml function.

trigger dleadstatus on Lead (After insert, After update) {

    //Introduce Lists, they are a key to bulkifying
    List<Task> sentemails = new List<Task>();
    List<Id> LeadIds = new List<id>();
    List<Lead> LeadsToUpdate = new List<Lead>();
    
   //Adding all the leads that were triggered by the DML actions update and insert in that instance (including dataloader)
    for (Lead L: Trigger.new) {
    LeadIds.add(L.Id);
    
    }
    
    //SOQL's removed from for loop (This is to avoid governor limits: When in loop it will perform a separate SOQL for every single Lead record that is updated or inserted in that instance
    sentemails = [SELECT Id FROM Task WHERE whoid IN: LeadIds];

   //Opening for loop again with a different for loop based on the tasks related to the leads affected by the trigger
    for (Lead L: Trigger.new) {
    for (Task Email: sentemails) {

//ensuring that the specific Lead and email are matched by whoid
    if(Email.whoid == L.Id){
//Your given criteria
    if((L.Eloqua_Lead_Rating__c =='D1' || L.Eloqua_Lead_Rating__c =='D2'|| L.Eloqua_Lead_Rating__c =='D3'|| L.Eloqua_Lead_Rating__c =='D4')&& L.Status =='Open') {
//Second criteria
    if(Email.Subject LIKE '&Message Sent:%'){
//Action to be taken
    L.Status = 'Contacted';
//Add leads which need to be updated into one list to avoid governor limits
    LeadsToUpdate.add(L);
    }
    }
    }    
    }
    }
    
//Check that update list has entries added
    if(LeadsToUpdate.size() > 0) {

//Update list rather than each record individually (same affect, just not as strenuous on the system)
    update LeadsToUpdate;
    }
    
  
}

 

I hope this helps, select as Best answer if it does!

All Answers

Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student

Hi Mike,

Nice name mate. I have reformed your trigger. There may be some issues as I was not 100% sure on what you were trying to do or how your org is structured.

However, this is what I have done: I have filled it to the brim with comments, this should help you with understanding bulkifying - 

 

Important thing to remember, is that you dont perform any DML actions or SOQL within a for loop and when you query, you query the whole group of records and then sort through it with a for loop and if statements.

NOTE: if you need to update, insert, query, delete, etc always add to a list and then perform one dml function.

trigger dleadstatus on Lead (After insert, After update) {

    //Introduce Lists, they are a key to bulkifying
    List<Task> sentemails = new List<Task>();
    List<Id> LeadIds = new List<id>();
    List<Lead> LeadsToUpdate = new List<Lead>();
    
   //Adding all the leads that were triggered by the DML actions update and insert in that instance (including dataloader)
    for (Lead L: Trigger.new) {
    LeadIds.add(L.Id);
    
    }
    
    //SOQL's removed from for loop (This is to avoid governor limits: When in loop it will perform a separate SOQL for every single Lead record that is updated or inserted in that instance
    sentemails = [SELECT Id FROM Task WHERE whoid IN: LeadIds];

   //Opening for loop again with a different for loop based on the tasks related to the leads affected by the trigger
    for (Lead L: Trigger.new) {
    for (Task Email: sentemails) {

//ensuring that the specific Lead and email are matched by whoid
    if(Email.whoid == L.Id){
//Your given criteria
    if((L.Eloqua_Lead_Rating__c =='D1' || L.Eloqua_Lead_Rating__c =='D2'|| L.Eloqua_Lead_Rating__c =='D3'|| L.Eloqua_Lead_Rating__c =='D4')&& L.Status =='Open') {
//Second criteria
    if(Email.Subject LIKE '&Message Sent:%'){
//Action to be taken
    L.Status = 'Contacted';
//Add leads which need to be updated into one list to avoid governor limits
    LeadsToUpdate.add(L);
    }
    }
    }    
    }
    }
    
//Check that update list has entries added
    if(LeadsToUpdate.size() > 0) {

//Update list rather than each record individually (same affect, just not as strenuous on the system)
    update LeadsToUpdate;
    }
    
  
}

 

I hope this helps, select as Best answer if it does!

This was selected as the best answer
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student

PS. This bit:

 

 if(Email.Subject LIKE '&Message Sent:%'){

may word, but im not 100% sure, it is something that you may have trouble with.

 

If so, just look into all the ways that you can compare a string.

 

LIKE, contains, substring, etc etc

Mike DeMilleMike DeMille
I was literally working on this when I got your response and trying to learn about bulkification and keeping my SOQL out of my loops.  I'm going to try this right now and I will let you know if I have any luck.. Thank you so much for the help.  I'm trying to learn and the best way to learn is with your own use cases!
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student

I could not agree more, I learnt all my salesforce development initially through the forums until I could read code better and then from others examples which I craft to my own.

Back in the day, I would be stuck on a trigger problem for days, raking through the forums and reposting my questions hoping that someone would see them.

Even if that trigger still needs some work, read through it carefully it will teach you some formations for bulkified triggers.

You are on the right track with bulkifying as governor limits is one of the main issues i would say that people experience with writing triggers.

 

Particularly the two errors:

 

Too many SOQL 101

Too many DML 101

 

Once you have mastered bulkifying, the next step is creating maps. If you have any questions about bulkification or the changes i amde to your trigger, just comment below.

Mike DeMilleMike DeMille
LIKE not working.  Can't figure out how to use Contains here
Mike DeMilleMike DeMille
User-added image

I changed LIKE to == for testing's sake and am still ending up with this error message
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student

Hey yeah,

So this error basically means that the SOQL you used to query into the tasks of your system, you did not add whoid to it.

My fault sorry - basically:

when using trigger, the for loop  for (Lead L: Trigger.new) { like this, already has all the fields that are contained within the object. However, should you use another object that is not mainly defined by trigger, you need to query each field that you  use within if statements.

Basically it is saying you used whoid in your if statement, but you did not query it within your soql.

 

Just add whoid to the lsit of queried fields from the task and thatll solve that issue

Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student
sentemails = [SELECT Id, subject, whoid FROM Task WHERE whoid IN: LeadIds];

this should solve it
Mike DeMilleMike DeMille
Yes, it works great, thanks so much for your help!  Now I just need to figure out how to use the 'Contains' function in this.  Looks like I will also need to create a trigger on task that updates the lead when these changes are made to task (subject contains Message Sent:)...  

I will stop bugging you and pick this up tomorrow.  Thank you for your help, it will really be good for me to review and learn from. 
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student
Haha, this isnt really trouble. I like doing this and am happy to help. Just dont forget to select my answer as the best answer to help others in the future.

This is waht i think it is from memory. Basically saying String (segement of characters) String.Contains({!InsertString_). So it would be:

if(Email.Subject.CONTAINS('Message Sent:')){

}

Email.Subject is a string field, .Contains (is the function) and 'Messeage Sent:" is the string we are looking for
Mike DeMilleMike DeMille
Well I hope to return the favor sometime.  You have no idea, this stuff is a bit over my head at this point, but I really want to get it dialed and this will help a ton.  Thanks 
JethaJetha
Hi Friend,

below is best way to wright the same above code and will work exactly the way you want it to work.
trigger dleadstatus on Lead (before insert, before update) 
{
    List<Task> sentemails = new List<Task>();
    Set<String> leadRating = new set<String>{'D1', 'D2', 'D3', 'D4'};
    
    sentemails = [SELECT Id FROM Task WHERE whoid IN: Trigger.new];

	for (Task Email: sentemails) 
	{
		if(leadRating.contains(L.Eloqua_Lead_Rating__c) && Email.Subject LIKE '&Message Sent:%')
		{
			trigger.newmap.get(Email.whoid).Status = 'Contacted';
		}
	}
}

Let me know if it helps you and don't forget to mark this as best answer if it resolves your query.
Mike DeMilleMike DeMille
Jetha,

Thanks for the response.  When using that version, I'm getting an error for using LIKE on line 10.  When I change it to .Contains, it gives me an error saying that "Error: Compile Error: Variable does not exist: L.Eloqua_Lead_Rating__c at line 10 column 32"
Developer.mikie.Apex.StudentDeveloper.mikie.Apex.Student

No worries at all Mike, there isnt really anything you can do for me. The best way to return the favour is to learn from what I have taught you and repeat it to someone who is asking if you ever have the opportunity. That is how these forums work.

 

In regards to writing another trigger for the task side of things. Personally, I think that you should just add it to your existing trigger.

Triggers can impact on each other and there is no real point in adding two different triggers that are triggered by the same thing on two different objects.

i am not completely sure what you mean by adding a trigger to ask, but:

If you are refering to adding information to the task when the criteria sets the lead status to "Contacted", then adding a completely new trigger would probably only complicate things. As you already have the event and the record queried within your current trigger you can do something like this:

Step 1: add custom boolean field to activity called something like: ContactStatusSet__c or whatever field you would like to use for stating that the lead status was altered through the trigger.

Step 2. Introduce a list of Tasks at the top of the page  List<Task> tasksToUpdate = new List<Task>();

Step 3: Change this line:
 

if(Email.Subject.Contains('&Message Sent:')){

//Action to be taken

    L.Status = 'Contacted';

//Add leads which need to be updated into one list to avoid governor limits
    LeadsToUpdate.add(L);

    }
 


to this line:
 

if(Email.Subject.Contains('&Message Sent:')){

//Action to be taken

    L.Status = 'Contacted';

//Add leads which need to be updated into one list to avoid governor limits
    LeadsToUpdate.add(L);
Email.ContactStatusSet__c = true;
tasksToUpdate.add(Email);

    }
 

Step 4: Add this line at the end:

if(tasksToUpdate.size() > 0) {

    update LeadsToUpdate;
    }
 

Finally:

It is best to keep all trigger functions to do with an object under one trigger. 

E.G. Every Account trigger in the one super account trigger. This is to ensure that you control the order of which processes are executed.

You can do so and keep it ordered through:

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

if(trigger.isbefore){
if(trigger.isinsert){

}else if(trigger.isupdate){

}
}
if(trigger.isafter){
if(trigger.isinsert){

}else if(trigger.isupdate){

}
}

}

All logic that occurs before insert, after insert, etc You can decide where and when it is exactly executed.

This way whenever a DML function is executed, you can catch it and perform neccessary work on it.

Hope this helps, all the best and goodluck

JethaJetha
Please give a try to below modified code :
 
trigger dleadstatus on Lead (before insert, before update) 
{
    List<Task> sentemails = new List<Task>();
    Set<String> leadRating = new set<String>{'D1', 'D2', 'D3', 'D4'};
    
    sentemails = [SELECT Id FROM Task WHERE whoid IN: Trigger.new];

	for (Task Email: sentemails) 
	{
		if(leadRating.contains(trigger.newmap.get(Email.whoid).Eloqua_Lead_Rating__c) && Email.Subject.contains(Message Sent))
		{
			trigger.newmap.get(Email.whoid).Status = 'Contacted';
		}
	}
}

 
Mike DeMilleMike DeMille
Jetha,

This is working.  I added whoid and subject to the selected SOQL items.  Question- how would i get this to update the lead when only the task is being updated to meet the criteria 'subject contains Message Sent:'  ?  Basically I like how it works now but I would like it to evaluate if even only the related task is updated or created to meet that criteria as well.  So if a lead already met the criteria then later a task was added to it with the subject= Message Sent: , it would still update the lead status???
JethaJetha
Mike, 

That make sense to me that instead of writing above trigger on Lead, you shoud be this on Task.

So whever you send an email, the trigger would fire n update the lead status.  Make sense......then let me know

 
trigger dleadstatus on Task (after insert, after update) 
{
    Set<Id> leadIds = new Set<Id>();
	Set<String> leadRating = new set<String>{'D1', 'D2', 'D3', 'D4'};
    
	for (Task Email: trigger.New)
	{
		if (Email.Subject.contains(Message Sent))
		{
			leadIds.add(Email.whoid);
		}
	}
	
	Map<Id, lead> idLeadMap = new Map<Id, lead>([SELECT Eloqua_Lead_Rating__c FROM Lead WHERE ID IN : leadIds AND Eloqua_Lead_Rating__c IN : leadRating]);
	
	for (Task Email: trigger.New)
	{
		idLeadMap.get(Email.whoid).Status = 'Contacted';
	}
	
	update idLeadMap.values();
}

Don't forget to mark it as best answer, if it resolve your issue.
Mike DeMilleMike DeMille
I need it to evalute on both the update of the lead and on the task.  Sounds like I might need 2 triggers
JethaJetha
one trigger on task is fine, because whenever any email is sent out, it will update your lead's status.

I am very sure in that case, one trigger is enough on task.

 
Mike DeMilleMike DeMille
Jetha,

Since I am a beginner I'm having a hard time writing a test class for this trigger on leads.  I used a version of your lead trigger above.  Here is what I have for a test class so far:

@isTest
public class Testdleadstatus2 {

    static testMethod void testLeads(){
    
    Lead l = new Lead();
    l.FirstName = 'Michael';
    l.LastName = 'Burns';
    l.Eloqua_Lead_Rating__c = 'D1';
    l.Status = 'Open';
    insert l;
    
    Task t = new Task();
    t.subject = 'Message Sent: Error';
    insert t;
    
    List<Lead> open = [SELECT Id, Status FROM Lead WHERE FirstName = 'Michael'];
    System.assertEquals(0,open.size());
    
    List<Lead> contacted = [SELECT Id, Status FROM Lead WHERE FirstName = 'Michael'];
    System.assertEquals(1,contacted.size());
    
    
    
    }}


I'm only getting about 57% coverage