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
RelaxItsJustCodeRelaxItsJustCode 

Please help, Pros and Cons between Triggers vs. Trigger and Class approach

 

 

I have recently created a trigger that validates that 3 LOOKUP Field related objects have completed status before allowing a user to complete a case the is being used for project management.

 

What I need to know is what is the benefit of using a Trigger and a Class Vs. just a trigger, if there is any?

 

Which is easier normally to do unit test on for code coverage?

 

Please let me know what you think.

 

Thank you,

Steve Laycock

Best Answer chosen by Admin (Salesforce Developers) 
SFAdmin5SFAdmin5

yeah chris is right.  it's not that a trigger alone is better or worse than a trigger that calls a class, it's just that using a class can allow you to write code once and then recycle it in another class or trigger in the future, so you don't have to do everything again. also it can keep your trigger code leaner and simpler, especially when you start dealing with advanced implementations where you've got lots of trigger logic within one trigger on one object all doing different things.  instead of having a fifty line trigger to do one thing you can have a 3 line trigger that calls the class where all the logic is processed.  generally speaking, unless it's a really simple trigger, the more work you can have done in a class for a trigger the better.  but at the end of the day if it works all in a trigger then in opinion there's nothing wrong with that at all.  

All Answers

chris.noechris.noe

Steve,

From a testing/code coverage perspective, there is no difference in the two implemenation methodoligies you list below.  Your test class  doesn't need to be modified in any way to ensure adequate coverage when using a trigger vs. trigger/class.  The main benefit of using the trigger/class approach is reusability of the class logic by other triggers/classes.

 

FYI, if I understand your requirements correctly below, you wouldn't need to do this with a trigger/class.  You should be able to accomplish this same logic with a Case validation rule that includes the following error condition formula:

 

OR(Case.RelatedObject1.Status!='Completed',Case.RelatedObject2.Status!='Completed',Case.RelatedObject3.Status!='Completed')

RelaxItsJustCodeRelaxItsJustCode

I wish I could do it via a formula but this is a one Case to Many Tasks, Many Training Information Records, and Many Developmnent request so a formula field is not going to work.

 

Plus you can't create a formula field in this context.

 

Also, rollup fields are out because it is a LookUp relationship model.

 

So we are back to Triggers or Triggers and class.

 

Please let me know what you think.  Thank you for your response....  Ideas???

 

Thank you

Steve Laycock

SFAdmin5SFAdmin5

yeah chris is right.  it's not that a trigger alone is better or worse than a trigger that calls a class, it's just that using a class can allow you to write code once and then recycle it in another class or trigger in the future, so you don't have to do everything again. also it can keep your trigger code leaner and simpler, especially when you start dealing with advanced implementations where you've got lots of trigger logic within one trigger on one object all doing different things.  instead of having a fifty line trigger to do one thing you can have a 3 line trigger that calls the class where all the logic is processed.  generally speaking, unless it's a really simple trigger, the more work you can have done in a class for a trigger the better.  but at the end of the day if it works all in a trigger then in opinion there's nothing wrong with that at all.  

This was selected as the best answer
BritishBoyinDCBritishBoyinDC

The other thing I would add to SFADmin5 comments is that class based logic allows you to control the sequence of execution since you can't rely on the order triggers are fired in - so if you have more than one trigger on an object, or a package that also fires triggers, you can use the class as a way to identify where you are in the process, and fire logic accordingly...

RelaxItsJustCodeRelaxItsJustCode

Good to know...

 

Thank you,

Steve Laycock

SFAdmin5SFAdmin5

yeah that is a great point.  it's a pretty advanced steatemtnt, but you're right.  i'm just getting started on dan appelman's advanced apex programming book and what you are saying is perfectly in line with that.  

 

it's definitely advanced what BritishBoiyinDc is saying, but he's definitely right.  learning how to control the order of execution is huge with the programmatic side of sfdc, and it's not easy to do, but he's spot on with his comment.  the only way you can deal with ordering the execution of multiple triggers on one object  is with classes.

RelaxItsJustCodeRelaxItsJustCode
trigger CaseTriggerBeforeComplete on Case (before update)
{
      CaseFunctions.CaseValidationBeforeComplete(Trigger.new, Trigger.newMap);
}

Ok I came up a trigger that is thin but does specifically what I need to do. Plus it works with the class in this post.

