Giter Site home page Giter Site logo

kevinohara80 / sfdc-trigger-framework Goto Github PK

View Code? Open in Web Editor NEW
903.0 102.0 502.0 45 KB

A minimal trigger framework for your Salesforce Apex Triggers

License: MIT License

Apex 100.00%
salesforce apex salesforce-developers salesforce-api apex-triggers

sfdc-trigger-framework's Introduction

SFDC trigger framework

npm version Maintainability

I know, I know...another trigger framework. Bear with me. ;)

Overview

Triggers should (IMO) be logicless. Putting logic into your triggers creates un-testable, difficult-to-maintain code. It's widely accepted that a best-practice is to move trigger logic into a handler class.

This trigger framework bundles a single TriggerHandler base class that you can inherit from in all of your trigger handlers. The base class includes context-specific methods that are automatically called when a trigger is executed.

The base class also provides a secondary role as a supervisor for Trigger execution. It acts like a watchdog, monitoring trigger activity and providing an api for controlling certain aspects of execution and control flow.

But the most important part of this framework is that it's minimal and simple to use.

Deploy to SFDX Scratch Org: Deploy

Deploy to Salesforce Org: Deploy

Usage

To create a trigger handler, you simply need to create a class that inherits from TriggerHandler.cls. Here is an example for creating an Opportunity trigger handler.

public class OpportunityTriggerHandler extends TriggerHandler {

In your trigger handler, to add logic to any of the trigger contexts, you only need to override them in your trigger handler. Here is how we would add logic to a beforeUpdate trigger.

public class OpportunityTriggerHandler extends TriggerHandler {
  
  public override void beforeUpdate() {
    for(Opportunity o : (List<Opportunity>) Trigger.new) {
      // do something
    }
  }

  // add overrides for other contexts

}

Note: When referencing the Trigger statics within a class, SObjects are returned versus SObject subclasses like Opportunity, Account, etc. This means that you must cast when you reference them in your trigger handler. You could do this in your constructor if you wanted.

public class OpportunityTriggerHandler extends TriggerHandler {

  private Map<Id, Opportunity> newOppMap;

  public OpportunityTriggerHandler() {
    this.newOppMap = (Map<Id, Opportunity>) Trigger.newMap;
  }
  
  public override void afterUpdate() {
    //
  }

}

To use the trigger handler, you only need to construct an instance of your trigger handler within the trigger handler itself and call the run() method. Here is an example of the Opportunity trigger.

trigger OpportunityTrigger on Opportunity (before insert, before update) {
  new OpportunityTriggerHandler().run();
}

Cool Stuff

Max Loop Count

To prevent recursion, you can set a max loop count for Trigger Handler. If this max is exceeded, and exception will be thrown. A great use case is when you want to ensure that your trigger runs once and only once within a single execution. Example:

public class OpportunityTriggerHandler extends TriggerHandler {

  public OpportunityTriggerHandler() {
    this.setMaxLoopCount(1);
  }
  
  public override void afterUpdate() {
    List<Opportunity> opps = [SELECT Id FROM Opportunity WHERE Id IN :Trigger.newMap.keySet()];
    update opps; // this will throw after this update
  }

}

Bypass API

What if you want to tell other trigger handlers to halt execution? That's easy with the bypass api:

public class OpportunityTriggerHandler extends TriggerHandler {
  
  public override void afterUpdate() {
    List<Opportunity> opps = [SELECT Id, AccountId FROM Opportunity WHERE Id IN :Trigger.newMap.keySet()];
    
    Account acc = [SELECT Id, Name FROM Account WHERE Id = :opps.get(0).AccountId];

    TriggerHandler.bypass('AccountTriggerHandler');

    acc.Name = 'No Trigger';
    update acc; // won't invoke the AccountTriggerHandler

    TriggerHandler.clearBypass('AccountTriggerHandler');

    acc.Name = 'With Trigger';
    update acc; // will invoke the AccountTriggerHandler

  }

}

If you need to check if a handler is bypassed, use the isBypassed method:

if (TriggerHandler.isBypassed('AccountTriggerHandler')) {
  // ... do something if the Account trigger handler is bypassed!
}

If you want to clear all bypasses for the transaction, simple use the clearAllBypasses method, as in:

// ... done with bypasses!

TriggerHandler.clearAllBypasses();

// ... now handlers won't be ignored!

Overridable Methods

Here are all of the methods that you can override. All of the context possibilities are supported.

