Search

Saturday, 5 March 2016

Trigger Pattern Framework - session 2





In my last blog I introduced my Trigger Pattern Framework, now I will cover Trigger Control, which is one of the main improvements to other well documented frameworks.

DMLs are very processor intensive and conserving this precious resource in a multitenant environment is ever more so important.
So I built into my framework a capability using a Custom Setting to activate / deactivate per trigger for any User, Profile, or the entire Organization; or with a separate Custom Setting to be able to activate / deactivate ALL triggers for any User, Profile, or the entire Organization. This was great in situations where companies require migrating data from 1 system to another and you want to safeguard that no unwanted actions occur through the execution of code in the trigger.


However there are also many situations such as in unit tests where we create test data, to avoid running through all the code in triggers which is not necessary unless we are testing specifically the triggers to disable the triggers using the Custom Settings above will require running a DML and since we are trying to avoid DMLs because they are expensive to run we need another mechanism to bypass the code in the trigger, therefore we introduce a static variable to do this work. Here is a section in the TriggerFactory class that controls the execution using these methods and also the code from the calling classes that are used in this part of the framework.


boolean notriggerSetting;
                 boolean noTriggersPerObject;
                 try{
                      notriggerSetting = TriggerController.globalTriggerControlSetting();   
                      noTriggersPerObject = TriggerController.globalTriggerPerObjectControlSetting(objType);   
                 }
                 catch (Exception ex){system.debug('error in trigger controller ' + ex); }

                Type soType = Type.forName(objType);
               
    if (!notriggerSetting && !noTriggersPerObject && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_ALL)) {  
                               
                                if (Trigger.isBefore){
                                                if (Trigger.isUpdate && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_UPDATE)){
                                                                                handler.beforeUpdate(Trigger.oldmap, Trigger.newmap);               
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_UPDATE), true);                                
                                                }
                                                else if (Trigger.isDelete && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_DELETE)){
                                                                                handler.beforeDelete(Trigger.oldmap);
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_DELETE), true);
                                                }
                                                else if (Trigger.isInsert && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_INSERT)){
                                                                                handler.beforeInsert(Trigger.newmap);                                                         
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_INSERT), true);
                                                }
                                                else if (Trigger.isUnDelete && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_UNDELETE)){
                                                                                handler.beforeUnDelete(Trigger.oldmap);
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_UNDELETE), true);          
                                                }
                                                 
                                }
                                else{
                                               
                                                if (Trigger.isUpdate && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_UPDATE)){
                                                                                handler.afterUpdate(Trigger.oldmap, Trigger.newmap);                                                               
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_UPDATE), true);
                                                }
                                                else if (Trigger.isDelete && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_DELETE)){
                                                                                handler.afterDelete(Trigger.oldmap);
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_DELETE), true);
                                                }
                                                else if (Trigger.isInsert && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_INSERT)){
                                                                                handler.afterInsert(Trigger.newmap);                                                            
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_INSERT), true);
                                                }
                                                else if (Trigger.isUnDelete && !TriggerController.getTriggerControlValue(soType, TriggerController.TRIGGER_UNDELETE)){
                                                                                handler.afterUnDelete(Trigger.oldmap, Trigger.newmap);
                                                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(soType, TriggerController.TRIGGER_UNDELETE), true);
                                                }
                                }
                               
    }




public class TriggerController {

                public static map<TriggerControlKeyValue, boolean> triggerDisableMap = new map<TriggerControlKeyValue, boolean>();
                public static map<TriggerControlKeyValue, boolean> triggerSuccessMap = new map<TriggerControlKeyValue, boolean>();
               
                public static final String TRIGGER_ALL = 'ALL';
                public static final String TRIGGER_INSERT = 'INSERT';
                public static final String TRIGGER_UPDATE = 'UPDATE';
                public static final String TRIGGER_DELETE = 'DELETE';
                public static final String TRIGGER_UNDELETE = 'UNDELETE';
               
                public static Boolean getTriggerControlValue(System.Type objType, String triggerType){
                                TriggerControlKeyValue tkv = new TriggerControlKeyValue(objType ,triggerType);
                                Boolean triggerDisable = false;
                                if (triggerDisableMap != null && triggerDisableMap.containskey(tkv))
                                                triggerDisable = triggerDisableMap.get(tkv);
                                               
                                return triggerDisable;
                }

                public static void setTriggerControlValue(System.Type objType, String triggerType, Boolean triggerDisable){
                                TriggerControlKeyValue tkv = new TriggerControlKeyValue(objType ,triggerType);
                                                               
                                for (TriggerControlKeyValue eachtk : triggerDisableMap.keyset()){
                                                if (eachtk == tkv){
                                                                tkv = eachtk;                                                                       
                                                                break;
                                                }
                                }
                                triggerDisableMap.put(tkv, triggerDisable);                   
                }
               
