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
pixelpixel 

trigger with nested loops

This is my first trigger. I'm new to the concept of implicit for loops as used in APEX compared to how I originally learned to do them using "for (i=0; i<j,i++)", etc. The references to the first nested loop below generates the following error:

 

"Didn't understand relationship 'Bhrs__r' in field path. If you are attempting to use a custom relationship, be sure to append the '__r' after the custom relationship name. Please reference your WSDL or the describe call for the appropriate names."

 

NOTE: My use of the '__r' was an attempt to fix the problem when it didn't understand the relationship without it being there, so I strongly suspect that's not the real problem.

 

The trigger is intended to act on a custom Time Record containing custom Actual Hours, custom Opportunity link and custom Resource link fields, then recalculate the total Actual Hours used by the Resource on that Opportunity by retrieving all the Actual Hours records for the Resource/Opportunity. Once it does, it uses the two links (Resource and Opportunity) to locate a custom Resource Tracking record and update a field in it called Burnt Hours with the new total of Actual Hours it calculated as having been spent on the Opportunity.

 

Here's the section of code I'm referring to:

 

// Declare variables used in loops below
SFL5_SFL5_Time__c BrntH;
SFL5_SFL5_Time__c BrnHrs;
List<SFL5_SFL5_Time__c> BH;
// Get count and record IDs being processed by trigger List AHrsID = [ SELECT SFL5_SFL5_Time__c.Id FROM SFL5_SFL5_Time__c WHERE SFL5_SFL5_Time__c.Id IN :Trigger.new]; // This single query finds the ID of every Time record that is associated with the trigger // Test to see if AHrsID.size returns null // Code Block omitted here for brevity // (1) Collect data to calculate new Burnt Hours as part of loop for each // resource/opplink combo on Time Records that hit the trigger List<SFL5_SFL5_Time__c> rList = [ SELECT id, Actual_Hours__c, SFL5_SFDC_Resource__c, Opportunity_Link__c FROM SFL5_SFL5_Time__c WHERE SFL5_SFL5_Time__c.Id = :AHrsID.Id]; for (SFL5_SFL5_Time__c[] Bhrs:rList ){ // (2) Create list of ActualHours records called "BrnHrs" for each unique combo of // OppLinks and Resource as the loop iterates thru instances of rList // Error is returned with line below. Can't declare Bhrs prior to loop since is declared in for statement.
BH = [ SELECT Bhrs__r.Actual_Hours__c FROMS FL5_SFL5_Time__c WHERE SFL5_SFL5_Time__c.SFL5_SFDC_Resource__c = :Bhrs__r.SFL5_SFDC_Resource__c AND SFL5_SFL5_Time__c.Opportunity_Link__c = :Bhrs__r.Opportunity_Link__c]; BrnHrs.Actual_Hours__c = '0.00'; // Needs to be reset to zero for each iteration of the loop
// (3) Sum Total Burnt Hours
for (SFL5_SFL5_Time__c BH:BrnHrs){ BrnHrs.Actual_Hours__c += BH.Actual_Hours__c ; } // (4) Retrieve record to update BrntH = [ SELECT Burnt_Hours__c FROM Resource_Tracking__c WHEREResource_Tracking__c.Contact__c = :BH.SFL5_SFDC_Resource__c AND Resource_Tracking__c.Opportunity__c = :BH.Opportunity_Link__c]; // (5) Update Burnt Hours into Opportunity Resource Tracking record Add BrntH.Burnt_Hours__c; // Uses last value of BH.Actual_Hours__c in loop (3) to update BrntH.Burnt_Hours__c
try{ updateBrntH; }catch(ListException e) { String myMessage4 = 'Unable to update Burnt Hours for this record with ID ='; // List Exception handling code here System.debug(e.getDmlMessage + myMessage4 + BrntH.Id); } }

 

Any suggestions on how to solve this problem would be most appreciated for this newbie to APEX and this type of implicit loop programming.

 

I'm confident this isn't the most efficient way to code this in APEX, but it's the only way I've been able to figure out how to approach writing the code for this within the constraints of APEX based on my experience of having used other programming languages. Were this in Fortran or C, I'd have been finished long ago.

 

Once I get a better grasp of a few more concepts, I think I'll do much better. :)

 

- pixel

Noam.dganiNoam.dgani

Hi Pixel

 

It's hard to understand the logic of your code from the snippet you published and your explanation.

 

but,

a few concepts to think about:

 

  1. dont query inside a for loop - you will hit governer limits really quick. bulkify your triggers to operate on... well, bulks of records.
  2. regarding the loops

if you have a list (LST) of object A and you want to interate it, you can do

for(Integer i = 0; i < LST.size(); i ++

{

 

}

Noam.dganiNoam.dgani

oops, hit enter too fast.

 

in any case, the for loop example above is the C way.

 

or you can do:

 

for(A a: LST)

{

    //your logic

}

 

hope this helps - remember - no query (or DML) in a for loop - very important

pixelpixel

I've tried numerous times to get this to work looping the "C way" and it's simply not going to get off the ground doing it that way. I understand why you're saying not to nest queries, but I don't have much choice in the matter. I don't expect to have many records hit the trigger at any given time; 5 to 10 at the most.

 

Here's some simplified code that might be easier to understand:

 

Trigger RcrdTimeUpdatesToBurntHours on Rcrd_Time (after update) {
    Rcrd_Time__c BrntH ;
    Rcrd_Time__c BHrs ;
    List<Rcrd_Time__c> BrnHrs;
    List<Rcrd_Time> Bhrs;

    // Get count and record IDs being processed by trigger
    AHrsID = [SELECT Rcrd_Time__c.Id FROM Rcrd_Time__c
                      WHERE Rcrd_Time__c.Id IN :Trigger.new];
    // This single query finds the ID of every Time record that is associated with the trigger


// (1) Collect data to calculate new Burnt Hours as part of loop for each resource/opplink combo on Time Records that hit the trigger

	rList = [select id, Actual_Hours__c, Resource__c, Opp_Link__c 
	from Rcrd_Time where Rcrd_Time.Id = :AHrsID.Id];
	for (Rcrd_Time[] Bh:rList ){       

// (2) Create list of ActualHours records called for each unique combo of OppLinks and Resource as the loop iterates thru instances of rList
	BH = [SELECT Bhrs.Actual_Hours__c
	FROM Rcrd_Time__c
	WHERE Rcrd_Time__c._Resource__c = :Bhrs.Resource__c
	AND Rcrd_Time__c.Opp_Link__c = :Bhrs.Opp_Link__c];

	// (3) Sum Actual Hours the Resource spent on Opportunity (= Total Burnt Hours) 
	for (Rcrd_Time__c BH:BrnHrs){
	BrnHrs.Actual_Hours__c += BH.Actual_Hours__c ;
	}

// (4) Retrieve Resource Tracking record to update
		BrntH = [SELECT Burnt_Hours__c
		FROM Resource_Tracking__c
		WHERE Resource_Tracking__c.Contact__c = :BH.Resource__c 
		AND Resource_Tracking__c.Opportunity__c = :BH.Opp_Link__c];

// (5) Update Burnt Hours into Opportunity Resource Tracking record
	
	Add BrntH.Burnt_Hours__c;  
          // Uses last value of BH.Actual_Hours__c in loop (3) to update BrntH.Burnt_Hours__c

	update BrntH;
	}
}
}
					

 

What I hope this helps show is that there are 3 items that I'm working with: Actual Hours, Resource, and Opportunity that are passed into the Trigger with Record_Time__c.  Using the pairs of Resource and Opportunity links, a number of time records are retrieved to calculate the total Actual Hours a resource has expended on an Opportunity.

 

Once the the Total is calculated for a given Resource and Opportunity, the Resource Tracking Record is retrieved and the Burnt Hours field is updated with the new total of Actual Hours (Burnt Hours = sum Actual Hours). 

 

So my problem is that if more than one Time record hits the trigger, it could have a different combination of Resource and Opportunity links associated with the Actual Hours in it. I need to know those before I can query to get all of the actual hours records for each combination and to retrieve the associated Resource Tracking Record.

 

I then need to sum those Actual Hours records for that combination of Resource and Opportunity to get new totals to put into the associated Resource Tracking Record.  You might say I have to "drill down" to get to where I ultimately need to get.

 

Is this an appropriate place to perhaps use a map? A traditional for loop won't allow me to associate any of the records with an iterator so I don't see how to use that type of logic in processing it that way.

 

Thanks for your replies. :)

 

