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
LloydSilverLloydSilver 

bulkify issue

Ran into a bulk issue on a class. I re-wrote it (See attached code) but am having an issue still. The problem is with creating the list of opportunities by adding them to a list via looking up their ID in the list for OpportunityContactRole (line 15 through 18).

 

My interpretation of:

 

List<Opportunity> oppSureLC = [select ID, StageName from Opportunity where ID IN :ocrSureLC];

 

Is that it should add an opportunity to the list if the ID of the opportunity is included in the OpportunityContactRole list, which contains OpportunityID. Obviously I'm wrong.

 

I'd appreciate help in fixing this. Do I need to use a for statement to add the Opportunities instead of doing it within a list? Is there a way of using a relationship query to eliminate some of this? 

 

public class UpdateOpportunityStageOnContact {
	
		public static void agentstatus(Contact[] contactsToUpdate) {
			
			List<Contact> conSureLC = [select ID, Agent_Status__c, Don_t_Update_Opportunity_Via_Flow__c from Contact where ID IN :contactsToUpdate AND Agent_Status__c = 'SureLC Packet Complete' AND Don_t_Update_Opportunity_Via_Flow__c = FALSE];
			List<Contact> conCarrier = [select ID, Agent_Status__c, Don_t_Update_Opportunity_Via_Flow__c from Contact where ID IN :contactsToUpdate AND Agent_Status__c = 'Carrier Contracting Complete' AND Don_t_Update_Opportunity_Via_Flow__c = FALSE];
			List<Contact> conFirst = [select ID, Agent_Status__c, Don_t_Update_Opportunity_Via_Flow__c from Contact where ID IN :contactsToUpdate AND Agent_Status__c = 'First Case Submitted' AND Don_t_Update_Opportunity_Via_Flow__c = FALSE];
			List<Contact> conClient = [select ID, Agent_Status__c, Don_t_Update_Opportunity_Via_Flow__c from Contact where ID IN :contactsToUpdate AND Agent_Status__c = 'Client' AND Don_t_Update_Opportunity_Via_Flow__c = FALSE];
			
			List<OpportunityContactRole> ocrSureLC = [select ID, OpportunityID from OpportunityContactRole where ContactID IN :conSureLC];
			List<OpportunityContactRole> ocrCarrier = [select ID, OpportunityID from OpportunityContactRole where ContactID IN :conCarrier];
			List<OpportunityContactRole> ocrFirst = [select ID, OpportunityID from OpportunityContactRole where ContactID IN :conFirst];
			List<OpportunityContactRole> ocrClient = [select ID, OpportunityID from OpportunityContactRole where ContactID IN :conClient];
			
			List<Opportunity> oppSureLC = [select ID, StageName from Opportunity where ID IN :ocrSureLC];
			List<Opportunity> oppCarrier = [select ID, StageName from Opportunity where ID IN :ocrCarrier];
			List<Opportunity> oppFirst = [select ID, StageName from Opportunity where ID IN :ocrFirst];
			List<Opportunity> oppClient = [select ID, StageName from Opportunity where ID IN :ocrClient];
			
			List<Opportunity> oppsToUpdateSureLC = new List<Opportunity>{};
			List<Opportunity> oppsToUpdateContracting = new List<Opportunity>{};
			List<Opportunity> oppsToUpdateFirst = new List<Opportunity>{};
			List<Opportunity> oppsToUpdateClient = new List<Opportunity>{};
			
			for(Opportunity opp1: oppSureLC){
						opp1.StageName = 'SureLC Packet Complete';
						oppsToUpdateSureLC.add(opp1);
			}
					
			for(Opportunity opp2: oppCarrier){
						opp2.StageName = 'Carrier Contracting Complete';
						oppsToUpdateContracting.add(opp2);
			}
					
			for(Opportunity opp3: oppFirst){
						opp3.StageName = 'First Case Submitted';
						oppsToUpdateFirst.add(opp3);
			}

			for(Opportunity opp4: oppClient){
						opp4.StageName = 'Closed Won';
						oppsToUpdateClient.add(opp4);
			}
			
			if(!oppsToUpdateSureLC.isEmpty())
			update oppsToUpdateSureLC;
			
			if(!oppsToUpdateContracting.isEmpty())
			update oppsToUpdateContracting;
			
			if(!oppsToUpdateFirst.isEmpty())
			update oppsToUpdateFirst;
			
			if(!oppsToUpdateClient.isEmpty())
			update oppsToUpdateClient;
			
		}

}

 