  • beforeInsert()
  • beforeUpdate()
  • beforeDelete()
  • afterInsert()
  • afterUpdate()
  • afterDelete()
  • afterUndelete()

sfdc-trigger-framework's People

Contributors

charlieccharliec avatar kevinohara80 avatar shindegirish avatar tompatros avatar trangoul avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sfdc-trigger-framework's Issues

bulk data load being identified as recursive transactions

When implementing the setMaxLoopCount from the individual handlers, the loop count gets incremented after the first chunk of 200 records are processed as it identifies the subsequent set of records as recursive transactions. Need to identify a way around that by comparing the old set of records with the new ones and if it doesn't match, then not to increment. Else identify the correct way to clearMaxLoopCount.

Constructor 'gets hit' twice

public class MyObjectTriggerHandler {
public MyObjectTriggerHandler() {
system.debug('==>constructor'); // in the log I see it two times
this.setMaxLoopCount(1); //exception of course but I want to only run once!
}
...
}

trigger MyObjectTrigger on MyObject(all events) {
new MyObjectTriggerHandler().run();
}

Fails in bulk handling ..Batch Apex, Bulk API and DML @3500 rows

@kevinohara80
Framework is very expensive and consumes heavily Apex CPU time.
Inserted @3500 Accounts and updated using batch apex ,

Account has NO workflows, processbuilder, flows or validation rules, matching or merge duplicate rules.

The Account handler has empty override methods
public void beforeUpdate(){}
public void afterUpdate(){}
Log statistics below :
11:23:05.375 (14375373751)|CUMULATIVE_PROFILING|method invocations|
External entry point: public static void execute(): executed 1 time in 13741 ms
Trigger.AccountTrigger: line 4, column 1: public AccountTriggerHandler(): executed 64 times in 3584 ms
Class.TriggerHandler.getHandlerName: line 165, column 1: public static String valueOf(Object): executed 256 times in 3564 ms
Class.TriggerHandler.run: line 35, column 1: private void addToLoopCount(): executed 32 times in 1813 ms
Class.TriggerHandler.addToLoopCount: line 141, column 1: private String getHandlerName(): executed 32 times in 1811 ms
Class.TriggerHandler.run: line 33, column 1: private Boolean validateRun(): executed 32 times in 1762 ms
Class.TriggerHandler.validateRun: line 157, column 1: private String getHandlerName(): executed 64 times in 1761 ms
External entry point: public void invoke(): executed 1 time in 146 ms
Class.TriggerHandler.run: line 38, column 1: public static Integer compareObjects(Object, Object): executed 32 times in 1 ms
11:23:05.375 (14375373751)|CUMULATIVE_PROFILING_END

Number of SOQL queries: 1 out of 100
Number of query rows: 3026 out of 50000
Number of SOSL queries: 0 out of 20
Number of DML statements: 1 out of 150
Number of DML rows: 3026 out of 10000
Maximum CPU time: 7820 out of 10000 ******* CLOSE TO LIMIT
Maximum heap size: 0 out of 6000000
Number of callouts: 0 out of 100
Number of Email Invocations: 0 out of 10
Number of future calls: 0 out of 50
Number of queueable jobs added to the queue: 0 out of 50
Number of Mobile Apex push calls: 0 out of 10

Handling iteration

Inside every handler, every developer creates a new service method that iterates over the list of records and if the record matches the condition then execute the further logic. Now as project goes complex we end up adding more service method that reiterates over same set of records and executes logic again.

The problem here is , we end up iterating again and again, which eventually adds up to CPU timeout.

For eg: If a DML is performed for 200 records, and we have 8 service methods present, thus we end up iterating 200*8=1600 times while ideally we should only be iterating it 200times.

I have an idea that the TriggerHandler class should declare 6 new method prototypes.
beforeInsertIterator();
afterInsertIterator();
beforeUpdateIterator();
afterUpdateIterator();
beforeDeleteIterator();
afterDeleteIterator();

which would automatically force developer to write the filter-Condition logic inside those methods and which would populate a map based on those conditions.

This would help writing clean code and stop anyone writing from reiterating again and again.

Multiple Trigger Handlers per object

Do you recommend having multiple trigger handlers per object? or do you recommend using one handler per object that in turn calls different methods? I ask cause the main benefit i would love to be able to use the bypass api for handlers on the same trigger object but not sure that will be respected?

getLoopCount() ?

Is it by design, or just an oversight that there doesn't appear to be a way of seeing how many loops/recursions you are current in?

Would be useful to say "only run this on the outer most loop" etc.

Or have I missed the mark here?

Framework is not able to handle bulk dataset

Hi @kevinohara80
I am using the Trigger framework but stuck at a point where I have to deal with bulk data. The framework is consuming a lot of CPU time. The trigger doesn't have any complex logic it simply needs to set field value in before event for approx 4k records.

Can you please help me to get out of this exception.

Any help would be appreciated.

Thank You.

Bypass does not work

Hello,

I have multiple trigger using this framework and the byPass function does not seem to work.
Version of TriggerHandler: unknown (not added on org. by myself)
E.g.:

AccountTrigger:

new AccountTriggerHandler().run();

AccountTriggerHandler:

AccountTriggerLogic.process(Trigger.New);

AccountTriggerLogic:

public static void process(Account[] accounts) { System.debug('Inside account triggerlogic'); }

And there is multiple statements in my code I do:

TriggerHandler.bypass('AccountTriggerHandler');
update account;
TriggerHandler.clearBypass('AccountTriggerHandler');

In my logs I can see that the AccountTriggerHandler is not bypassed.
Is there any update to do ?

Performance concerns with getHandlerName

@kevinohara80 if you're interested, I can create a PR for your review.

We have run into performance issues with getHandlerName. Apparently executing String.valueOf(this) can take a long time to execute if the concrete handler class has a lot of data stored as properties (i.e. storing newList and newMap as properties on your handler class). We have reason to believe this has become more of an issue since Summer 22, but I have no hard evidence on that.

The best solution for us was to replace usage of handlerName Strings with Types throughout the framework. When using types