I have a few questions though,  Seeing that the class is "CaseFunctions" we should be able to add even more case related functionality. I'm not so sure I'm using the trigger's variables, Trigger.New and Trigger.NewMap correctly.

 

I would like the trigger and class to be able to compare the oldMap as well as the newMap when we address adding more functions to this class.

 

Also, I need an opion on the class, it is currently only applies to one use case I can think of but it may be worth while to split it into separate functions later but that said,

 

How in a single trigger would I call the separate functions at the same time or should I just build three separate triggers.... Which would be the best approach in your thoughts???

 

Also, I would like to build an escape for the current user's profile "Name" for example "System Administrator". I have no idea how to achieve these things I've tried to work it out myself but unfortunately I haven't found the correct approach. Please help if you can field this one.

 

Thanks,

Steve Laycock

 

public class CaseFunctions
{
   public static void CaseValidationBeforeComplete(Case[] caseRecs, Map<ID,Case> newMap)
   {
        List<Id> caseIds = new List<Id>();
        
        Id caseRecordTypeId = [SELECT Id FROM RecordType WHERE Name = 'PM Case' AND SObjectType = 'Case'].Id;

        Id TaskRecordTypeId = [SELECT Id FROM RecordType WHERE Name = 'PM Task' AND SObjectType = 'Task'].Id;
        
        for (Case c : caseRecs)
        {
            Case newRec = newMap.get(c.Id);
            if(c.RecordTypeId == caseRecordTypeId && (c.Status == 'Completed' || c.Status == 'Closed'))
            {
                 caseIds.add(c.Id);
            }
        }
        for(Training_Information__c customobj : [Select Id, Status__c,Case__c From Training_Information__c Where Case__c in: caseIds])
        {
            if(customobj.Status__c == 'Pending' || customobj.Status__c == 'Scheduled')
            {    
                 newMap.get(customobj.Case__c).addError('Training has not been completed.');
            }     
        }    
        
        for(Development_Request__c customobj : [Select Id, Stage__c,Case__c From Development_Request__c Where Case__c in: caseIds])
        {
            if(customobj.Stage__c != 'Completed' && customobj.Stage__c != 'Cancelled DR/ VOID' && customobj.Stage__c != 'Draft')
            {
                newMap.get(customobj.Case__c).addError('There are open Development Request associated with this Case.');
            }
        }  

        for(Task T : [Select Id, Status, WhatId, RecordTypeId From Task Where WhatId in: caseIds])
        {
            if(T.RecordTypeId == TaskRecordTypeId && (T.Status != 'Completed' && T.Status != 'Discarded'))
            {
                newMap.get(T.WhatId).addError('There are still open PM Task on this Case.');
            }
        }
    }    
}

 

 

 

 

 

BritishBoyinDCBritishBoyinDC

Re the escape - look at the userinfo functions - there is one to bring back the Profile Id, so you can use that to identify the profile of the current user

 

Re separate methods in one class, you could do something like this with your code - have a method for each trigger action, and then use that as the handling place for calling additional methods for each action: 

 

public class CaseFunctions {

public static beforeinsert (Case[] caseRecs, Map<ID,Case> newMap) {
//before insert logic
}

public static beforeupdate (Case[] caseRecs, Map<ID,Case> newMap) {
CaseValidationBeforeComplete(caseRecs, newMap);
}



   public static void CaseValidationBeforeComplete(Case[] caseRecs, Map<ID,Case> newMap)  {
        List<Id> caseIds = new List<Id>();
        
        Id caseRecordTypeId = [SELECT Id FROM RecordType WHERE Name = 'PM Case' AND SObjectType = 'Case'].Id;

        Id TaskRecordTypeId = [SELECT Id FROM RecordType WHERE Name = 'PM Task' AND SObjectType = 'Task'].Id;
        
        for (Case c : caseRecs) {
            if(c.RecordTypeId == caseRecordTypeId && (c.Status == 'Completed' || c.Status == 'Closed')) {
                 caseIds.add(c.Id);
            }
        }

        for(Training_Information__c customobj : [Select Id, Status__c,Case__c From Training_Information__c Where Case__c in: caseIds])
        {
            if(customobj.Status__c == 'Pending' || customobj.Status__c == 'Scheduled')
            {    
                 newMap.get(customobj.Case__c).addError('Training has not been completed.');
            }     
        }    
        
        for(Development_Request__c customobj : [Select Id, Stage__c,Case__c From Development_Request__c Where Case__c in: caseIds])
        {
            if(customobj.Stage__c != 'Completed' && customobj.Stage__c != 'Cancelled DR/ VOID' && customobj.Stage__c != 'Draft')
            {
                newMap.get(customobj.Case__c).addError('There are open Development Request associated with this Case.');
            }
        }  

        for(Task T : [Select Id, Status, WhatId, RecordTypeId From Task Where WhatId in: caseIds])
        {
            if(T.RecordTypeId == TaskRecordTypeId && (T.Status != 'Completed' && T.Status != 'Discarded'))
            {
                newMap.get(T.WhatId).addError('There are still open PM Task on this Case.');
            }
        }
    } //end case validation    

} //end case management class

 Then the trigger looks like this:

trigger ManageCases on Case (before insert, before update) {

    if(Trigger.isInsert && Trigger.isBefore){
        CaseFunctions.beforeInsert(Trigger.New, Trigger.NewMap); 
    }
       
    if(Trigger.isUpdate && Trigger.isBefore){
        CaseFunctions.beforeUpdate(Trigger.new, Trigger.newMap);
    }  
}

 

RelaxItsJustCodeRelaxItsJustCode

But how do I do a compare between NewMap and OldMap?  Ideas?


BritishBoyinDC wrote:

Re the escape - look at the userinfo functions - there is one to bring back the Profile Id, so you can use that to identify the profile of the current user

 

Re separate methods in one class, you could do something like this with your code - have a method for each trigger action, and then use that as the handling place for calling additional methods for each action: 

 

public class CaseFunctions {

public static beforeinsert (Case[] caseRecs, Map<ID,Case> newMap) {
//before insert logic
}

public static beforeupdate (Case[] caseRecs, Map<ID,Case> newMap) {
CaseValidationBeforeComplete(caseRecs, newMap);
}



   public static void CaseValidationBeforeComplete(Case[] caseRecs, Map<ID,Case> newMap)  {
        List<Id> caseIds = new List<Id>();
        
        Id caseRecordTypeId = [SELECT Id FROM RecordType WHERE Name = 'PM Case' AND SObjectType = 'Case'].Id;

        Id TaskRecordTypeId = [SELECT Id FROM RecordType WHERE Name = 'PM Task' AND SObjectType = 'Task'].Id;
        
        for (Case c : caseRecs) {
            if(c.RecordTypeId == caseRecordTypeId && (c.Status == 'Completed' || c.Status == 'Closed')) {
                 caseIds.add(c.Id);
            }
        }

        for(Training_Information__c customobj : [Select Id, Status__c,Case__c From Training_Information__c Where Case__c in: caseIds])
        {
            if(customobj.Status__c == 'Pending' || customobj.Status__c == 'Scheduled')
            {    
                 newMap.get(customobj.Case__c).addError('Training has not been completed.');
            }     
        }    
        
        for(Development_Request__c customobj : [Select Id, Stage__c,Case__c From Development_Request__c Where Case__c in: caseIds])
        {
            if(customobj.Stage__c != 'Completed' && customobj.Stage__c != 'Cancelled DR/ VOID' && customobj.Stage__c != 'Draft')
            {
                newMap.get(customobj.Case__c).addError('There are open Development Request associated with this Case.');
            }
        }  

        for(Task T : [Select Id, Status, WhatId, RecordTypeId From Task Where WhatId in: caseIds])
        {
            if(T.RecordTypeId == TaskRecordTypeId && (T.Status != 'Completed' && T.Status != 'Discarded'))
            {
                newMap.get(T.WhatId).addError('There are still open PM Task on this Case.');
            }
        }
    } //end case validation    

} //end case management class

 Then the trigger looks like this:

trigger ManageCases on Case (before insert, before update) {

    if(Trigger.isInsert && Trigger.isBefore){
        CaseFunctions.beforeInsert(Trigger.New, Trigger.NewMap); 
    }
       
    if(Trigger.isUpdate && Trigger.isBefore){
        CaseFunctions.beforeUpdate(Trigger.new, Trigger.newMap);
    }  
}

 


 

BritishBoyinDCBritishBoyinDC

OldMap is just that ...a map of the old records - so you can use the Id of the record to retrieve the old record

 

e.g.

 

for (Case c: newrecords) {
if c.Status != oldmap.get(c.Id).Status) {
//process here
}
}