sfdcfoxsfdcfox

The problem was that you were querying opportunities against opportunity contact role IDs, so there would be no results.

 

Regardless, this code is horribly inefficient, in any case, using 12 queries instead of 2 (NOTE: if you know what Agent_Status__c is, you could make it just 1 query), and 4 DML statements instead of 1.

 

Here's a revised version of your code:

 

public class UpdateOpportunityStageOnContact {

        public static void agentstatus(Contact[] contactsToUpdate) {
            Map<Id, Contact> contacts = new Map<Id, Contact>(
                [SELECT Id, Agent_Status__c
                 FROM   Contact
                 WHERE  Id IN :contactsToUpdate AND
                 Don_t_Update_Opportunity_Via_Flow__c = FALSE AND
                 Agent_Status__c IN ('SureLC Packet Complete','Carrier Contracting Complete','First Case Submitted','Client')]);
            Opportunity[] opps = new Opportunity[0];
            OpportunityContactRole[] roles = [SELECT Id, ContactId, OpportunityId FROM OpportunityContactRole WHERE ContactId IN :contacts];

            Map<String, String> convMap = new Map<String, String> {
                'SureLC Packet Complete' => 'SureLC Packet Complete',
                'Carrier Contracting Complete' => 'Carrier Contracting Complete',
                'First Case Submitted' => 'First Case Submitted',
                'Client' => 'Closed Won'
            };

            for(OpportunityContactRole contactRole: roles) {
                opps.add(new Opportunity(Id=contactRole.OpportunityId, StageName=convMap.get(contacts.get(contactRole.ContactId).Agent_Status__c)));
            }

            update opps;
        }
}

Assuming that contactsToUpdate is from a trigger, you can just loop through the records, and save another query:

 

Map<Id, Contact> contacts = new Map<Id, Contact>();
Set<String> statusValues = new Set<String> { 'SureLC Packet Complete','Carrier Contracting Complete','First Case Submitted','Client' };
for(Contact record:contactsToUpdate) {
    if(!record.Don_t_Update_Opportunity_Via_Flow__c && statusValues.containsKey(record.Agent_Status__c)) {
        contacts.put(record.Id, record);
    }
}

A total of 1 query to get all the data you need to update the opportunities, and 1 DML statement to update all the opportunities involved.

LloydSilverLloydSilver

Thanks for the reply and lesson. I'm a bit confused about "Assuming that contactsToUpdate is from a trigger, you can just loop through the records, and save another query:" It is from a trigger (code attached).

 

I'm basically checking to see if the custom field Agent_Status__c has changed upon a contact update. If it has changed, and it the new value is one of 4 possibilities (out of 8), then run through the class.

 

And then via the class, assuming the contact has Don_t_Update_Opportunity_Via_Flow__c = FALSE, update the StageName of any opportunities with which they are associated (through the OpportunityContactRole) and change the StageName based upon the contact's Agent_Status__c value.

 

If you could clarify what you meant by contactsToUpdate being from a trigger, I'd appreciate it.

 

Your code is WAY beyond me at this point. I don't get the whole "Map" statement. So I'm going to have to spend some time going through it and figuring out what it does. But . . . that's why I posted and am grateful for the help.

 

Thanks.

 