                public static Boolean getTriggerSuccessValue(System.Type objType, String triggerType){
                                TriggerControlKeyValue tkv = new TriggerControlKeyValue(objType ,triggerType);
                                Boolean triggerSuccess = false;
                               
                                for (TriggerControlKeyValue eachtk : triggerSuccessMap.keyset()){
                                                if (eachtk == tkv){
                                                                triggerSuccess = triggerSuccessMap.get(eachtk);
                                                                break;
                                                }
                                }
                               
                                return triggerSuccess;
                }
               
               
public static boolean globalTriggerControlSetting(){
               
                return (((Triggers_Off__c.getOrgDefaults() != null) ? Triggers_Off__c.getOrgDefaults().value__c : false) || Triggers_Off__c.getInstance(UserInfo.getUserId()).value__c  || Triggers_Off__c.getInstance(UserInfo.getProfileId()).value__c) ;
}

public static boolean globalTriggerPerObjectControlSetting(String obj){
               
                if (obj != null && obj != '') {
                                if (!obj.endswith('__c')) obj += '__c';
                                                boolean s = false;
                                                if (Trigger_Per_Object__c.getOrgDefaults() != null) s =  (boolean)Trigger_Per_Object__c.getOrgDefaults().get(obj);

                                                boolean t = false;
                                                if (Trigger_Per_Object__c.getInstance(UserInfo.getUserId()) != null) t =  (boolean)Trigger_Per_Object__c.getInstance(UserInfo.getUserId()).get(obj);

                                                boolean u = false;
                                                if (Trigger_Per_Object__c.getInstance(UserInfo.getProfileId()) != null) u =  (boolean)Trigger_Per_Object__c.getInstance(UserInfo.getProfileId()).get(obj);

                                               
                                                if  (s == null) s = false;
                                                if  (t == null) t = false;
                                                if  (u == null) u = false;
                                               
                                                return (s || t || u);
                }else
                                return false;
}

}




public class TriggerControlKeyValue {
               
public system.type objectType;
public string triggerType;

                public TriggerControlKeyValue(system.type thisObjectType, string thisTriggerType) {
                                objectType = thisObjectType;
                                triggerType = thisTriggerType;
                }

               
                public boolean equals(object obj){
                               
                                if (obj instanceof TriggerControlKeyValue){
                                                TriggerControlKeyValue t = (TriggerControlKeyValue)obj;
                                                return (objectType.equals(t.objectType) && triggerType.equals(t.triggerType));
                                }
                                return false;
                }
               
                public integer hashCode(){
                                return system.hashCode(objectType) * system.hashCode(triggerType);
                }

}



We also need to be able to unit test the framework to test the trigger control has been built correctly and remains operational. To test this part of the framework we don’t want to test the outcomes from running each individual part of the trigger as the outcomes will be different per trigger, instead we just need to test that the code passed through the track of code we expect. For this purpose, another map is used.


@istest
public class TriggerControllerTest {
                public static TestDataCreation td = new TestDataCreation();
                public static Account acc;
                public static Triggers_Off__c trig;
                public static Trigger_Per_Object__c trigPerObject;
               
                static{
                                acc = td.insertAccount(null);
                }
               