- pixel

 

Noam.dganiNoam.dgani
Trigger RcrdTimeUpdatesToBurntHours on Rcrd_Time (after update) {

	
	/*
		Iterate the records in the trigger and understand what resources and opplinks are relevant.
		create a set of resource ids (which are contact ids)
		and a set of opp links (which are opportunity ids
	*/
	
	set<Id> resourceIds = new set<Id>();//relevant resource ids
	set<Id> oppIds = new set<Id>();//relevant opp ids
	map<Id,Rcrd_Time__c> relevantRcrdTimes = new map<Id,Rcrd_Time__c>();//the records that had a relevant update
	map<Id,map<id,List<Rcrd_Time__c>>> opplink_to_resource_to_listOfRcrdTimes = new map<Id,map<Id,List<Rcrd_Time__c>>>();
	
	for(Rcrd_Time__c rTime : trigger.new)
	{
		//NOTE: this is an update trigger - what is the update you are looking to fire this process? any update on Rcrd_Time? thats not logical - set a filter for relevant records
		if(a relevant update occured)
		{
			//if a previous Rcrd_Time__c record in the trigger hasnt inserted that resource to are set already - insert it to the set
			if(!resourceIds.contains(rTime.Resource__c))
				resourceIds.add(rTime.Resource__c);
			//if a previous Rcrd_Time__c record in the trigger hasnt inserted that opp id to are set already - insert it to the set
			if(!oppIds.contains(rTime.Opp_Link__c))
				oppIds.add(rTime.Opp_Link__c);
			//maintain a map of record id to record of your relevant record times
			relevantRcrdTimes.put(rTime.Id,rTime);
			
			//create a grouping of Rcrd_Time__c records in the trigger by oppId and resourceid
			if(!opplink_to_resource_to_listOfRcrdTimes.containsKey(rTime.Opp_Link__c))
				opplink_to_resource_to_listOfRcrdTimes.put(rTime.Opp_Link__c,new map<Id,List<Rcrd_Time__c>>());
			if(!opplink_to_resource_to_listOfRcrdTimes.get(rTime.Opp_Link__c).containsKey(rTime.Resource__c))
			{
				List<Rcrd_Time__c> tempList = new List<Rcrd_Time__c>();
				tempList.add(rTime);
				opplink_to_resource_to_listOfRcrdTimes.get(rTime.Opp_Link__c).put(rTime.Resource__c,tempList);

			}
			else
			{
				opplink_to_resource_to_listOfRcrdTimes.get(rTime.Opp_Link__c).get(rTime.Resource__c).add(rTime);
			}
			
		}
		
	}
	
	FROM THIS POINT ON, I STILL DO NOT UNDERSTAND WHAT YOU WANT TO DO. BUT, I HOPE THIS SETS YOU ON THE RIGHT PATH

}

 

