Bulkifying a Trigger (an example)

Back in June, I blogged about a trigger I made to auto-create a Campaign Member record for new Leads if the Lead Source value matched the Name of an existing Campaign.  Steve Andersen rightfully commented on that post about the need to bulkify the trigger.  I have been pretty slow in learning Apex and over the past week or so things have started clicking for me.  I am now (finally) beginning to “get it”.  With the help of Matt Kaufman and a friend at Salesforce, I have come to understand Apex a bit more and can much more easily apply it to my day to day Salesforce work.

Therefore, I thought I should correct the trigger I wrote and make it bulkified.  This should serve as a good before and after example of a trigger that accomplishes the same thing, but is now bulkified.

The original Trigger
Below is how the trigger looked originally.

trigger Create_CampaignMember_For_New_Leads on Lead (after insert) {

	try {	
		
		if (Trigger.new.size() == 1) {
			
			List <CampaignMember> cm = new list<CampaignMember>();
			
			for(Lead L : Trigger.new) {
				
					String cname = L.leadsource;
					
					// Added for AppExchange Partners that get leads via AppExchange where Salesforce added the "dup-" term to signify a duplicate
					String replaceText2 = 'dup-';
					cname = cname.replace(replaceText2,'');
					
					List <Campaign> c = [select id, name from Campaign where name = :cname limit 1];
					
					if(!c.isEmpty()){
						CampaignMember cml = new CampaignMember();
						cml.campaignid = c[0].id;
						cml.leadid = l.id;
						cm.add(cml);
					}
			}
			
			if(!cm.isEmpty()){
				insert cm;
			}
		}
		
		
	} catch(Exception e) {
		system.debug ('error: ' + e.getMessage() );
	} 
}

Bulkified Trigger
And now a corrected version to be bulkified.

trigger Create_CampaignMember_For_New_Leads on Lead (after insert) {

	/*
    * Loop through all leads and collect the necessary lists
    */
	list<Lead> theLeads = new list<Lead>(); // List containing each Lead being processed
	list<String> cNames = new list<String>(); // List of Campaign Names
	map<String, String> map_lSource_to_cName = new map<String, String>(); // Mapping Lead Sources to Campaign Names. We have this because we are cleaning up Lead Sources in some cases. 
	String wrkText = ''; // Temporary, working variable
	String replaceText = 'dup-'; // Text to replace. This is included for ISV partners who want to remove the "-dup" string that is included for duplicate AppExchange Lead Submissions  
	
		for(Lead l:trigger.new) { 
			theLeads.add(l); // add lead to the main lead list
			if (l.leadsource != null) {
				wrkText = l.leadsource;
				wrkText = wrkText.replace(replaceText,'');
				cNames.add(wrkText); // add to list of Campaign Names
				map_lSource_to_cName.put(l.leadsource,wrkText); // add to map of Lead Sources to Campaign Names
			}
		}
	
	/*
	* Create a map containing an association of Campaign Names to Campaign IDs
	*/
	list<Campaign> theCampaigns = [SELECT Id, Name FROM Campaign WHERE Name IN :cNames]; // Campaign sObjects we are dealing with
	map<String, ID> map_cName_to_cID = new map<String, ID>(); // Mapping Campaign Names to Campaign IDs
	
		for (Campaign c:theCampaigns) {
			map_cName_to_cID.put(c.Name,c.Id);
		}
		
	/*
	* Loop through the main list of Leads
	*/
	list <CampaignMember> theCampaignMembers = new list<CampaignMember>(); // List containing Campaign Member records to be inserted
	
	for (Lead l:theLeads) {
		if(map_cName_to_cID.get(map_lSource_to_cName.get(l.leadsource)) != null) {
			CampaignMember cml = new CampaignMember();
			cml.leadid = l.id;
			cml.campaignid = map_cName_to_cID.get(map_lSource_to_cName.get(l.leadsource));
			theCampaignMembers.add(cml);
		}

	}
	
	/*
	* Insert the list of Campaign Members
	*/
	if(!theCampaignMembers.isEmpty()){
		insert theCampaignMembers;
	}

}

Updated Test Class (100% code coverage)
I updated the test class too to make it cover 100% of the code.

public class Create_CampaignMember_For_New_Leads {

	static testMethod void Create_CampaignMember_For_New_Leads() {
	
		// Create a Campaign to be matched on
		Campaign C1 = new Campaign();
		C1.Name = 'Matching Campaign';
		insert C1;
		
		// Create a Lead with a Lead Source matching a Campaign
		Lead L1 = new Lead();
		L1.lastname = 'Create_CampaignMember_For_New_Leads';
		L1.firstname = 'Test For';
		L1.company = 'Company ABC';
		L1.leadsource = 'Matching Campaign';
		   
		insert L1;
		String holder = L1.id;
		
		List <CampaignMember> cm = [select id from CampaignMember where leadid = :holder limit 1];
		system.AssertEquals(1,cm.size());
		
		// Create a Lead without a Lead Source matching a Campaign
		Lead L2 = new Lead();
		L2.lastname = 'Create_CampaignMember_For_New_Leads';
		L2.firstname = 'Test For';
		L2.company = 'Company ABC';
		L2.leadsource = 'No Matching Campaign';
		   
		insert L2;
		
		String holder2 = L2.id;
		
		List <CampaignMember> cm2 = [select id from CampaignMember where leadid = :holder2 limit 1];
		system.AssertEquals(0,cm2.size());
	                       
	}
	
}

Feel free to make additional recommendations on this code. I updated the Arrowpointe Google Code Site with this code too.