                static testMethod void AccountTriggerGlobalCSTest() {
                                //test global CS on/off
                                trig = td.insertTriggersOff(null);
                                Test.startTest();
                                                //record should be inserted                                
                                                //system.assert([Select id From Account where Name=:defaultCusName].size() == 1);
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_INSERT) == true);
                                               
                                                //should change
                                                acc.Name = 'ChangeCusName';
                                                update acc;                                          
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE) == true);

                                                //reset                                    
                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE), false);
                                               
                                                trig.value__c = true;
                                                update trig;
                                               
                                                //should not change
                                                acc.Name = 'DefaultCusName';
                                                update acc;                                          
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE) == false);
                                               
                                                //disable insert
                                               
                                test.stopTest();
                }

                static testMethod void AccountTriggerPerObjectCSStaticTest() {
                                //test trigger control using Per Object CS
                                trigPerObject = td.insertTriggersPerObject(null);
                                Test.startTest();
                                                //reset
                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT), true);
                                               
                                                //record should be inserted but shouldnt set Account_Insert_Succeeded
                                                trigPerObject.Account__c = true;
                                                update trigPerObject;
                                               
                                                //reset                                    
                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT), false);
                                               
                                                system.assert([Select id From Account].size() == 1);
                                               
                                                Account acc2 = td.insertAccount(null);
               
                                                system.assert([Select id From Account].size() == 2);
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_INSERT) == false);

                                test.stopTest();
                }             

                static testMethod void AccountTriggerGlobalStaticTest() {
                                //test trigger control using static variables
                                Test.startTest();
                                                //disable update
                                                TriggerController.triggerDisableMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_ALL), true);
                                               
                                                acc.Name = 'ChangeCusName';
                                                update acc;                                          
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE) == false);

                                                //reset
                                                TriggerController.triggerDisableMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_ALL), false);
                                                TriggerController.triggerDisableMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE), true);
                                               
                                                acc.Name = 'DefaultCusName';
                                                update acc;                                          
                                               
                                                //should not change
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE) == false);
                                               
                                                //update should run
                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE), false);
                                                TriggerController.triggerDisableMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_UPDATE), false);
                                                acc.Name = 'DefaultCusName';
                                                update acc;
                                                system.debug('## TriggerController ' + TriggerController.triggerSuccessMap);                                       
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_UPDATE) == true);
                                               
                                                //test insert trigger code off
                                                TriggerController.triggerSuccessMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT), false);
                                                TriggerController.triggerDisableMap.put(new TriggerControlKeyValue(Account.class, TriggerController.TRIGGER_INSERT), true);
                                                system.assert([Select id From Account].size() == 1);
                                               
                                                Account acc2 = td.insertAccount(null);
               
                                                system.assert([Select id From Account].size() == 2);
                                                //should not change
                                                system.assert(TriggerController.getTriggerSuccessValue(Account.class,TriggerController.TRIGGER_INSERT) == false);
                                               
                                test.stopTest();
                }

}




Sunday, 21 February 2016

The Complete Unified Trigger Framework

It has been a while since I wrote a blog for my readers but Ive been working hard in developing a framework to improve your organisations in a big way.
I decided to turn my attention to triggers. There are many well published trigger frameworks and all have merits in that they improve the manageability of code, correctly orders execution, however as with everything good technologists will continually evolve a model. I was impressed with Tony Scotts http://developer.force.com/cookbook/recipe/trigger-pattern-for-tidy-streamlined-bulkified-triggers pattern as it simplifies the trigger.
Independent to Hari Krishnan https://krishhari.wordpress.com/tag/apex-trigger-design-pattern/ I too noticed some room for improvement because Tony’s framework would require continual adaption of the TriggerFactory for every new trigger that is developed. The solution that I came up with was basically the same as Hari.
However, I was concerned that all frameworks to date have only been designed to solve the old problem of code manageability and order of execution, but I always incorporated far more into my frameworks, notably the following additional features:

1.      Trigger Control
2.      Monitoring
3.      DML Consolidation


We will later explore these 3 facets of the framework in more detail. Lets first of all have an overview of the building blocks of the framework


The classes in Red make up the baseline of the framework. These classes do not need to be changed. The classes in Blue are classes that will be created for each Trigger. You can create as many "Logic" classes as you wish depending on the number of separate business areas and complexity of codebase in your organisation. The "Account Helper" class is also optional, this just aids the Logic classes and provides better modularised code.
Of course we must have  the starting Trigger as well, shown as "AllAccountTrigger" above.

In the next blog I will go into details of each class.

Friday, 19 February 2016

Watch out for Time Based Workflows

Time Based Workflows are very useful to action something in the future, which saves you writing scheduled jobs. But be careful, very careful.
I will tell you a story. I had a very critical Time Based Workflow which if it didnt fire as expected it would severely affect revenue, but it had always run very smoothly so there was no expectation of that changing.
Well we ended up creating a number of Time Based Workflows and typically each record would fire about 6 at different times. But since orders are coming in from many agents there were a lot of workflows firing. Unfortunately we not only hit our limit of 1000 per hour, but a large number were queueing up to enter the 1000 queue and so we could only see in monitoring section the same orders queueing and we thought that the Time Based Workflow had broken somehow.
So, if you want to control when your actions will fire relative to say the creation or update of a record of course Time Based Workflow are ideal, but if the queue is clogged your actions wont fire as expected. So what do you do?
You can take various actions, I will try to provide most cost effective methods to avoid coding which will be expensive:
  1. Create a scheduled report to keep a track of how many records you expect will be in the queue. This will match the criteria clause of your Time Based Workflow. If the report shows there are too many queued make sure you have a script that manually process any remaining records.
  2. Carefully calculate how many you expect to be entering the queue at any specific time so you can plan if you will be well within your limits or not. If you expect the limit to be broken simply put in a business case to Salesforce for this to be increased. They will listen
  3. If its not 100% necessary for all actions to occur at specific times relative to the workflow criteria then you can create a scheduled job to process any other records