  • you get the benefits of compile-time errors
  • you optimize the performance of the framework
  • the performance does not vary by use case
  • you eliminate the possibility of throwing a heap exception, which can occur when the property data is too large to convert into a string.

Here are my test results.

Using Types

      Arrangement: handler with property = 200 sobjects, each with a 100k char description
      Call: handler.getHandlerType() 1000 times
      Result Timings: 238ms, 221ms, 194ms, 191ms

Using Strings

      Arrangement: handler with property = 200 sobjects, each with a 100k char description field
      Action: handler.getHandlerName() 1000 times
      Result: System.LimitException: Apex heap size too large: 48042718

      Arrangement: handler with property = 200 sobjects, each with a 10k char description
      Call: handler.getHandlerName() 1000 times
      Result Timings: 6379ms, 6323ms, 6135ms, 6268ms, 10994ms, 10628ms, 6232ms

Issue with addToLoopCount method

Hi Kevin,

I believe there's an issue with the addToLoopCount method, but only when the amount of records changed is greater than 200.

It's easier to explain with an example, so let's assume the following:

  • We implemented the framework in our org and we created AccountTriggerHandler than extends TriggerHandler class
  • We have the following code in the constructor, to ensure the handler executes only once:
public AccountTriggerHandler() {
    this.setMaxLoopCount(1);
}

Now if we run a DML on 600 Account records, it will results in 3 Trigger batches (in Trigger context, records are executed in sets of 200):