pixelpixel

Thank you. I will study your code to see if it helps streamline what I've done in the mean time this morning which was to create an interim temp list from the initial for loop so I could close it.

 

I used that temp list to then get the lists of Actual Hours records that needed to be summed for each combination of resource and opportunity. From there I was able to call up the Resource list and update it with the new Burnt Hours (Total of the Actual Hours for the Resource on that Opportunity). In doing it that way, I think I avoided deeply nesting my queries.

 

I didn't include my other code which tested for some of the conditions you've noted. Thank you for your time and attention. Will let you know if what I've done works once I've actually tested it beyond just getting it to compile. :)

 

- pixel

pixelpixel

I took a closer look at your reply and have some comments.

 

In the opening section you wrote:

 

// Iterate the records in the trigger and understand what resources and opplinks are relevant. create a set of resource ids (which are contact ids)and a set of opp links (which are opportunity ids 

 

 

Actually, in the custom Time Record, the resource links and opp links go to a custom application and ARE NOT "true" contact id's or opportunity id's in the sense you might be thinking of them as far as the overall Opportunity Application goes. I'm not familiar enough yet with CRM to say for certain, but my impression is that they're linked to custom objects in another part of the custom application.

 

I think I now get why you didn't understand what I'm trying to accomplish. This section of code below was what gave it away to me. The relevant resource ids and opp ids are linked. They only occur in pairs so to speak. Resource ids and opp ids from different time records would be irrelevant. These two items really operate in pairs.

 

 