trigger AgentStatusChange on Contact (after update) {
	
	List<Contact> contactsToUpdate = new List<Contact>{};
	
	for (Contact agents: Trigger.new){
		Contact oldContact = Trigger.oldMap.get(agents.ID);
		if (agents.Agent_Status__c != oldContact.Agent_Status__c){
			if (agents.Agent_Status__c == 'SureLC Packet Complete' || agents.Agent_Status__c == 'Carrier Contracting Complete' || agents.Agent_Status__c == 'First Case Submitted' || agents.Agent_Status__c == 'Client'){
				
				contactsToUpdate.add(agents);
			}
		}
	}
	
	if(!contactsToUpdate.isEmpty())
	UpdateOpportunityStageOnContact.agentstatus(contactsToUpdate);
	
}

 

 

sfdcfoxsfdcfox

The fact that the code is called from a trigger makes the first query in my code (and the first four in yours), pointless. This is because the trigger automatically has all of the fields from the updated record available to do. The point of a query is to retrieve information you do not already know. Since you already know the value of Agent_Status__c and Don_t_Update_Opportunity_From_Flow__c, there's no point in querying it a second time. Had your utility method been called from a non-trigger context (say, on a Visualforce controller), it may be possible that Agent_Status__c wasn't included in the original data (because the record may not be fully populated).

 

Your trigger's code can be simplified (because we filter by status in the code already), reducing it to:

 

trigger AgentStatusChange on Contact (after update) {
    UpdateOpportunityStageOnContact.agentstatus(Trigger.new);
}

This way, if the list of status values change in the future, you only have to update one piece of code instead of two.

 

