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
Oliver_DunfordOliver_Dunford 

Help constructing Apex Class

Hi Everyone, 

 

I'm trying to learn APEX and wonder whether you could help.  I have a trigger which simply calls a class when a Slot__c record is inserted or updated.

 

trigger InsertAvailableServices on Slots__c (after insert, after update) {
    //Pass new/updated slot record to class    	
    AvailableServicesController.handleOpeningChange(Trigger.old, Trigger.new);    	
}

 My class is as follows and works until I add the following line and then it all goes rather wrong. 

 

insert availableTreatments; 

 

public class AvailableServicesController {
        public class AvailableServicesException extends Exception {}
        public static void handleOpeningChange(List<Slots__c> oldSlots, List<Slots__c> newSlots) { 

            for (Slots__c slot : newSlots) {  

            List<Treatment__c> treatments = [select Id, Name, Account__c, Duration_Int__c, Price__c from Treatment__c where Account__c =: slot.Account__c and Duration_Int__c <=: slot.Duration_mins__c];
            
            if (treatments.size() == 0) {  
                throw new AvailableServicesException('There are no Available Products/Services for your opening duration');  // then an exception is thrown.
            }
            
                List<Available_Treatments__c> availableTreatments = new List<Available_Treatments__c>();
	       	
	            for (Treatment__c treatment : treatments) {
	               if (treatment.Duration_Int__c <= slot.Duration_mins__c) {
	                   AvailableTreatments.add(new Available_Treatments__c(Slot__c = slot.id, treatment__c = treatment.id, Start_Time__c = slot.Start_Time__c,
	                                                                       Duration_mins__c = treatment.Duration_Int__c,
	                                                                       Internet_Price__c = treatment.Price__c - ((treatment.Price__c * slot.Discount__c) / 100),
	                                                                       Treatments_for_Slot__c = ((slot.Duration_mins__c / treatment.Duration_Int__c).round(roundingMode.DOWN))));          
	                }
	            }
	            
	            insert availableTreatments; 
            }

        }
    } 

I get this error: 

 

Insert failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, InsertAvailableServices: maximum trigger depth exceeded
Slots trigger event AfterInsert for [a00i0000003ejIb]
Slots trigger event AfterUpdate for [a00i0000003ejIb]

 

I'm sure it's because I'm trying to do everything in for loops of death, but here are the things I can't quite work out. 

 

1.  When a Slots__c record is inserted I need to store information about it for use in SOQL queries e.g. AccountId and Duration_mins__c.  I had it working with a load of variables but it just feels messy.  Not sure what the best way to do this is?

 

2.  For each inserted Slots__c record I want to query a list of Treatment__c based on criteria from the Slot__c record.  I then want to insert these records into the Available_Treatments__c object.  I also need to populate this object with information from the inserted Slot__c record.

 

My challenge is that I always need to re-use information from the Slot__c record throughout my SOQL queries and when I'm populating the list for insert.  I could put everything in variables but is there a better way?

 

Thanks

 

Best Answer chosen by Admin (Salesforce Developers) 
Alex.AcostaAlex.Acosta

Strange... usually booleans are considered false if they are null by default... do this inside instead for your if statement...

 