set<Id> resourceIds = new set<Id>();

//relevant resource ids 

set<Id> oppIds = new set<Id>();

//relevant opp ids 

 

 

 

You asked a very valid question below:

 

//NOTE: this is an update trigger - what is the update you are looking to fire this process? 

// any update on Rcrd_Time? that's not logical - set a filter for relevant records if(a relevant update occurred) 

 

 

Updates only occur if there's a change in Actual_Hours in the Time Record. If the Opp is changed or the Resource is changed, I've been told not to worry about it, that it gets updated later during a bulk update. If the opportunity is reassigned, then the existing resource doesn't care about the Burnt Hours tracking. At least that's what I've been told.

 

So, I only check to see if there's a Resource and Opportunity Link that I can use to locate the relevant Resource Tracking record that contains the Burnt Hours (Total of Actual Hours spent on the Opportunity by the Resource). Note: Only 1 Resource is assigned to an Opportunity in this custom application. Perhaps that's the essential piece of the puzzle that wasn't conveyed.

 

So when you ask what kind of update I'm looking for that should fire this trigger, its any kind of change in Actual Hours by a Resource assigned to an Opportunity. There shouldn't be a change in Resource or Opportunity that triggers it. If there is, then that's a management reassignment or reallocation of resources of some kind which the trigger isn't intended to handle at this time. 

 

For all I know, that refinement may become an assignment at some point later in time. I think putting in error handling code for it would be a good idea before this is implemented. More than likely, for the time being, I'd expect to see it being a case where no Resource Tracking record is returned when the new Time Record hits the trigger, but I've still much to learn.

 

Below appears to be a creation of a series of maps which I'm still trying to understand how I'd use them. Based on what I've said above to try and clarify things. Could you please explain what you've done or what I would need to change to utilize this approach?

 

 

} 

set<Id> resourceIds = new set<Id>();

//relevant resource ids 

set<Id> oppIds = new set<Id>();

//relevant opp ids 

map<Id,Rcrd_Time__c> relevantRcrdTimes = new map<Id,Rcrd_Time__c>();

//the records that had a relevant update 

map<Id,map<id,List<Rcrd_Time__c>>> opplink_to_resource_to_listOfRcrdTimes = 

new map<Id,map<Id,List<Rcrd_Time__c>>>(); 

for(Rcrd_Time__c rTime : trigger.new) { 

{

//if a previous Rcrd_Time__c record in the trigger hasn't inserted that resource to are set
 already - insert it to the set

if(!resourceIds.contains(rTime.Resource__c))

resourceIds.add(rTime.Resource__c);

//if a previous Rcrd_Time__c record in the trigger hasn't inserted that opp id to are set
 already - insert it to the set

if(!oppIds.contains(rTime.Opp_Link__c))

oppIds.add(rTime.Opp_Link__c);

//maintain a map of record id to record of your relevant record times

relevantRcrdTimes.put(rTime.Id,rTime);



//create a grouping of Rcrd_Time__c records in the trigger by oppId and resourceid

if(!opplink_to_resource_to_listOfRcrdTimes.containsKey(rTime.Opp_Link__c))

opplink_to_resource_to_listOfRcrdTimes.put(rTime.Opp_Link__c,new map<Id,List<Rcrd_Time__c>>());

if(!opplink_to_resource_to_listOfRcrdTimes.get(rTime.Opp_Link__c).containsKey(rTime.Resource__c))

{

List<Rcrd_Time__c> tempList = new List<Rcrd_Time__c>();

tempList.add(rTime);

opplink_to_resource_to_listOfRcrdTimes.get(rTime.Opp_Link__c).put(rTime.Resource__c,tempList);

} else

{

opplink_to_resource_to_listOfRcrdTimes.get(rTime.Opp_Link__c).get(rTime.Resource__c).add(rTime);

}

}

}