Maps are called "dictionaries" in some programming circles, because that's how they function. For example, if you didn't know the meaning of "dodecahedron" (and you didn't have Google), you would look up the word in a dictionary to get the meaning of the word. Similarly, maps let you define "words" (called "keys"), and attach a "meaning" (called a "value").

 

You can then look up the value based on the key without looping through each individual element. You can define a key and its value, or replace a key's value with a new value, or remove that key and its value, without having to find that value; this changes the number of code statements required to find the value you're looking for from m * n (number of items in the outer array times the number of values to check), to just the number of times in the outer array (linear time).

 

The "statusValue" map, for example, I used to convert status values to stage values. This is how I eliminated the need for four different "if" statements. You'll probably want to read up on how to use Maps, because you will need them to write any reasonably complex code. You'll also want to learn about a related concept, a "set." A set is a list that doesn't allow duplicate values. A map consists of a set, and a list.

LloydSilverLloydSilver
Wow. Thanks for the fantastic help. I've learned a lot.

If I want to run this still on contacts only with a change in status, I presume I still do that through the trigger. Compare old and new trigger values and add results to the list passed to the class. In that case, are all of the object fields still passed on so that I can avoid that first lookup?

Sent from my Android phone using TouchDown (www.nitrodesk.com)
sfdcfoxsfdcfox

There are four variables, two maps and two lists, Trigger.old, Trigger.oldMap, Trigger.new, and Trigger.newMap. The easy way to find deltas is to loop through either Trigger.old or Trigger.new, and compare the values from the map. You can also use an index counter, as old and new will be aligned to the same before and after records.

 

trigger XYZ on Contact (after update) {
    Contact[] changes = new Contact[0];
    Set<String> triggers = new Set<String> { 'SureLC Packet Complete', 'Carrier Contracting Complete', 'First Case Submitted', 'Client' };
    for(Integer index = 0; index < Trigger.new.size(); index++) {
        if(Trigger.new[index].Agent_Status__c != Trigger.old[index].Agent_Status__c &&  // value changed
           triggers.contains(Trigger.new[index].Agent_Status__c) &&                     // and is a triggering change
           !Trigger.new[index].Don_t_Update_Opportunity_From_Flow__c) {                 // and doesn't have flag
           changes.add(Trigger.new[index]);       // add to list
        }
    }
    if(!changes.isEmpty()) {
        UpdateOpportunityStageOnContact.agentstatus(changes);
    }
}

You can also use Trigger.newMap or Trigger.oldMap, such that:

 

for(Contact record:Trigger.new) {
    if(Trigger.oldMap.get(record.id).Agent_Status_c != record.Agent_Status__c ...

Like the other maps I demonstrated, these maps have a "key" of the record's ID, and a value of the record in a particular state (old is before the last update, new is after the last update). Note that Trigger.old and Trigger.oldMap do not exist in a "before insert", "after insert", or "after undelete" event (because there is no "old" data), and Trigger.new does not exist in "before delete" and "after delete" (because there is no "new" data).

LloydSilverLloydSilver

Ok, I'm putting together the final code here, trying to make sure I fully understand everything that is going on. 

 

In your suggested code you had:

 

if(!record.Don_t_Update_Opportunity_Via_Flow__c  && statusValues.containsKey(record.Agent_Status__c)) {

 

Should I not add == FALSE after record.Don_t_Update_Opportunity_Via_Flow__c? And should we be using containsKey when we are working with a string and not a map? Or should we use contains?

 

So the final code would be:

 

if(!record.Don_t_Update_Opportunity_Via_Flow__c == FALSE && statusValues.contains(record.Agent_Status__c)) {

 

Lastly, in the code below, should IN :contacts be instead IN :contacts.keyset()

 

OpportunityContactRole[] roles = [SELECT Id, ContactId, OpportunityId FROM OpportunityContactRole WHERE ContactId IN :contacts];

 

 

Again, thanks so much. I'm really learning a lot here.

 

sfdcfoxsfdcfox

You're correct on two of three points. ContainsKey should have been contains, and :contacts should have been :contacts.keySet(). However, the == FALSE is not necessary. The point of a condition is to have a true or false value. Since a checkbox is already a true or false value, we don't need to compare it against true or false. The "!" at the beginning of the condition inverts the flag, such that true becomes false and false becomes true. In other words:

 

Boolean falseValue = !true;
Boolean trueValue = !false;

if(trueValue) {
    System.Debug('This is true!');
}
if(!falseValue) {
    System.Debug('This is also true!');
}

Here's the final revision you should need:

 

public class UpdateOpportunityStageOnContact {

        public static void agentstatus(Contact[] contactsToUpdate) {
            Map<Id, Contact> contacts = new Map<Id, Contact>(
                [SELECT Id, Agent_Status__c
                 FROM   Contact
                 WHERE  Id IN :contactsToUpdate AND
                 Don_t_Update_Opportunity_Via_Flow__c = FALSE AND
                 Agent_Status__c IN ('SureLC Packet Complete','Carrier Contracting Complete','First Case Submitted','Client')]);
            Opportunity[] opps = new Opportunity[0];
            OpportunityContactRole[] roles = [SELECT Id, ContactId, OpportunityId FROM OpportunityContactRole WHERE ContactId IN :contacts.keySet()];

            Map<String, String> convMap = new Map<String, String> {
                'SureLC Packet Complete' => 'SureLC Packet Complete',
                'Carrier Contracting Complete' => 'Carrier Contracting Complete',
                'First Case Submitted' => 'First Case Submitted',
                'Client' => 'Closed Won'
            };

            for(OpportunityContactRole contactRole: roles) {
                opps.add(new Opportunity(Id=contactRole.OpportunityId, StageName=convMap.get(contacts.get(contactRole.ContactId).Agent_Status__c)));
            }

            update opps;
        }
}

 

Map<Id, Contact> contacts = new Map<Id, Contact>();
Set<String> statusValues = new Set<String> { 'SureLC Packet Complete','Carrier Contracting Complete','First Case Submitted','Client' };
for(Contact record:contactsToUpdate) {
    if(!record.Don_t_Update_Opportunity_Via_Flow__c && statusValues.contains(record.Agent_Status__c)) {
        contacts.put(record.Id, record);
    }
}