if(null == AvailableServicesController.isRunning || !AvailableServiceController.isRunning){

 

 

Post final Answer below:

 

IE:

class

public class AvailableServicesController {
	public class AvailableServicesException extends Exception {}
	
	public static Boolean isRunning {get;set;} /*****Newly added code *****/
	
	public static void handleOpeningChange(List<Slots__c> oldSlots, List<Slots__c> newSlots) { 

		for (Slots__c slot : newSlots) {  

		List<Treatment__c> treatments = [select Id, Name, Account__c, Duration_Int__c, Price__c from Treatment__c where Account__c =: slot.Account__c and Duration_Int__c <=: slot.Duration_mins__c];
		
		if (treatments.size() == 0) {  
			throw new AvailableServicesException('There are no Available Products/Services for your opening duration');  // then an exception is thrown.
		}
		
			List<Available_Treatments__c> availableTreatments = new List<Available_Treatments__c>();
		
			for (Treatment__c treatment : treatments) {
			   if (treatment.Duration_Int__c <= slot.Duration_mins__c) {
				   AvailableTreatments.add(new Available_Treatments__c(Slot__c = slot.id, treatment__c = treatment.id, Start_Time__c = slot.Start_Time__c,
																	   Duration_mins__c = treatment.Duration_Int__c,
																	   Internet_Price__c = treatment.Price__c - ((treatment.Price__c * slot.Discount__c) / 100),
																	   Treatments_for_Slot__c = ((slot.Duration_mins__c / treatment.Duration_Int__c).round(roundingMode.DOWN))));          
				}
			}
			
			insert availableTreatments; 
		}

	}
} 

 

trigger:

trigger InsertAvailableServices on Slots__c (after insert, after update) {
  if(null == AvailableServicesController.isRunning || !AvailableServicesController.isRunning){
    // prevent code from activating multiple times
    AvailableServicesController.isRunning = true;
    //Pass new/updated slot record to class    	
    AvailableServicesController.handleOpeningChange(Trigger.old, Trigger.new); 
  }   	
}

 

All Answers

Alex.AcostaAlex.Acosta

Do you have a rollup or some sort of workflow on this?

 

What I'm guessing just skimming through the code is that you're reactivating your trigger. This can occur if you have a rollup field on your master object which seems to be your Slots__c Sobject.

 

What I suggest to avoid it from rolling up multiple times is by having a static varible which can tell you if you've already activated your class for this session.

 

You may have to tweek your static variables from the example i provided if you want further control of what you're looking for.... hopefully this helps.

 

IE:

class

public class AvailableServicesController {
	public class AvailableServicesException extends Exception {}
	
	public static Boolean isRunning {get;set;} /*****Newly added code *****/
	
	public static void handleOpeningChange(List<Slots__c> oldSlots, List<Slots__c> newSlots) { 

		for (Slots__c slot : newSlots) {  

		List<Treatment__c> treatments = [select Id, Name, Account__c, Duration_Int__c, Price__c from Treatment__c where Account__c =: slot.Account__c and Duration_Int__c <=: slot.Duration_mins__c];
		
		if (treatments.size() == 0) {  
			throw new AvailableServicesException('There are no Available Products/Services for your opening duration');  // then an exception is thrown.
		}
		
			List<Available_Treatments__c> availableTreatments = new List<Available_Treatments__c>();
		
			for (Treatment__c treatment : treatments) {
			   if (treatment.Duration_Int__c <= slot.Duration_mins__c) {
				   AvailableTreatments.add(new Available_Treatments__c(Slot__c = slot.id, treatment__c = treatment.id, Start_Time__c = slot.Start_Time__c,
																	   Duration_mins__c = treatment.Duration_Int__c,
																	   Internet_Price__c = treatment.Price__c - ((treatment.Price__c * slot.Discount__c) / 100),
																	   Treatments_for_Slot__c = ((slot.Duration_mins__c / treatment.Duration_Int__c).round(roundingMode.DOWN))));          
				}
			}
			
			insert availableTreatments; 
		}

	}
} 

 

trigger:

trigger InsertAvailableServices on Slots__c (after insert, after update) {
  if(!AvailableServicesController.isRunning){
    // prevent code from activating multiple times
    AvailableServicesController.isRunning = true;
    //Pass new/updated slot record to class    	
    AvailableServicesController.handleOpeningChange(Trigger.old, Trigger.new); 
  }   	
}

 

Oliver_DunfordOliver_Dunford

Thanks Alex, I think you may well be correct, darn I hadn't even considered that.  I have workflows and roll-up summary fields on that object. 

 

I added your code but now seem to get another error, any ideas?  Thanks again for your help!

 

27.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WORKFLOW,INFO
20:20:43.188 (188650000)|EXECUTION_STARTED
20:20:43.188 (188684000)|CODE_UNIT_STARTED|[EXTERNAL]|01qi00000005Hmo|InsertAvailableServices on Slots trigger event AfterInsert for [a00i0000003ejqg]
20:20:43.203 (203300000)|METHOD_ENTRY|[1]|01pi0000002ZYXn|AvailableServicesController.AvailableServicesController()
20:20:43.203 (203331000)|METHOD_EXIT|[1]|AvailableServicesController
20:20:43.203 (203608000)|FATAL_ERROR|System.NullPointerException: Attempt to de-reference a null object

Trigger.InsertAvailableServices: line 3, column 1
20:20:43.203 (203633000)|FATAL_ERROR|System.NullPointerException: Attempt to de-reference a null object

Trigger.InsertAvailableServices: line 3, column 1
20:20:43.277 (203689000)|CUMULATIVE_LIMIT_USAGE
20:20:43.277|CUMULATIVE_LIMIT_USAGE_END

20:20:43.203 (203739000)|CODE_UNIT_FINISHED|InsertAvailableServices on Slots trigger event AfterInsert for [a00i0000003ejqg]
20:20:43.203 (203750000)|EXECUTION_FINISHED
Alex.AcostaAlex.Acosta

is the if statement on line 3 of your trigger?

Oliver_DunfordOliver_Dunford

It sure is, this is the trigger.  

 

trigger InsertAvailableServices on Slots__c (after insert, after update) {
    //Pass new/updated slot record to class    	
   if(!AvailableServicesController.isRunning){
	    // prevent code from activating multiple times
	    AvailableServicesController.isRunning = true;
	    //Pass new/updated slot record to class   	
	    AvailableServicesController.handleOpeningChange(Trigger.old, Trigger.new); 
  	}   	  	
}

 I'll eventually perform different logic for inserts and udpates as well as a few checks on record types so I'm more than open to doing things differently.   Here's the class now with your code included:

 

public class AvailableServicesController {
        public class AvailableServicesException extends Exception {}
      	public static Boolean isRunning {get;set;}

        public static void handleOpeningChange(List<Slots__c> oldSlots, List<Slots__c> newSlots) { 
            for (Slots__c slot : newSlots) {  

            List<Treatment__c> treatments = [select Id, Name, Account__c, Duration_Int__c, Price__c from Treatment__c where Account__c =: slot.Account__c and Duration_Int__c <=: slot.Duration_mins__c];
            
            if (treatments.size() == 0) {  
                throw new AvailableServicesException('There are no Available Products/Services for your opening duration');  // then an exception is thrown.
            }
            
                List<Available_Treatments__c> availableTreatments = new List<Available_Treatments__c>();
	       	
	            for (Treatment__c treatment : treatments) {
	               if (treatment.Duration_Int__c <= slot.Duration_mins__c) {
	                   AvailableTreatments.add(new Available_Treatments__c(Slot__c = slot.id, treatment__c = treatment.id, Start_Time__c = slot.Start_Time__c,
	                                                                       Duration_mins__c = treatment.Duration_Int__c,
	                                                                       Internet_Price__c = treatment.Price__c - ((treatment.Price__c * slot.Discount__c) / 100),
	                                                                       Treatments_for_Slot__c = ((slot.Duration_mins__c / treatment.Duration_Int__c).round(roundingMode.DOWN))));          
	                }
	            }
	            
	            insert availableTreatments; 
            }

        }
    }

 

Alex.AcostaAlex.Acosta

Strange... usually booleans are considered false if they are null by default... do this inside instead for your if statement...

 

if(null == AvailableServicesController.isRunning || !AvailableServiceController.isRunning){

 

 

Post final Answer below:

 

IE:

class

public class AvailableServicesController {
	public class AvailableServicesException extends Exception {}
	
	public static Boolean isRunning {get;set;} /*****Newly added code *****/
	
	public static void handleOpeningChange(List<Slots__c> oldSlots, List<Slots__c> newSlots) { 

		for (Slots__c slot : newSlots) {  

		List<Treatment__c> treatments = [select Id, Name, Account__c, Duration_Int__c, Price__c from Treatment__c where Account__c =: slot.Account__c and Duration_Int__c <=: slot.Duration_mins__c];
		
		if (treatments.size() == 0) {  
			throw new AvailableServicesException('There are no Available Products/Services for your opening duration');  // then an exception is thrown.
		}
		
			List<Available_Treatments__c> availableTreatments = new List<Available_Treatments__c>();
		
			for (Treatment__c treatment : treatments) {
			   if (treatment.Duration_Int__c <= slot.Duration_mins__c) {
				   AvailableTreatments.add(new Available_Treatments__c(Slot__c = slot.id, treatment__c = treatment.id, Start_Time__c = slot.Start_Time__c,
																	   Duration_mins__c = treatment.Duration_Int__c,
																	   Internet_Price__c = treatment.Price__c - ((treatment.Price__c * slot.Discount__c) / 100),
																	   Treatments_for_Slot__c = ((slot.Duration_mins__c / treatment.Duration_Int__c).round(roundingMode.DOWN))));          
				}
			}
			
			insert availableTreatments; 
		}

	}
} 

 

trigger:

trigger InsertAvailableServices on Slots__c (after insert, after update) {
  if(null == AvailableServicesController.isRunning || !AvailableServicesController.isRunning){
    // prevent code from activating multiple times
    AvailableServicesController.isRunning = true;
    //Pass new/updated slot record to class    	
    AvailableServicesController.handleOpeningChange(Trigger.old, Trigger.new); 
  }   	
}

 

This was selected as the best answer
Oliver_DunfordOliver_Dunford

Alex, massive thanks for your help! it's working nicely :-D

 

I know I have hassled you a lot already but I wonder whether you had any suggestions on improving the code at all?  I am learning, so want to ensure I'm doing things as they should be done.  If you're busy then don't worry about it, you have done enough already :-D

Alex.AcostaAlex.Acosta

Only advice I can really give you is pull all your records prior to making quries within loops. I'll take a closer look later today after work for you. I'll also edit my  chosen post as an answer to correctly reflect all the code / changes in clase someone ever has a similar issue.

Oliver_DunfordOliver_Dunford

That's awesome, thanks Alex! 

 

I found a great thread and I would like to use this trigger which hands all logic off to the class:

 

trigger InsertAvailableServices on Slots__c (before insert, before update, after insert, after update) {
    if(trigger.isAfter){
        if(trigger.isInsert){
            OpeningInsertAfter myInsertAfter = new OpeningInsertAfter(Trigger.new);
        }
        if(Trigger.isUpdate){
        	//OpeningUpdateAfter myUpdateAfter = new OpeningUpdateAfter(Trigger.old,Trigger.new);
        }
    }
	  	
}

 Then start to build out the class as follows which would let me pull the records before all the for loops.

 

public class OpeningInsertAfter {
	
//create your variables and data structures
    //constructor
    public OpeningInsertAfter() { 

    }
    
    //constructor accepting a list of myObjects
    public OpeningInsertAfter(Slots__c[] myOpenings){
    	
    //call whatever methods you need to get the job done
    }        
}

 But this is currently over my head and I wouldn't even know where to start! 

Alex.AcostaAlex.Acosta

Mostly because I don't like coding 2 very similar methods, I've combined your call into 1 using the following method for you

 

public class HandleOpeningChange {	
	public HandleOpeningChange(Map<String, Slots__c> oldSlots, List<Slots__c> newSlots) { 
		Boolean isUpdate = false;
		if(null != oldSlots && oldSlots.keySet().size() != 0) isUpdate = true;
		/* At the point above you'll know if it's an update or not... use IF statment as desired */

... other code ....
} }

 

Trigger would look like this now:

trigger InsertAvailableServices on Slots__c (before insert, before update, after insert, after update) {
/* I'm also a defensive programmer so in case the event types ever change, it will not fire on delete or undelete events. */ if(trigger.isAfter && !trigger.isDelete && !trigger.isUndelete){ OpeningUpdateAfter myUpdateAfter = new OpeningUpdateAfter(Trigger.oldMap, Trigger.new); } }

 

 

 

Otherwise you can have more than 1 constructor

 

public class HandleOpeningChange {
	// code for handling updates
	public HandleOpeningChange(Map<String, Slots__c> oldSlots, List<Slots__c> newSlots) { 
	     ... code ....
	}

        // code for handling inserts
	public HandleOpeningChange(List<Slots__c> newSlots) { 
	     ... code ....
	}
} 

 

Also keep in mind, I have made your 2 lists to a map and list. If it's an update, you can get your current updated record, and pull the old information by using the Id as such

// code for handling updates
public HandleOpeningChange(Map<String, Slots__c> oldSlots, List<Slots__c> newSlots) { 
     for(Slots__c s :newSlots){
Slots__c oldSlotRecord = oldSlots.get(s.Id);
// s being your current slot record
// oldSlotRecord being prior values on your slot record
} }

 

 

Alex.AcostaAlex.Acosta
Many updates to original post... done today, and sorry for the delay. Swamped at work
Oliver_DunfordOliver_Dunford

You sir are awesome, it's a long and painful road learning APEX.

 

I really liked your Map suggestion, this allows me to access all the Slot__c information that I need throughout my SOQL queries and insert list.   This enabled me to remove some of my logic from the For Loop which is always a bonus!

 

I don't have much to offer you in return but I did find a really interesting resource on the Instance method.  This prevents mutliple instances of the class being initiated at any one time and is something you helped me with previously.  It seems like a nicer way to do things maybe...

 

check it out at just over 5mins.

 

http://www.youtube.com/watch?v=J372XmYds-A&list=PLtmcj-0Z5b8iziDedmgW4DrwccJHcUNAT