16 Comments

  1. Steve Judd Said,

    August 24, 2009 @ 7:53 pm

    Scott,

    Thanks for your Apex Code for Creating Campaign members from the lead source. I’ve modified the code to allow multiple campaigns (up to 5) to be added from one web-to-lead form. Are you interested in posting the code on your site?

    Steve

  2. newbee Said,

    September 16, 2009 @ 9:17 am

    Hi,
    I’m a newbie on Apex code and I really need some help to make an update after insert/update.
    I’m trying to do something similar. I’d love to see your complete code.

    Thanks,
    newbee

  3. Scott Hemmeter Said,

    September 16, 2009 @ 9:43 am

    @newbee,

    The complete code is in the post.

  4. Marco Said,

    September 25, 2009 @ 5:02 am

    I think your test method are not bulk yet as you only insert a lead at a time. You should create and insert a list of Lead.

  5. Scott Hemmeter Said,

    September 25, 2009 @ 8:06 am

    @marco, good point. You are right.

  6. Amanda Said,

    April 16, 2010 @ 3:42 pm

    I’m a complete amateur at coding in Apex code. I’d like to modify your trigger to add members if certain criteria are met (other than leadsource), but can’t seem to figure out where to start. I think it’s the mapping lines that are confusing me..

    For example, if a sales guy has marked the lead to “receive the email campaign” (checkbox) or if the lead status is “Closed Lost”… they’d be added to a campaign based on their “Company”..

    Any suggestions where to start?

  7. Amanda Said,

    April 19, 2010 @ 4:29 pm

    Okay, I posted my previous comment prematurely. I figured out how to add campaign members based on my set criteria. However, my issue now is that once a lead has been added to a campaign (after update), I get an error of “DUPLICATE_VALUE, This entity is already a member of this campaign: []” any time I try to edit the lead for any other reason.

    I thought the last line of the code (below) handled this error and I’m now pulling my hair out trying to familiarize myself with the Apex language.

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

    Has anyone faced this problem or come up with a solution? It seems like one quick line right before the “insert” should do it (somehow checking if the lead is already a campaign member), but my attempts seem completely futile. Please help!

  8. Scott Hemmeter Said,

    April 19, 2010 @ 4:47 pm

    @Amanda, in my example above, it’s an AFTER INSERT so there is never a case where a duplicate will occur. In your case, since you are doing an update, you’d need to do a query before creating your theCampaignMembers list to see if you should add the record to the list. Don’t add it if it’s already there.

    An alternative approach would be to look into the Database.insert() method, which gives you more flexibility in handling errors and inserting partial batches.

  9. James Cooper Said,

    April 23, 2010 @ 7:18 am

    Is this limited to 100 object in the list “theCampaignMembers”
    I think that is the governor limit for triggers.

    How would you add an asynchronous @future insert function?
    Do you have an example that would save me some dev time? 🙂

  10. Scott Hemmeter Said,

    April 23, 2010 @ 7:48 am

    If you are creating 1 Campaign Member per Lead, you should be okay, right? Either way, I don’t have an @future example. Perhaps the easier thing would be to gather up theCampaignMembers and then, rather than inserting them, loop through that and add items to a temporary list. When that temporary list reaches 100, insert it and then clear it out and continue looping. After that loop is done, do one more check to see if anything is in the temporary list and insert those. Make sense? I am making this up without testing it, but it’d look something like.

    /* a bunch of code has already run */
    list tmp = new List ();

    // Loop through and add to tmp list
    for (CampaignMember cm : theCampaignMembers ){
    tmp.add(cm);
    if (tmp.size() == 100){
    insert tmp;
    tmp.clear();
    }
    }

    // After loop, do one more check on tmp list and insert
    if (!tmp.isEmpty()){
    insert tmp;
    }

  11. James Cooper Said,

    April 26, 2010 @ 5:54 am

    Thanks for your help! This looks like a good candidate for a generic future insert function using the new List.

    global class listHelper{
    @future
    static void futureInsert(List lo){I
    list tmp = new List ();

    // Loop through and add to tmp list
    for (sObject o : lo){
    tmp.add(o);
    if (tmp.size() == 100){
    insert tmp;
    tmp.clear();
    }
    }
    // After loop, do one more check on tmp list and insert
    if (!tmp.isEmpty()){
    insert tmp;
    }
    }
    }

    I haven’t tried it yet. Do you think it will work or will it hit the 100 insert limit

  12. James Cooper Said,

    April 26, 2010 @ 6:38 am

    I didn’t realise less than and greater than characters are not permitted in posts!
    It should read (List &gt sObject &lt lo){

  13. Steven Said,

    September 11, 2011 @ 7:59 pm

    A huge thanks!

    I was able to use this code as a starting point for my processes. This also helped to make Apex ‘click” in my brain. I was able to write a couple other triggers this weekend that will be HUGE time savers.

  14. shraddha Said,

    December 11, 2011 @ 4:35 am

    hi, Ihave created an email log object in that leadinfo is a lookup field related to lead
    i have to write trigger
    when email inserts it will match the deal id field from email log to lead number field of lead & assign leadid to lead info
    plz help me for that

  15. Gareth Said,

    March 6, 2013 @ 7:13 am

    @Scott this has been a big help for me, I have used a slightly different method based on your code to use a field value to add to certain campaigns (a checkbox) and I was wondering if it is possible to remove the record automatically should the checkbox be changed to false. any ideas?

  16. Scott Hemmeter Said,

    March 6, 2013 @ 12:58 pm

    @Gareth, it certainly would be possible. A bit too much to explain here. If you create an AFTER UPDATE trigger you have access to the before and after values of a field to know whether it’s changing. You’d have to key off of that situation and code away!

RSS feed for comments on this post