  • In the first batch of 200 Account, addToLoopCount will add AccountTriggerHandler and set the count to 1
  • In the second batch of 200 Accounts, addToLoopCount will throw an exception, since AccountTriggerHandler was already executed for the first batch. I don't believe this is the correct behaviour. While technically the AccountTriggerHandler was attempted twice (once for each batch), semantically the handler did NOT run twice (it just ran again for another batch). In fact, it didn't even finish the first run (it only processed the first 200 before failing on the next 200).
  • If we decided to catch the Exception instead of throwing it, we could potentially "silence" the trigger handler for the remaining batches. The impact of that could be quite substantial.

Therefore, I don't think we can keep track of execution counts per handler itself. It would need to be per record per handler I believe.

Cheers

max loop count

Please can ask a question about MaxLoopCount?

I am trying to use this to handle recursion which instead of maintaining a global variable "RunOnce", like it says it throws an exception. I don't really need this exception to be thrown. Am I missing the purpose of this method?

SF System Exception In Triggerhandler.getHandlerName() :191

Hello,

Love this frame work. Been using it recently. I noticed that every now and then my contact object throws a system exception during trigger validation. Specifically Triggerhandler.getHandlerName() :191

Salesforce System Error: 211775977-20872 (-1527031483) (-1527031483)

I opened up a case with sfdc support to see what that means.

They reported that under the hood java was stating:

Subject: 
Unsupported exception when doing a java call from apex
ExtendedMessage: 
Your call to com/salesforce/api/interop/apex/bcl/StringMethods:valueOf resulted in an exception of type: java.lang.IllegalStateException, which we do not wish to bubble up into apex.  Please trap it and convert to a HandledException or ExecutionException

Error : 
java.lang.IllegalStateException: Programmer error: Cannot compare savepoints from two different transactions

Apex Stack Trace At Last Failure:
 	TriggerHandler:getHandlerName():191
 	TriggerHandler:validateRun():186
 	TriggerHandler:run():31
 	__sfdc_trigger/contact_Trigger:invoke():12

Last SOQL query: SELECT Name, Processing_filter_ui_config__c, Processing_filter_soql__c, Filters_json__c, Default_map__c, For_territory__c, For_match__c FROM cls_Object__c WHERE Name IN ('Contact')

Which on my end references this snippet in the TriggerHandler.cls

       // make sure this trigger should continue to run
    @TestVisible
    private Boolean validateRun() {
        if (!this.isTriggerExecuting || this.context == null) {
            throw new TriggerHandlerException(
                'Trigger handler called outside of Trigger execution'
            );
        }
        return !TriggerHandler.bypassedHandlers.contains(getHandlerName());
    }