// FROM THIS POINT ON, I STILL DO NOT UNDERSTAND WHAT YOU WANT TO DO. BUT, I HOPE THIS SETS YOU

 ON THE RIGHT PATH

}

 

  

Here's the approach I was trying to use that still isn't working. Perhaps you or someone else will be able to understand further if I post it and have some suggestions. I know I'm not quite there yet when it comes to understanding the proper use of syntax and am having trouble with the 2nd loop.

 

 

Trigger RcrdTimeUpdatesToBurntHours on Rcrd_Time (after update) { 

// Get count and record IDs being processed by trigger  

AHrsID = [SELECT Rcrd_Time__c.Id FROM Rcrd_Time__c WHERE Rcrd_Time__c.Id IN :Trigger.new]; 

// This single query finds the ID of every Time record that is associated with the trigger  

// (1)Retrieve time records and put into a list 

List<Rcrd_Time__c> rList = [SELECT Id, Actual_Hours__c, Opp_Link__c, Resource__c FROM Rcrd_Time__c]; 

for (List<Rcrd_Time__c> r2List : AHrsID){ 

// Line below doesn't work. Perhaps I need to use get?? Like getSObject String fieldName sObject 
(schema.sObject.field) ??  

rList.id = r2List.id; 
update rList; } // (2) Create list of ActualHours records called "BrnHrs" for each unique combo of OppLinks and Resource as the loop iterates thru instances of rList Rcrd_Time__c Brn_Hrs; List<Resource_Tracking__c> NBrntHrs; for (Rcrd_Time__c BrntH : rList){ BrntH = [SELECT Actual_Hours__c FROM Rcrd_Time__c WHERE Rcrd_Time__c.Resource__c = :rList.Resource__c AND Rcrd_Time__c.Opp_Lnk__c = :rList.Opp_Lnk__c]; update BrntH; // (3) Retrieve Resource Tracking record to update for this combo of OppLinks & Resource NBrntHrs = [SELECT Id, Burnt_Hours__c FROM Resource_Tracking__c WHERE Resource_Tracking__c.Contact__c = :BrntH.Resource__c AND Resource_Tracking__c.Opportunity__c = :BrntH.Opp_Lnk__c]; Brn_Hrs.Actual_Hours__c = '0.00'; // Needs to be reset to zero for each iteration of the loop // (4) Sum Total Burnt Hours for this combo for (SFL5_SFL5_Time__c B_Hrs : BrntH){ Brn_Hrs.Actual_Hours__c += B_Hrs.Actual_Hours__c ; update Brn_Hrs; } // (5) Update Burnt Hours into Opportunity Resource Tracking record NBrntHrs.Burnt_Hours__c = Brn_Hrs.Actual_Hours__c; // Uses last value of Brn_Hrs.Actual_Hours__c in loop (4) to update NBrntHrs.Burnt_Hours__c update NBrntHrs; } }

 

  

I feel like the above method is on the right track, but the syntax is incorrect. I'm just not experienced enough yet with APEX to understand the nuances of the API or the syntax to figure out the corrections I need to make so that it will compile. I'm confident that a map would be the most efficient way to do this. With experience, I hope to get to where I'll be able to do that.

 

Suggestions and comments anyone??

 

- pixel