    @TestVisible
    private String getHandlerName() {
        return String.valueOf(this)
            .substring(0, String.valueOf(this).indexOf(':'));
    }

Wondering if anyone else has run into this problem / how they fixed it.

Fails in bulk handling ..Batch Apex, Bulk API and DML @3500 rows

@kevinohara80
Framework is very expensive and consumes heavily Apex CPU time.
Inserted @3500 Accounts and updated using batch apex ,

Account has NO workflows, processbuilder, flows or validation rules, matching or merge duplicate rules.

The Account handler has empty override methods
public void beforeUpdate(){}
public void afterUpdate(){}
Log statistics below :
11:23:05.375 (14375373751)|CUMULATIVE_PROFILING|method invocations|
External entry point: public static void execute(): executed 1 time in 13741 ms
Trigger.AccountTrigger: line 4, column 1: public AccountTriggerHandler(): executed 64 times in 3584 ms
Class.TriggerHandler.getHandlerName: line 165, column 1: public static String valueOf(Object): executed 256 times in 3564 ms
Class.TriggerHandler.run: line 35, column 1: private void addToLoopCount(): executed 32 times in 1813 ms
Class.TriggerHandler.addToLoopCount: line 141, column 1: private String getHandlerName(): executed 32 times in 1811 ms
Class.TriggerHandler.run: line 33, column 1: private Boolean validateRun(): executed 32 times in 1762 ms
Class.TriggerHandler.validateRun: line 157, column 1: private String getHandlerName(): executed 64 times in 1761 ms
External entry point: public void invoke(): executed 1 time in 146 ms
Class.TriggerHandler.run: line 38, column 1: public static Integer compareObjects(Object, Object): executed 32 times in 1 ms
11:23:05.375 (14375373751)|CUMULATIVE_PROFILING_END

Number of SOQL queries: 1 out of 100
Number of query rows: 3026 out of 50000
Number of SOSL queries: 0 out of 20
Number of DML statements: 1 out of 150
Number of DML rows: 3026 out of 10000
Maximum CPU time: 7820 out of 10000 ******* CLOSE TO LIMIT
Maximum heap size: 0 out of 6000000
Number of callouts: 0 out of 100
Number of Email Invocations: 0 out of 10
Number of future calls: 0 out of 50
Number of queueable jobs added to the queue: 0 out of 50
Number of Mobile Apex push calls: 0 out of 10

Fails in Batch Apex and anonymous execute , Bulk API

@kevinohara80
Framework is very expensive and consumes heavily Apex CPU time.
Inserted @3500 Accounts and updated using batch apex ,

Account has NO workflows, processbuilder, flows or validation rules, matching or merge duplicate rules.

The Account handler has empty override methods
public void beforeUpdate(){}
public void afterUpdate(){}
Log statistics below :
11:23:05.375 (14375373751)|CUMULATIVE_PROFILING|method invocations|
External entry point: public static void execute(): executed 1 time in 13741 ms
Trigger.AccountTrigger: line 4, column 1: public AccountTriggerHandler(): executed 64 times in 3584 ms
Class.TriggerHandler.getHandlerName: line 165, column 1: public static String valueOf(Object): executed 256 times in 3564 ms
Class.TriggerHandler.run: line 35, column 1: private void addToLoopCount(): executed 32 times in 1813 ms
Class.TriggerHandler.addToLoopCount: line 141, column 1: private String getHandlerName(): executed 32 times in 1811 ms
Class.TriggerHandler.run: line 33, column 1: private Boolean validateRun(): executed 32 times in 1762 ms
Class.TriggerHandler.validateRun: line 157, column 1: private String getHandlerName(): executed 64 times in 1761 ms
External entry point: public void invoke(): executed 1 time in 146 ms
Class.TriggerHandler.run: line 38, column 1: public static Integer compareObjects(Object, Object): executed 32 times in 1 ms
11:23:05.375 (14375373751)|CUMULATIVE_PROFILING_END

Number of SOQL queries: 1 out of 100
Number of query rows: 3026 out of 50000
Number of SOSL queries: 0 out of 20
Number of DML statements: 1 out of 150
Number of DML rows: 3026 out of 10000
Maximum CPU time: 7820 out of 10000 ******* CLOSE TO LIMIT
Maximum heap size: 0 out of 6000000
Number of callouts: 0 out of 100
Number of Email Invocations: 0 out of 10
Number of future calls: 0 out of 50
Number of queueable jobs added to the queue: 0 out of 50
Number of Mobile Apex push calls: 0 out of 10

Recurssion Control is not correct

Hi,
First of all I want to thank you Kevin for this great work.

In fact I discuvred some issue, the control of recurssion is based on the triggerHandler name not the exact event (Before Insert, Before Update, ...), so by this way, you w'll stop other events from running. Say for example if the before insert is running and by some way another event must run, so the control w'll stop it. we aleready encountred this scenarion.

Please find attached my proposed correction and tell me if you think I have right.
https://gist.github.com/fbouzeraa/29f6b84e7a93c3c39b20631b89a97f8d
Best regards,

Specify a Bypass by Type

It would be helpful to be able to specify a bypass by passing in a Type rather than a String.

Examples:
TriggerHandler.bypass(AccountTriggerHandler.class);
TriggerHandler.clearBypass(AccountTriggerHandler.class);
TriggerHandler.isBypassed(AccountTriggerHandler.class);

This would create a hard reference to the class being bypassed, so typos would no longer be a possibility and the bypasses could be updated or removed when renaming or deleting a trigger handler.

Storing bypassed handlers as Types instead of Strings might not be a bad idea as well, though it wouldn't be entirely necessary for the public bypass APIs.

Request for Enhancement: ignore Dummy Updates

Hi Kevin,

Can you validate and include new feature that ignore dummy updates on trigger.

here is the sample implementation:
trigger ApplicationTrigger on Application__C (before insert, before update, after insert, after update, before delete, after delete) { boolean anyChanges = true; if (Trigger.isUpdate) { anyChanges = !Trigger.oldMap.equals(Trigger.newMap); } if (anyChanges == true) { new ApplicationTriggerHelper().process(); } }

Siva Mamidi.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.